このドキュメントでは、既存の Room 実装を Kotlin マルチプラットフォーム(KMP)を使用する実装に移行する方法について説明します。
既存の Android コードベース内の Room の使用状況を共通の共有 KMP モジュールに移行する作業の難易度は、使用する Room API や、コードベースですでにコルーチンを使用しているかどうかによって大きく異なります。このセクションでは、Room の使用を共通のモジュールに移行する際のガイダンスとヒントについて説明します。
まず、Android バージョンの Room と KMP バージョンの相違点と不足している機能、および関連する設定を理解することが重要です。基本的に、移行を成功させるには、SupportSQLite*
API の使用をリファクタリングし、それを SQLite Driver API に置き換えるとともに、Room 宣言(@Database
アノテーション付きのクラス、DAO、エンティティなど)を共通のコードに移動する必要があります。
続行する前に、以下の情報を再度確認してください。
以降のセクションでは、移行を成功させるために必要な手順について説明します。
Support SQLite から SQLite ドライバへの移行
androidx.sqlite.db
の API は Android 専用であり、使用する際は SQLite Driver API でリファクタリングする必要があります。下位互換性のため、また、RoomDatabase
が SupportSQLiteOpenHelper.Factory
で構成されている(つまり、SQLiteDriver
が設定されていない)限り、Room は「互換モード」で動作し、Support SQLite API と SQLite Driver API の両方が期待どおりに動作します。これにより段階的な移行が可能になるため、1 回の変更で Support SQLite の使用をすべて SQLite ドライバに変換する必要がなくなります。
次の例は、サポート SQLite とそれに対応する SQLite ドライバの一般的な使用例です。
SQLite をサポートする(from)
結果のないクエリを実行する
val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")
結果が返され、引数なしでクエリを実行する
val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
while (cusor.moveToNext()) {
// read columns
cursor.getInt(0)
cursor.getString(1)
}
}
結果と引数を指定してクエリを実行する
database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
if (cursor.moveToNext()) {
// row found, read columns
} else {
// row not found
}
}
SQLite ドライバ(変換先)
結果のないクエリを実行する
val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")
結果が返され、引数なしでクエリを実行する
val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
while (statement.step()) {
// read columns
statement.getInt(0)
statement.getText(1)
}
}
結果と引数を指定してクエリを実行する
connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
statement.bindInt(1, id)
if (statement.step()) {
// row found, read columns
} else {
// row not found
}
}
データベース トランザクション API は、beginTransaction()
、setTransactionSuccessful()
、endTransaction()
により、SupportSQLiteDatabase
で直接使用できます。runInTransaction()
を使用すると、Room からも削除されます。これらの使用方法を SQLite Driver API に移行します。
SQLite をサポートする(from)
トランザクションを実行する(RoomDatabase
を使用)
val database: RoomDatabase = ...
database.runInTransaction {
// perform database operations in transaction
}
トランザクションを実行する(SupportSQLiteDatabase
を使用)
val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
// perform database operations in transaction
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
SQLite ドライバ(変換先)
トランザクションを実行する(RoomDatabase
を使用)
val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
トランザクションを実行する(SQLiteConnection
を使用)
val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
// perform database operations in transaction
connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
connection.execSQL("ROLLBACK TRANSACTION")
}
さまざまなコールバック オーバーライドも、対応するドライバに移行する必要があります。
SQLite をサポートする(from)
移行のサブクラス
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// ...
}
}
自動移行仕様のサブクラス
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// ...
}
}
データベースのコールバック サブクラス
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(db: SupportSQLiteDatabase) {
// ...
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// ...
}
override fun onOpen(db: SupportSQLiteDatabase) {
// ...
}
}
SQLite ドライバ(変換先)
移行のサブクラス
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// ...
}
}
自動移行仕様のサブクラス
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// ...
}
}
データベースのコールバック サブクラス
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(connection: SQLiteConnection) {
// ...
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// ...
}
override fun onOpen(connection: SQLiteConnection) {
// ...
}
}
まとめると、コールバックのオーバーライド(onMigrate
、onCreate
など)など、RoomDatabase
が利用できない場合は、SQLiteDatabase
の使用を SQLiteConnection
に置き換えます。RoomDatabase
が使用可能な場合は、RoomDatabase.openHelper.writtableDatabase
ではなく RoomDatabase.useReaderConnection
と RoomDatabase.useWriterConnection
を使用して、基盤となるデータベース接続にアクセスします。
ブロッキング DAO 関数を suspend 関数に変換する
KMP バージョンの Room は、コルーチンを使用して、構成された CoroutineContext
に対する I/O オペレーションを実行します。つまり、ブロッキング DAO 関数をすべて移行して suspend 関数にする必要があります。
ブロッキング DAO 関数(from)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
DAO 関数の一時停止(to)
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
既存のコードベースにコルーチンがまだ組み込まれていない場合、既存の DAO ブロッキング関数を suspend 関数に移行する作業は複雑になる可能性があります。コードベースでコルーチンを使用する方法については、Android のコルーチンをご覧ください。
リアクティブな戻り値の型を Flow に変換する
すべての DAO 関数を suspend 関数である必要はありません。LiveData
や RxJava の Flowable
などのリアクティブ型を返す DAO 関数は、suspend 関数に変換しないでください。ただし、LiveData
などの一部の型は KMP と互換性がありません。リアクティブな戻り値の型を持つ DAO 関数は、コルーチン フローに移行する必要があります。
互換性のない KMP タイプ(移行元)
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
互換性のある KMP タイプ(to)
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
コードベースで Flow を使用する方法については、Android のフローをご覧ください。
コルーチンのコンテキストを設定する(省略可)
必要に応じて、RoomDatabase.Builder.setQueryExecutor()
を使用して共有アプリケーション エグゼキュータで RoomDatabase
を構成し、データベース オペレーションを実行できます。エグゼキュータは KMP と互換性がないため、Room の setQueryExecutor()
API は一般的なソースでは使用できません。代わりに、CoroutineContext
を使用して RoomDatabase
を構成する必要があります。コンテキストは RoomDatabase.Builder.setCoroutineContext()
を使用して設定できます。設定されていない場合、RoomDatabase
はデフォルトで Dispatchers.IO
を使用します。
SQLite ドライバを設定する
サポート SQLite の使用を SQLite Driver API に移行したら、RoomDatabase.Builder.setDriver
を使用してドライバを構成する必要があります。推奨されるドライバは BundledSQLiteDriver
です。使用可能なドライバの実装については、ドライバの実装をご覧ください。
RoomDatabase.Builder.openHelperFactory()
を使用して構成されたカスタム SupportSQLiteOpenHelper.Factory
は KMP でサポートされていません。カスタム オープン ヘルパーによって提供される機能は、SQLite ドライバ インターフェースを使用して再実装する必要があります。
会議室の申告を移動する
ほとんどの移行ステップが完了したら、Room の定義を共通のソースセットに移動できます。expect
/ actual
戦略を使用すると、Room 関連の定義を段階的に移動できます。たとえば、すべてのブロッキング DAO 関数を suspend 関数に移行できるわけではない場合は、expect
@Dao
アノテーション付きのインターフェースを宣言できます。このインターフェースは、一般的なコードでは空ですが、Android にはブロッキング関数が含まれています。
// shared/src/commonMain/kotlin/Database.kt
@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getDao(): TodoDao
abstract fun getBlockingDao(): BlockingTodoDao
}
@Dao
interface TodoDao {
@Query("SELECT count(*) FROM TodoEntity")
suspend fun count(): Int
}
@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt
@Dao
actual interface BlockingTodoDao {
@Query("SELECT count(*) FROM TodoEntity")
fun count(): Int
}