Kotlin Room withTransaction in Repository or @Transaction in Dao

Viewed 1332

I have a question regarding Room and it’s withTransaction { } code block in combination with Koin.

I have a repository where I need to access a couple of DAOs at the same time. I wanted to work with a withTransaction { } so I wouldn’t clutter 1 DAO with references to other DAOs. I’m not sure which object to inject in the constructor of my repository. The withTransaction{ } can only be accessed by getting the RoomDatabase. But having the RoomDatabase in my Repository means that I have access to all the DAOs connected to that RoomDatabase. I’m not sure what the best practice around this use-case would be.

Should I use the withTransaction { } and risk that all DAOs are accessible be that Repository or should I have the DAOs in my Repository's constructor and hand them to the `ReviewDao' to handle every insert?

An example would be something like this with withTransaction { }

class ReviewRepository(
    private val roomDatabase: RoomDatabase
) {
    private val reviewDao = roomDatabase.reviewDao()
    private val userDao = roomDatabase.userDao()

    suspend fun saveReview(reviewResponse: ReviewResponse) {
        roomDatabase.withTransaction {
            reviewDao.insert(reviewResponse.getAsEntity())
            userDao.insert(reviewResponse.user.getAsEntity())
        }
    }
}

And an example without withTransaction { } would be this

class ReviewRepository(
    private val reviewDao : ReviewDao,
    private val userDao : UserDao
) {
    suspend fun saveReview(reviewResponse: ReviewResponse) {
        reviewDao.insertWithUser(reviewResponse.getAsEntity(), reviewResponse.user.getAsEntity(), userDao)
    }
}

@Dao
interface ReviewDao {
    @Transaction
    suspend fun insertWithUser(review: Review, user: User, userDao: UserDao) {
        insert(review)
        userDao.insert(user)
    }
}
2 Answers

The solution that worked for me was to use a separate class that takes in the database in the constructor and provides an extension to the withTransaction.

class TransactionProvider(
    private val db: AppDatabase
) {
    suspend fun <R> runAsTransaction(block: suspend () -> R): R {
        return db.withTransaction(block)
    }
}

I can then inject this class into the Repository I need without providing the complete database object to that Repository.

I would go with a variant of your Repository solution. In a project, we had UseCase and Handler structures. To simply put;

class SaveReviewHandler(private val db: RoomDatabase) {
    private val reviewDao = db.reviewDao()
    private val userDao = db.userDao()

    suspend fun execute(useCase: SaveReview) {
        db.withTransaction {
            reviewDao.insert(useCase.review)
            userDao.insert(useCase.user)
        }
    }
}

data class SaveReview(review: ReviewEntitry, user: UserEntity)

and we can call it as follows:

saveReviewHandler.execute(SaveReview(reviewResponse.getAsEntity(),reviewResponse.user.getAsEntity()))

One good side of this architecture is; your repository may have other complex transactions, but it does not have to have direct access to DAOs.

Related