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

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

依存関係をセットアップする

KMP をサポートする Room の現在のバージョンは 2.7.0-alpha01 以降です。

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

  • androidx.room:room-gradle-plugin - Room スキーマを構成する Gradle プラグイン
  • androidx.room:room-compiler - コードを生成する KSP プロセッサ
  • androidx.room:room-runtime - ライブラリのランタイム部分
  • androidx.sqlite:sqlite-bundled - (省略可)バンドルされた SQLite ライブラリ

また、Room の SQLite ドライバを構成する必要があります。これらのドライバは、ターゲット プラットフォームによって異なります。使用可能なドライバ実装の説明については、ドライバの実装をご覧ください。

設定の詳細については、以下をご覧ください。

データベース クラスの定義

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

インターフェース RoomDatabaseConstructor を使用して expect オブジェクトを宣言すると、Room コンパイラは actual 実装を生成します。Android Studio から警告 "Expected object 'AppDatabaseConstructor' has no actual declaration in module" が出される場合がありますが、@Suppress("NO_ACTUAL_FOR_EXPECT") で警告を抑制できます。

// 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("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

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

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

必要に応じて、actual / expect 宣言を使用して、プラットフォーム固有の Room 実装を作成できます。たとえば、expect を使用して共通コードで定義されたプラットフォーム固有の DAO を追加し、プラットフォーム固有のソースセットで追加のクエリを使用して actual 定義を指定できます。

データベース ビルダーを作成する

各プラットフォームで Room をインスタンス化するためのデータベース ビルダーを定義する必要があります。これは、ファイル システム API の違いにより、プラットフォーム固有のソースセットに含める必要がある API の唯一の部分です。たとえば、Android では通常、データベースの場所は Context.getDatabasePath() API を介して取得されますが、iOS では NSFileManager を使用して取得されます。

Android

データベース インスタンスを作成するには、データベース パスとともにコンテキストを指定します。

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

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

iOS

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

// shared/src/iosMain/kotlin/Database.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.kt

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

圧縮と難読化

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

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

データベースのインスタンス化

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

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

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

SQLiteDriver の選択

上記のコード スニペットでは BundledSQLiteDriver を使用しています。これは、ソースからコンパイルされた SQLite を含む推奨ドライバです。すべてのプラットフォームで最も一貫性があり、最新バージョンの SQLite を提供します。OS 提供の SQLite を使用する場合は、プラットフォーム固有のドライバを指定するプラットフォーム固有のソースセットで setDriver API を使用します。Android の場合は AndroidSQLiteDriver を使用し、iOS の場合は NativeSQLiteDriver を使用します。NativeSQLiteDriver を使用するには、iOS アプリがシステム SQLite と動的にリンクするように、リンカー オプションを指定する必要があります。

// 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")
        }
    }
}

相違点

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

ブロッキング DAO 関数

Room for KMP を使用する場合、Android 以外のプラットフォーム用にコンパイルされたすべての DAO 関数は、Flow などのリアクティブな戻り値の型を除き、suspend 関数にする必要があります。

// shared/src/commonMain/kotlin/MultiplatformDao.kt

@Dao
interface MultiplatformDao {
  // ERROR: Blocking function not valid for non-Android targets
  @Query("SELECT * FROM Entity")
  fun blockingQuery(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  suspend fun query(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  fun queryFlow(): Flow<List<Entity>>

  // ERROR: Blocking function not valid for non-Android targets
  @Transaction
  fun blockingTransaction() { // … }

  // OK
  @Transaction
  suspend fun transaction() { // … }
}

Room は、Kotlin が複数のプラットフォームで提供する機能豊富な非同期 kotlinx.coroutines ライブラリを利用しています。最適な機能のために、既存のコードベースとの下位互換性を維持する Android 固有の DAO を除き、KMP プロジェクトでコンパイルされた DAO に suspend 関数が適用されます。

KMP との機能の違い

このセクションでは、Room の KMP バージョンと Android プラットフォーム バージョンの機能の違いについて説明します。

@RawQuery DAO 関数

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

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

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

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

クエリ コールバック

クエリ コールバックを構成するための次の 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 でのみ使用でき、一般的なプラットフォームや他のプラットフォームでは使用できません。