-
Notifications
You must be signed in to change notification settings - Fork 2
Abstract RoomDatabase.withTransaction
to allow Jvm implementation
#256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
9391eb6
to
2a5533c
Compare
2a5533c
to
cdd836e
Compare
Job Summary for GradleBuild & Test :: unit-tests
|
/gemini review this pr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces the TransactionRunner
abstraction to allow JVM implementation. The changes are solid, but there are a few areas in TaskRepository.kt
that could be improved for maintainability and correctness, especially around handling data returned from transactions and fixing a potential bug.
single<TransactionRunner> { | ||
object : TransactionRunner { | ||
override suspend fun <R> runInTransaction(logic: suspend () -> R): R { | ||
val db = get<TasksAppDatabase>() | ||
return db.withTransaction { | ||
logic() | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TasksAppDatabase
instance is resolved using get()
inside runInTransaction
, meaning it's resolved on every call. Resolving the dependency once when the TransactionRunner
is created would be more efficient.
single<TransactionRunner> {
val db = get<TasksAppDatabase>()
object : TransactionRunner {
override suspend fun <R> runInTransaction(logic: suspend () -> R): R {
return db.withTransaction(logic)
}
}
}
single<TransactionRunner> { | ||
object : TransactionRunner { | ||
override suspend fun <R> runInTransaction(logic: suspend () -> R): R { | ||
val db = get<TasksAppDatabase>() | ||
return db.useWriterConnection { transactor -> | ||
transactor.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { | ||
logic() | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TasksAppDatabase
for JVM resolves on every transaction. Optimizing by resolving it once when the TransactionRunner
is created would be more efficient.
single<TransactionRunner> {
val db = get<TasksAppDatabase>()
object : TransactionRunner {
override suspend fun <R> runInTransaction(logic: suspend () -> R): R {
return db.useWriterConnection { transactor ->
transactor.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE, logic)
}
}
}
}
val taskListEntity = transactionRunner.runInTransaction { | ||
requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" } | ||
.copy( | ||
title = newTitle, | ||
lastUpdateDate = now | ||
).also { taskListEntity -> | ||
taskListDao.upsert(taskListEntity) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of .also
with a lambda parameter taskListEntity
that shadows the outer variable being assigned can be confusing. Rewriting the code to be more explicit and easier to follow by using intermediate variables would improve readability and maintainability.
val taskListEntity = transactionRunner.runInTransaction { | |
requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" } | |
.copy( | |
title = newTitle, | |
lastUpdateDate = now | |
).also { taskListEntity -> | |
taskListDao.upsert(taskListEntity) | |
} | |
} | |
val taskListEntity = transactionRunner.runInTransaction { | |
val originalTaskListEntity = requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" } | |
val updatedTaskListEntity = originalTaskListEntity.copy( | |
title = newTitle, | |
lastUpdateDate = now | |
) | |
taskListDao.upsert(updatedTaskListEntity) | |
updatedTaskListEntity | |
} |
var taskId: Long = -1 | ||
val (taskListEntity, parentTaskEntity, taskEntity) = transactionRunner.runInTransaction { | ||
val taskListEntity = requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" } | ||
val parentTaskEntity = parentTaskId?.let { requireNotNull(taskDao.getById(it)) { "Invalid parent task id $parentTaskId" } } | ||
val now = nowProvider.now() | ||
val firstPosition = 0.toTaskPosition() | ||
val currentTasks = taskDao.getTasksFromPositionOnward(taskListId, parentTaskId, firstPosition) | ||
.toMutableList() | ||
val taskEntity = TaskEntity( | ||
parentListLocalId = taskListId, | ||
parentTaskLocalId = parentTaskId, | ||
title = title, | ||
notes = notes, | ||
lastUpdateDate = now, | ||
dueDate = dueDate, | ||
position = firstPosition, | ||
) | ||
// FIXME dedicated data structure for all returned data to avoid var & Triple | ||
val taskId_ = taskDao.insert(taskEntity) | ||
taskId = taskId_ | ||
if (currentTasks.isNotEmpty()) { | ||
val updatedTasks = computeTaskPositions(currentTasks, newPositionStart = 1) | ||
taskDao.upsertAll(updatedTasks) | ||
} | ||
Triple(taskListEntity, parentTaskEntity, taskEntity) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation uses a var taskId
which is modified as a side effect from within the transaction lambda. Refactor this to have the transaction block return all necessary data, avoiding side effects. Using a dedicated data class
for the return type is the cleanest solution.
val (transactionResult, taskId) = transactionRunner.runInTransaction {
val taskListEntity = requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" }
val parentTaskEntity = parentTaskId?.let { requireNotNull(taskDao.getById(it)) { "Invalid parent task id $parentTaskId" } }
val now = nowProvider.now()
val firstPosition = 0.toTaskPosition()
val currentTasks = taskDao.getTasksFromPositionOnward(taskListId, parentTaskId, firstPosition)
.toMutableList()
val taskEntity = TaskEntity(
parentListLocalId = taskListId,
parentTaskLocalId = parentTaskId,
title = title,
notes = notes,
lastUpdateDate = now,
dueDate = dueDate,
position = firstPosition,
)
val newTaskId = taskDao.insert(taskEntity)
if (currentTasks.isNotEmpty()) {
val updatedTasks = computeTaskPositions(currentTasks, newPositionStart = 1)
taskDao.upsertAll(updatedTasks)
}
Triple(taskListEntity, parentTaskEntity, taskEntity) to newTaskId
}
val (taskListEntity, parentTaskEntity, taskEntity) = transactionResult
val (taskListRemoteId, updatedTaskEntity) = transactionRunner.runInTransaction { | ||
val taskEntity = requireNotNull(taskDao.getById(taskId)) { "Invalid task id $taskId" } | ||
// FIXME early exit, how? | ||
// val updatedTaskEntity = updateLogic(taskEntity, now) ?: return | ||
val updatedTaskEntity = updateLogic(taskEntity, now) ?: error("TODO") | ||
|
||
taskDao.upsert(updatedTaskEntity) | ||
taskDao.upsert(updatedTaskEntity) | ||
|
||
val taskListRemoteId = taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId | ||
taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId to updatedTaskEntity | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle early exits from within a transaction block. A clean way to handle this is to have the transaction return a nullable result. If updateLogic
returns null
, the transaction block can also return null
.
val result = transactionRunner.runInTransaction {
val taskEntity = requireNotNull(taskDao.getById(taskId)) { "Invalid task id $taskId" }
updateLogic(taskEntity, now)?.let { updatedTaskEntity ->
taskDao.upsert(updatedTaskEntity)
taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId to updatedTaskEntity
}
} ?: return
val (taskListRemoteId, updatedTaskEntity) = result
@sourcery-ai review |
@coderabbitai review |
✅ Actions performedReview triggered.
|
Warning Rate limit exceeded@opatry has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 21 minutes and 13 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (7)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Reviewer's GuideThis PR introduces a TransactionRunner abstraction to unify transaction handling across platforms, refactors TaskRepository to wrap DAO operations in runInTransaction blocks, and updates DI modules and tests to provide platform-specific implementations. Sequence diagram for TaskRepository DAO operations with TransactionRunnersequenceDiagram
participant Client
participant TaskRepository
participant TransactionRunner
participant DAO
Client->>TaskRepository: call repository method (e.g., deleteTask)
TaskRepository->>TransactionRunner: runInTransaction { ... }
TransactionRunner->>DAO: perform DAO operations
DAO-->>TransactionRunner: result
TransactionRunner-->>TaskRepository: result
TaskRepository-->>Client: result
Class diagram for TransactionRunner abstraction and TaskRepository refactorclassDiagram
class TransactionRunner {
<<interface>>
+suspend runInTransaction(logic: suspend () -> R): R
}
class TaskRepository {
-transactionRunner: TransactionRunner
+deleteTaskList(taskListId: Long)
+renameTaskList(taskListId: Long, newTitle: String)
+clearTaskListCompletedTasks(taskListId: Long)
+createTask(taskListId: Long, parentTaskId: Long?, title: String, notes: String, dueDate: Instant?): Long
+deleteTask(taskId: Long)
+applyTaskUpdate(taskId: Long, updateLogic: suspend (TaskEntity, Instant) -> TaskEntity?)
+indentTask(taskId: Long)
+unindentTask(taskId: Long)
+moveToTop(taskId: Long)
+moveToList(taskId: Long, destinationListId: Long)
}
TaskRepository --> TransactionRunner
Class diagram for platform-specific TransactionRunner implementationsclassDiagram
class TransactionRunner {
<<interface>>
+suspend runInTransaction(logic: suspend () -> R): R
}
class AndroidTransactionRunner {
+suspend runInTransaction(logic: suspend () -> R): R
}
class JvmTransactionRunner {
+suspend runInTransaction(logic: suspend () -> R): R
}
TransactionRunner <|.. AndroidTransactionRunner
TransactionRunner <|.. JvmTransactionRunner
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @opatry - I've reviewed your changes - here's some feedback:
- TaskRepository’s runInTransaction blocks use mutable vars and Triple/Pair destructuring with FIXMEs—extract domain-specific result data classes or helper methods to eliminate the var/Triple pattern and improve readability.
- There’s duplicated upsert-and-position-recompute logic across multiple repository methods—consider abstracting the fetch-update-upsert sequence into a reusable higher-order function to reduce boilerplate.
- The JVM TransactionRunner uses IMMEDIATE transactions while Android uses the default type—review and align these choices to ensure consistent isolation semantics across platforms.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- TaskRepository’s runInTransaction blocks use mutable vars and Triple/Pair destructuring with FIXMEs—extract domain-specific result data classes or helper methods to eliminate the var/Triple pattern and improve readability.
- There’s duplicated upsert-and-position-recompute logic across multiple repository methods—consider abstracting the fetch-update-upsert sequence into a reusable higher-order function to reduce boilerplate.
- The JVM TransactionRunner uses IMMEDIATE transactions while Android uses the default type—review and align these choices to ensure consistent isolation semantics across platforms.
## Individual Comments
### Comment 1
<location> `tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt:447` </location>
<code_context>
+ val (taskListEntity, parentTaskEntity, taskEntity) = transactionRunner.runInTransaction {
</code_context>
<issue_to_address>
The use of a mutable var (taskId) outside the transaction block is error-prone.
Returning taskId as part of the Triple and using destructuring will help avoid mutable state and reduce the risk of concurrency or retry-related bugs.
Suggested implementation:
```
val (taskListEntity, parentTaskEntity, taskId) = transactionRunner.runInTransaction {
val taskListEntity = requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" }
val parentTaskEntity = parentTaskId?.let { requireNotNull(taskDao.getById(it)) { "Invalid parent task id $parentTaskId" } }
val now = nowProvider.now()
val firstPosition = 0.toTaskPosition()
val currentTasks = taskDao.getTasksFromPositionOnward(taskListId, parentTaskId, firstPosition)
.toMutableList()
val taskEntity = TaskEntity(
parentListLocalId = taskListId,
parentTaskLocalId = parentTaskId,
title = title,
)
val insertedTaskId = taskDao.insert(taskEntity)
Triple(taskListEntity, parentTaskEntity, insertedTaskId)
```
If you need to use `taskEntity` after the transaction, you can return a Quadruple or a data class containing all needed values instead of a Triple. Adjust the destructuring and return accordingly.
</issue_to_address>
### Comment 2
<location> `tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt:526` </location>
<code_context>
- val updatedTaskEntity = updateLogic(taskEntity, now) ?: return
+ val (taskListRemoteId, updatedTaskEntity) = transactionRunner.runInTransaction {
+ val taskEntity = requireNotNull(taskDao.getById(taskId)) { "Invalid task id $taskId" }
+ // FIXME early exit, how?
+// val updatedTaskEntity = updateLogic(taskEntity, now) ?: return
+ val updatedTaskEntity = updateLogic(taskEntity, now) ?: error("TODO")
- taskDao.upsert(updatedTaskEntity)
</code_context>
<issue_to_address>
The workaround for early exit in runInTransaction is inelegant and may cause confusion.
Using error("TODO") as a placeholder will crash the app if updateLogic returns null. Please refactor runInTransaction or updateLogic to handle early returns or nulls more safely.
</issue_to_address>
### Comment 3
<location> `tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt:640` </location>
<code_context>
+ val taskListRemoteId = taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId
- val taskListRemoteId = taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId
+ // FIXME
+ previousSubtaskRemoteId = previousSubtaskRemoteId_
+ Triple(taskListRemoteId, parentTaskEntity, updatedTaskEntity)
+ }
if (taskListRemoteId != null && updatedTaskEntity.remoteId != null) {
</code_context>
<issue_to_address>
The use of a mutable variable (previousSubtaskRemoteId) outside the transaction block is fragile.
Returning all necessary values from the transaction block avoids potential bugs and improves code clarity.
Suggested implementation:
```
val (taskListRemoteId, parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId) = withContext(Dispatchers.IO) {
// ... your transaction block code ...
val taskListRemoteId_ = taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId
// ... compute parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId_ ...
Triple(taskListRemoteId_, parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId_)
}
val task = /* ... use taskListRemoteId, parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId as needed ... */
try {
}
```
- You will need to ensure that the transaction block returns a data structure (such as a data class or a tuple) containing all the required values: `taskListRemoteId`, `parentTaskEntity`, `updatedTaskEntity`, and `previousSubtaskRemoteId`.
- Update all usages of these variables after the transaction block to use the unpacked values.
- Remove any mutable variable declarations for `previousSubtaskRemoteId` outside the transaction block.
- If you need more than three values, consider using a data class instead of `Triple`.
</issue_to_address>
### Comment 4
<location> `tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt:793` </location>
<code_context>
+ taskDao.upsertAll(updatedTaskEntities)
+ }
+
+ // FIXME should already be available in entity, quick & dirty workaround
+ val newTaskListRemoteId = destinationTaskListEntity.remoteId
+ val taskListRemoteId = taskListDao.getById(taskEntity.parentListLocalId)?.remoteId
- // FIXME should already be available in entity, quick & dirty workaround
- val newTaskListRemoteId = destinationTaskListEntity.remoteId
- val taskListRemoteId = taskListDao.getById(taskEntity.parentListLocalId)?.remoteId
- if (taskListRemoteId != null && taskEntity.remoteId != null && newTaskListRemoteId != null) {
+ Triple(taskListRemoteId, newTaskListRemoteId, updatedTaskEntity)
+ }
+ if (taskListRemoteId != null && updatedTaskEntity.remoteId != null && newTaskListRemoteId != null) {
</code_context>
<issue_to_address>
Fetching remoteId from the database after upsert is inefficient and may be inconsistent.
Fetching remoteId after upsert risks inconsistencies if the entity changes in between. Please ensure remoteId is reliably available in the updated entity or refactor the model to eliminate the need for this extra lookup.
Suggested implementation:
```
// Use remoteId directly from the in-memory entities to avoid extra DB lookup
val newTaskListRemoteId = destinationTaskListEntity.remoteId
val taskListRemoteId = taskEntity.parentListRemoteId
Triple(taskListRemoteId, newTaskListRemoteId, updatedTaskEntity)
}
if (taskListRemoteId != null && updatedTaskEntity.remoteId != null && newTaskListRemoteId != null) {
```
- Ensure that `TaskEntity` has a `parentListRemoteId` property that is always set when the entity is loaded or updated. If this property does not exist, you will need to add it and ensure it is populated wherever `TaskEntity` is constructed or updated.
- Remove any other unnecessary lookups for `remoteId` from the database in similar contexts.
</issue_to_address>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
val (taskListEntity, parentTaskEntity, taskEntity) = transactionRunner.runInTransaction { | ||
val taskListEntity = requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" } | ||
val parentTaskEntity = parentTaskId?.let { requireNotNull(taskDao.getById(it)) { "Invalid parent task id $parentTaskId" } } | ||
val now = nowProvider.now() | ||
val firstPosition = 0.toTaskPosition() | ||
val currentTasks = taskDao.getTasksFromPositionOnward(taskListId, parentTaskId, firstPosition) | ||
.toMutableList() | ||
val taskEntity = TaskEntity( | ||
parentListLocalId = taskListId, | ||
parentTaskLocalId = parentTaskId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): The use of a mutable var (taskId) outside the transaction block is error-prone.
Returning taskId as part of the Triple and using destructuring will help avoid mutable state and reduce the risk of concurrency or retry-related bugs.
Suggested implementation:
val (taskListEntity, parentTaskEntity, taskId) = transactionRunner.runInTransaction {
val taskListEntity = requireNotNull(taskListDao.getById(taskListId)) { "Invalid task list id $taskListId" }
val parentTaskEntity = parentTaskId?.let { requireNotNull(taskDao.getById(it)) { "Invalid parent task id $parentTaskId" } }
val now = nowProvider.now()
val firstPosition = 0.toTaskPosition()
val currentTasks = taskDao.getTasksFromPositionOnward(taskListId, parentTaskId, firstPosition)
.toMutableList()
val taskEntity = TaskEntity(
parentListLocalId = taskListId,
parentTaskLocalId = parentTaskId,
title = title,
)
val insertedTaskId = taskDao.insert(taskEntity)
Triple(taskListEntity, parentTaskEntity, insertedTaskId)
If you need to use taskEntity
after the transaction, you can return a Quadruple or a data class containing all needed values instead of a Triple. Adjust the destructuring and return accordingly.
// FIXME early exit, how? | ||
// val updatedTaskEntity = updateLogic(taskEntity, now) ?: return | ||
val updatedTaskEntity = updateLogic(taskEntity, now) ?: error("TODO") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): The workaround for early exit in runInTransaction is inelegant and may cause confusion.
Using error("TODO") as a placeholder will crash the app if updateLogic returns null. Please refactor runInTransaction or updateLogic to handle early returns or nulls more safely.
// FIXME | ||
previousSubtaskRemoteId = previousSubtaskRemoteId_ | ||
Triple(taskListRemoteId, parentTaskEntity, updatedTaskEntity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The use of a mutable variable (previousSubtaskRemoteId) outside the transaction block is fragile.
Returning all necessary values from the transaction block avoids potential bugs and improves code clarity.
Suggested implementation:
val (taskListRemoteId, parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId) = withContext(Dispatchers.IO) {
// ... your transaction block code ...
val taskListRemoteId_ = taskListDao.getById(updatedTaskEntity.parentListLocalId)?.remoteId
// ... compute parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId_ ...
Triple(taskListRemoteId_, parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId_)
}
val task = /* ... use taskListRemoteId, parentTaskEntity, updatedTaskEntity, previousSubtaskRemoteId as needed ... */
try {
}
- You will need to ensure that the transaction block returns a data structure (such as a data class or a tuple) containing all the required values:
taskListRemoteId
,parentTaskEntity
,updatedTaskEntity
, andpreviousSubtaskRemoteId
. - Update all usages of these variables after the transaction block to use the unpacked values.
- Remove any mutable variable declarations for
previousSubtaskRemoteId
outside the transaction block. - If you need more than three values, consider using a data class instead of
Triple
.
// FIXME should already be available in entity, quick & dirty workaround | ||
val newTaskListRemoteId = destinationTaskListEntity.remoteId | ||
val taskListRemoteId = taskListDao.getById(taskEntity.parentListLocalId)?.remoteId | ||
|
||
// FIXME should already be available in entity, quick & dirty workaround | ||
val newTaskListRemoteId = destinationTaskListEntity.remoteId | ||
val taskListRemoteId = taskListDao.getById(taskEntity.parentListLocalId)?.remoteId | ||
if (taskListRemoteId != null && taskEntity.remoteId != null && newTaskListRemoteId != null) { | ||
Triple(taskListRemoteId, newTaskListRemoteId, updatedTaskEntity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Fetching remoteId from the database after upsert is inefficient and may be inconsistent.
Fetching remoteId after upsert risks inconsistencies if the entity changes in between. Please ensure remoteId is reliably available in the updated entity or refactor the model to eliminate the need for this extra lookup.
Suggested implementation:
// Use remoteId directly from the in-memory entities to avoid extra DB lookup
val newTaskListRemoteId = destinationTaskListEntity.remoteId
val taskListRemoteId = taskEntity.parentListRemoteId
Triple(taskListRemoteId, newTaskListRemoteId, updatedTaskEntity)
}
if (taskListRemoteId != null && updatedTaskEntity.remoteId != null && newTaskListRemoteId != null) {
- Ensure that
TaskEntity
has aparentListRemoteId
property that is always set when the entity is loaded or updated. If this property does not exist, you will need to add it and ensure it is populated whereverTaskEntity
is constructed or updated. - Remove any other unnecessary lookups for
remoteId
from the database in similar contexts.
Description
RoomDatabase.withTransaction
to allow Jvm implementation usingTransactor
Checklist
See https://developer.android.com/training/data-storage/room/room-kmp-migration#sqlite-driver-to_1
Room Android
Room KMP
expect
/actual
and just always rely on the KMP version even for Android.Summary by Sourcery
Introduce a TransactionRunner abstraction to centralize database transactions and wrap all TaskRepository operations in transactions, and provide platform-specific implementations via DI modules for JVM and Android while updating tests and DI definitions to accommodate the new interface.
Enhancements:
Tests: