KMP 用に Room データベースを設定する

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

依存関係を設定する

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

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

[versions]
room = "2.8.4"
sqlite = "2.6.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)
}

KSP の依存関係をルート dependencies ブロックに追加します。アプリで使用するターゲットをすべて追加する必要があります。詳しくは、Kotlin Multiplatform で 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 モジュールの共通ソースセット内に、DAO とエンティティとともに @Database アノテーション付きのデータベース クラスを作成する必要があります。これらのクラスを共通ソースに配置すると、すべてのターゲット プラットフォームで共有できます。

// 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 Multiplatform に移行する

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

Support SQLite から SQLite ドライバに移行する

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

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

Room SQLite Wrapper を使用する(省略可)

androidx.room:room-sqlite-wrapper アーティファクトは、移行中に SQLiteDriverSupportSQLiteDatabase の間をブリッジする API を提供します。

SQLiteDriver で構成された RoomDatabase から SupportSQLiteDatabase を取得するには、新しい拡張関数 RoomDatabase.getSupportWrapper() を使用します。この互換性ラッパーは、SQLiteDriver を採用しながら、SupportSQLiteDatabase の既存の使用箇所(多くの場合、RoomDatabase.openHelper.writableDatabase から取得)を維持するのに役立ちます。特に、BundledSQLiteDriver を使用する SupportSQLite API の使用箇所が多いコードベースの場合に役立ちます。

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

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

Kotlin Multiplatform

移行サブクラス

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 Multiplatform

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 Multiplatform

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

@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 ライブラリを利用しています。最適な機能を実現するため、既存のコードベースとの下位互換性を維持するために androidMain で実装された DAO を除き、KMP プロジェクトでコンパイルされた DAO には suspend 関数が適用されます。KMP で Room を使用する場合、Android 以外のプラットフォーム用にコンパイルされたすべての DAO 関数は suspend 関数にする必要があります。

Kotlin Multiplatform

クエリの一時停止

@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 Multiplatform

リアクティブ型 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 Multiplatform

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: Write-Ahead Logging(WAL)モード (デフォルト)では、このタイプのトランザクションは開始時にロックを取得しますが、 読み取り元は引き続き読み取ることができます。ほとんどの場合、これが推奨されます。

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

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

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

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

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

Kotlin Multiplatform では使用できない

Android で使用できた API の一部は、Kotlin Multiplatform では使用できません。

クエリ コールバック

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

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

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

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

データベースの自動クローズ

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

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

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

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

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

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

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