Search code examples
androidkotlinandroid-roomkotlin-flow

How to manually map a Room one-to-one relationship using Kotlin Flow


In my scenario there is a one-to-one relationship between transactions and categories.

In order to query the list of transactions and corresponding categories, documentation tells me i must first model the one-to-one relationship between the two entities (https://developer.android.com/training/data-storage/room/relationships) by creating a TransactionAndCategory class

I really don't want to do this, it is not clean and it really is bad coding imo. Therefore i tried associating these objects by myself but i haven't found a way to do it. Here's what i tried to do:

class TransactionRepositoryImpl(private val transactionDao: TransactionDao, private val categoryDao: CategoryDao) : TransactionRepository {

   override fun getAll(): Flow<List<Transaction>> {
    return transactionDao
        .getAll()
        .mapIterable{
            Transaction(it, categoryDao.get(it.categoryId))
        }
    }
}

This results in an error because categoryDao.get(it.categoryId) returns a flow itself:

@Dao
interface CategoryDao {
    @Query("SELECT * FROM categories WHERE uid = :id")
    fun get(id: Int): Flow<DatabaseCategory>
}

The mapIterable function is just an extension to 'unwrap' the list:

inline fun <T, R> Flow<Iterable<T>>.mapIterable(crossinline transform: (T) -> R): Flow<List<R>> =
map { it.map(transform) }

Transactions itself are retrieved like this from the TransactionDao:

fun getAll(): Flow<List<DatabaseTransaction>>

The constructor of my Transaction domain model takes two database models as arguments:

 constructor(databaseTransaction: DatabaseTransaction, databaseCategory: DatabaseCategory) 

I'm hoping someone with more Kotlin Flow experience than me has encountered the same issue and can provide me some ideas/insights/solutions on how to link these objects in a clean way without creating a TransactionAndCategory class.

EDIT: I tried suspending the categoryDao.get method:

@Query("SELECT * FROM categories WHERE uid = :id")
suspend fun get(id: Int): DatabaseCategory

override suspend fun getAll(): Flow<List<Transaction>> {
    return transactionDao
        .getAll()
        .mapIterable{
            Transaction(it, categoryDao.get(it.categoryId))
        }
}

Unfortunatly it's not possible to call categoryDao.get inside mapIterable, so i'm still stuck on this

compilation error: "suspension functions can only be called only within coroutine body"


Solution

  • As said in the comment, a room database can return an instance of your object, when you change Dao#get to a suspend fun Dao#get.

    In this example, CategoryDao#get should not return a Flow, but the object itself. Since we can't / shouldn't access the db on the main-thread, we use coroutines.

    Changing CategoryDao#get to suspend fun get(): DatabaseCategory and furthermore changing TransactionRepositoryImpl#getall to suspend fun getAll should solve your problems :)

    Edit

    override suspend fun getAll(): Flow<List<Transaction>> {
        return transactionDao
            .getAll()
            .map { list ->
               list.map {
                 Transaction(it, categoryDao.get(it.categoryId))
               }
            }
    }
    

    Or if you want to use your mapIterable function change it to the following

    inline fun <T, R> Flow<Iterable<T>>.mapIterable(crossinline transform: suspend (T) -> R): Flow<List<R>> =
        map { list ->
            list.map { item ->
                transform(item)
            }
        }
    
    
    override suspend fun getAll(): Flow<List<Transaction>> {
        return transactionDao
            .getAll()
            .mapIterable {
               Transaction(it, categoryDao.get(it.categoryId))
            }
    }