Room(Kotlin マルチプラットフォーム)

Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、データベースへのより安定したアクセスを可能にし、SQLite を最大限に活用できるようにします。このページでは、Kotlin マルチプラットフォーム(KMP)プロジェクトで Room を使用する方法について説明します。Room の使用方法について詳しくは、Room を使用してローカル データベースにデータを保存するまたは公式サンプルをご覧ください。

依存関係を設定する

KMP プロジェクトで Room を設定するには、KMP モジュールの build.gradle.kts ファイルにアーティファクトの依存関係を追加します。

libs.versions.toml ファイルで依存関係を定義します。

[versions]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"

[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room", version.ref = "room" }

Room Gradle プラグインを追加して Room スキーマと KSP プラグインを構成します。

plugins {
  alias(libs.plugins.ksp)
  alias(libs.plugins.androidx.room)
}

Room ランタイムの依存関係とバンドルされた SQLite ライブラリを追加します。

commonMain.dependencies {
  implementation(libs.androidx.room.runtime)
  implementation(libs.androidx.sqlite.bundled)
}

// Optional when using Room SQLite Wrapper
androidMain.dependencies {
  implementation(libs.androidx.room.sqlite.wrapper)
}

ルート dependencies ブロックに KSP 依存関係を追加します。アプリで使用するすべてのターゲットを追加する必要があります。詳しくは、Kotlin マルチプラットフォームでの KSP をご覧ください。

dependencies {
    add("kspAndroid", libs.androidx.room.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    add("kspIosX64", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.compiler)
    // Add any other platform target you use in your project, for example kspDesktop
}

Room スキーマ ディレクトリを定義します。詳しくは、Room Gradle プラグインを使用してスキーマの場所を設定するをご覧ください。

room {
    schemaDirectory("$projectDir/schemas")
}

データベース クラスを定義する

共有 KMP モジュールの共通ソースセット内に、@Database アノテーションを付けたデータベース クラスと DAO、エンティティを作成する必要があります。これらのクラスを共通ソースに配置すると、すべてのターゲット プラットフォームで共有できるようになります。

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
}

// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

インターフェース RoomDatabaseConstructor を使用して expect オブジェクトを宣言すると、Room コンパイラによって actual 実装が生成されます。Android Studio で次の警告が表示される場合がありますが、@Suppress("KotlinNoActualForExpect") を使用して抑制できます。

Expected object 'AppDatabaseConstructor' has no actual declaration in module`

次に、新しい DAO インターフェースを定義するか、既存のインターフェースを commonMain に移動します。

// shared/src/commonMain/kotlin/TodoDao.kt

@Dao
interface TodoDao {
  @Insert
  suspend fun insert(item: TodoEntity)

  @Query("SELECT count(*) FROM TodoEntity")
  suspend fun count(): Int

  @Query("SELECT * FROM TodoEntity")
  fun getAllAsFlow(): Flow<List<TodoEntity>>
}

エンティティを定義するか、commonMain に移動します。

// shared/src/commonMain/kotlin/TodoEntity.kt

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

プラットフォーム固有のデータベース ビルダーを作成する

各プラットフォームで Room をインスタンス化するには、データベース ビルダーを定義する必要があります。ファイル システム API の違いにより、プラットフォーム固有のソースセットに含める必要があるのは、API のこの部分だけです。

Android

Android では、通常、Context.getDatabasePath() API を介してデータベースの場所を取得します。データベース インスタンスを作成するには、データベース パスとともに Context を指定します。

// shared/src/androidMain/kotlin/Database.android.kt

fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = context.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

iOS でデータベース インスタンスを作成するには、NSFileManager を使用してデータベース パスを指定します。通常、これは NSDocumentDirectory にあります。

// shared/src/iosMain/kotlin/Database.ios.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = documentDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
    )
}

private fun documentDirectory(): String {
  val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
    directory = NSDocumentDirectory,
    inDomain = NSUserDomainMask,
    appropriateForURL = null,
    create = false,
    error = null,
  )
  return requireNotNull(documentDirectory?.path)
}

JVM(パソコン)

データベース インスタンスを作成するには、Java または Kotlin API を使用してデータベース パスを指定します。

// shared/src/jvmMain/kotlin/Database.desktop.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

データベースをインスタンス化する

プラットフォーム固有のコンストラクタのいずれかから RoomDatabase.Builder を取得したら、実際のデータベースのインスタンス化とともに、共通コードで残りの Room データベースを構成できます。

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

SQLite ドライバを選択する

前のコード スニペットでは、setDriver ビルダー関数を呼び出して、Room データベースで使用する SQLite ドライバを定義しています。これらのドライバは、ターゲット プラットフォームによって異なります。上記のコード スニペットでは BundledSQLiteDriver を使用しています。これは、ソースからコンパイルされた SQLite を含む推奨ドライバであり、すべてのプラットフォームで最も一貫性のある最新バージョンの SQLite を提供します。

OS 提供の SQLite を使用する場合は、プラットフォーム固有のドライバを指定するプラットフォーム固有のソースセットで setDriver API を使用します。利用可能なドライバ実装の説明については、ドライバ実装をご覧ください。次のいずれかを使用できます。

NativeSQLiteDriver を使用するには、iOS アプリがシステム SQLite と動的にリンクするように、リンカー オプション -lsqlite3 を指定する必要があります。

// shared/build.gradle.kts

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "TodoApp"
            isStatic = true
            // Required when using NativeSQLiteDriver
            linkerOpts.add("-lsqlite3")
        }
    }
}

コルーチン コンテキストを設定する(省略可)

Android の RoomDatabase オブジェクトは、必要に応じて RoomDatabase.Builder.setQueryExecutor() を使用して共有アプリケーション エグゼキュータで構成し、データベース オペレーションを実行できます。

実行プログラムは KMP と互換性がないため、Room の setQueryExecutor() API は commonMain で使用できません。代わりに、RoomDatabase オブジェクトを CoroutineContext で構成する必要があります。これは RoomDatabase.Builder.setCoroutineContext() を使用して設定できます。コンテキストが設定されていない場合、RoomDatabase オブジェクトはデフォルトで Dispatchers.IO を使用します。

軽量化と難読化

プロジェクトが縮小または難読化されている場合は、Room がデータベース定義の生成された実装を見つけられるように、次の ProGuard ルールを含める必要があります。

-keep class * extends androidx.room.RoomDatabase { <init>(); }

Kotlin マルチプラットフォームに移行する

Room は元々 Android ライブラリとして開発されましたが、API の互換性を重視して KMP に移行されました。Room の KMP バージョンは、プラットフォーム間や Android 固有のバージョンと若干異なります。これらの違いは、次のとおりです。

Support SQLite から SQLite Driver に移行する

androidx.sqlite.db での SupportSQLiteDatabase やその他の API の使用は、SQLite Driver API でリファクタリングする必要があります。androidx.sqlite.db の API は Android 専用であるためです(KMP パッケージとは異なるパッケージであることに注意してください)。

下位互換性を確保するため、RoomDatabaseSupportSQLiteOpenHelper.Factory で構成されている(たとえば SQLiteDriver が設定されていない)限り、Room は「互換モード」で動作し、Support SQLite API と SQLite Driver API の両方が想定どおりに動作します。これにより、増分移行が可能になり、すべての Support SQLite の使用を 1 回の変更で SQLite Driver に変換する必要がなくなります。

移行サブクラスを変換する

移行サブクラスは、SQLite ドライバの対応するクラスに移行する必要があります。

Kotlin マルチプラットフォーム

移行サブクラス

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // …
  }
}

自動移行仕様のサブクラス

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // …
  }
}

Android のみ

移行サブクラス

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // …
  }
}

自動移行仕様のサブクラス

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // …
  }
}

データベース コールバックの変換

データベース コールバックを SQLite ドライバの対応するコールバックに移行する必要があります。

Kotlin マルチプラットフォーム

object MyRoomCallback : RoomDatabase.Callback() {
  override fun onCreate(connection: SQLiteConnection) {
    // …
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // …
  }

  override fun onOpen(connection: SQLiteConnection) {
    // …
  }
}

Android のみ

object MyRoomCallback : RoomDatabase.Callback() {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // …
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // …
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // …
  }
}

@RawQuery DAO 関数を変換する

Android 以外のプラットフォーム用にコンパイルされる @RawQuery でアノテーションが付けられた関数は、SupportSQLiteQuery ではなく RoomRawQuery 型のパラメータを宣言する必要があります。

Kotlin マルチプラットフォーム

未加工のクエリを定義する

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}

RoomRawQuery を使用して、実行時にクエリを作成できます。

suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
    val query = RoomRawQuery(
        sql = "SELECT * FROM TodoEntity WHERE title = ?",
        onBindStatement = {
            it.bindText(1, title.lowercase())
        }
    )

    return todoDao().getTodos(query)
}

Android のみ

未加工のクエリを定義する

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}

SimpleSQLiteQuery を使用して、実行時にクエリを作成できます。

suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = SimpleSQLiteQuery(
      query = "SELECT * FROM TodoEntity WHERE title = ?",
      bindArgs = arrayOf(title.lowercase())
  )
  return getTodos(query)
}

ブロッキング DAO 関数を変換する

Room は、Kotlin が複数のプラットフォーム向けに提供する機能豊富な非同期 kotlinx.coroutines ライブラリのメリットを享受しています。機能を最適化するため、KMP プロジェクトでコンパイルされた DAO には suspend 関数が適用されます。ただし、既存のコードベースとの下位互換性を維持するために androidMain で実装された DAO は除きます。KMP で Room を使用する場合、Android 以外のプラットフォーム用にコンパイルされたすべての DAO 関数は suspend 関数である必要があります。

Kotlin マルチプラットフォーム

クエリの一時停止

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

トランザクションの一時停止

@Transaction
suspend fun transaction() {  }

Android のみ

ブロックされたクエリ

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

トランザクションのブロック

@Transaction
fun blockingTransaction() {  }

リアクティブ型を Flow に変換する

すべての DAO 関数が suspend 関数である必要はありません。LiveData や RxJava の Flowable などのリアクティブ型を返す DAO 関数は、suspend 関数に変換しないでください。ただし、LiveData などの一部のタイプは KMP と互換性がありません。リアクティブな戻り値の型を持つ DAO 関数は、コルーチン フローに移行する必要があります。

Kotlin マルチプラットフォーム

リアクティブ タイプ Flows

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

Android のみ

LiveData や RxJava の Flowable などのリアクティブ型

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

トランザクション API を変換する

Room KMP のデータベース トランザクション API では、書き込み(useWriterConnection)トランザクションと読み取り(useReaderConnection)トランザクションを区別できます。

Kotlin マルチプラットフォーム

val database: RoomDatabase = 
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Android のみ

val database: RoomDatabase = 
database.withTransaction {
  // perform database operations in transaction
}

書き込みトランザクション

書き込みトランザクションを使用して、複数のクエリがデータをアトミックに書き込むようにします。これにより、リーダーはデータに一貫してアクセスできます。これを行うには、次の 3 種類のトランザクション タイプのいずれかで useWriterConnection を使用します。

  • immediateTransaction: ログ先行書き込み(WAL)モード(デフォルト)では、このタイプのトランザクションは開始時にロックを取得しますが、リーダーは読み取りを続行できます。ほとんどの場合、この方法が推奨されます。

  • deferredTransaction: トランザクションは、最初の書き込みステートメントまでロックを取得しません。このタイプのトランザクションは、トランザクション内で書き込みオペレーションが必要になるかどうかが不明な場合に、最適化として使用します。たとえば、プレイリストの名前だけを指定してプレイリストから曲を削除するトランザクションを開始し、そのプレイリストが存在しない場合、書き込み(削除)オペレーションは必要ありません。

  • exclusiveTransaction: このモードの動作は、WAL モードの immediateTransaction と同じです。他のジャーナリング モードでは、トランザクションの実行中に他のデータベース接続がデータベースを読み取れないようにします。

読み取りトランザクション

読み取りトランザクションを使用して、データベースから複数回一貫して読み取ります。たとえば、2 つ以上の個別のクエリがあり、JOIN 句を使用していない場合などです。リーダー接続では、遅延トランザクションのみが許可されます。リーダー接続で即時トランザクションまたは排他トランザクションを開始しようとすると、例外がスローされます。これらは「書き込み」オペレーションとみなされるためです。

val database: RoomDatabase = 
database.useReaderConnection { transactor ->
  transactor.deferredTransaction {
      // perform database operations in transaction
  }
}

Kotlin マルチプラットフォームでは利用不可

Android で利用可能だった API の一部は、Kotlin Multiplatform では利用できません。

クエリ コールバック

クエリ コールバックを構成するための次の API は共通ではなく、Android 以外のプラットフォームでは使用できません。

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Room の今後のバージョンで、クエリ コールバックのサポートを追加する予定です。

クエリ コールバック RoomDatabase.Builder.setQueryCallback とコールバック インターフェース RoomDatabase.QueryCallback を使用して RoomDatabase を構成する API は、共通では利用できないため、Android 以外のプラットフォームでは利用できません。

Auto Closing Database

タイムアウト後に自動的に閉じることを有効にする API RoomDatabase.Builder.setAutoCloseTimeout は、Android でのみ使用でき、他のプラットフォームでは使用できません。

事前パッケージ化されたデータベース

既存のデータベース(あらかじめパッケージ化されたデータベースなど)を使用して RoomDatabase を作成する次の API は common では利用できないため、Android 以外のプラットフォームでは利用できません。これらの API は次のとおりです。

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

Room の今後のバージョンで、事前パッケージ化されたデータベースのサポートを追加する予定です。

マルチインスタンスの無効化

マルチインスタンスの無効化を有効にする API RoomDatabase.Builder.enableMultiInstanceInvalidation は Android でのみ使用でき、共通プラットフォームや他のプラットフォームでは使用できません。