Room を Kotlin Multiplaform に移行する

このドキュメントでは、既存の 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 でリファクタリングする必要があります。下位互換性のため、また、RoomDatabaseSupportSQLiteOpenHelper.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) {
    // ...
  }
}

まとめると、コールバックのオーバーライド(onMigrateonCreate など)など、RoomDatabase が利用できない場合は、SQLiteDatabase の使用を SQLiteConnection に置き換えます。RoomDatabase が使用可能な場合は、RoomDatabase.openHelper.writtableDatabase ではなく RoomDatabase.useReaderConnectionRoomDatabase.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
}