🗄

RoomのDaoからwithTransactionしやすくする

2022/02/23に公開

Roomのsuspend関数とwithTransaction

AndroidX RoomのDAOで定義したsuspend関数の実行は RoomDatabase クラスの拡張関数として定義されている withTransaction ブロック内で実行することが推奨されています。

正しくは「withTransaction ブロック内で呼び出されるDAO関数はすべてサスペンド関数であることが推奨されている」でした。

なのでこの記事では単にDAOから RoomDatabase に簡単にアクセスするための方法を紹介する記事になります ><

It is recommended that all Dao function invoked within the block be suspending functions.

androidx.room | Android Developers

問題点

DAOは以下のようにインターフェースで定義している場合が多いと思います。

UserDao.kt
@Dao
interface UserDao {
    @Insert
    suspend fun insertAll(vararg users: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAll(): List<User>
}

RoomDatabase とDAOを同じ場所で扱える場合は特に問題はありませんが、DIなどでDAOのみを渡しているようなクラスでは RoomDatabase にアクセスすることができません。
この場合、DAOの代わりに RoomDatabase を直接渡して利用側でDAOを取得するなどして解決できますが、本記事ではDAOから RoomDatabase にアクセスする方法を考えていきます。

DAOから RoomDatabase にアクセスする

Dao アノテーションのドキュメントには以下の記述があります。

An abstract @Dao class can optionally have a constructor that takes a Database as its only parameter.

Dao | Android Developers

インターフェースではなく抽象クラスでDAOを定義する場合、 Database をコンストラクトパラメータにすることが可能なようです。
次のようにDAOクラスを書き換えてみます。Databaseクラスに変更は必要ありません。

UserDao.kt
@Dao
abstract class UserDao(val database: RoomDatabase) {
    @Insert
    abstract suspend fun insertAll(vararg users: User)

    @Delete
    abstract suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    abstract suspend fun getAll(): List<User>
}

こうすることによって、利用側ではDAOから RoomDatabase を取得できるようになりました。

dao.database.withTransaction {
    dao.getAll()
}

withTransaction の呼び出しを簡素化する

先述の例だと dao の呼び出しがくどいのでDAOクラスに関数を追加します。

UserDao.kt
@Dao
abstract class UserDao(val database: RoomDatabase) {
    // 省略..

    suspend fun <R> withTransaction(block: suspend UserDao.() -> R): R {
        return database.withTransaction {
            block(this)
        }
    }
}

DAOクラスに withTransaction 関数を追加することで呼び出し側がだいぶ簡素になります。

dao.withTransaction {
    getAll()
}

しかし、これだとDAOクラス全てに withTransaction 関数を追加していく必要がありちょっとつらいので次のようなインターフェースと拡張関数を定義します。

internal interface Transactionable {
    val database: RoomDatabase
}

suspend fun <R, T : Transactionable> T.withTransaction(block: suspend T.() -> R): R {
    return database.withTransaction {
        block(this)
    }
}

あとは、DAOクラスに先程追加した関数を削除して Transactionable インターフェースを継承する形にすればOK

UserDao.kt
@Dao
abstract class UserDao(override val database: RoomDatabase) : Transactionable {
    @Insert
    abstract suspend fun insertAll(vararg users: User)

    @Delete
    abstract suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    abstract suspend fun getAll(): List<User>
}

Discussion