Room (Kotlin Multiplatform)

Room 持續性資料庫是 SQLite 提供的抽象層,讓您在充分運用 SQLiteg 的強大效能時,也能順暢且穩定地存取資料庫。本頁重點介紹如何在 Kotlin Multiplatform (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 驅動程式。這些驅動程式會因目標平台而異。如要瞭解可用的驅動程式實作方式,請參閱「驅動程式實作」。

如需其他設定資訊,請參閱下列資源:

定義資料庫類別

您需要建立使用 @Database 註解的資料庫類別,以及共用 KMP 模組的通用來源集內的 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
)

請注意,您可以選擇使用實際 / 預期宣告,建立特定平台的 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,
    )
}

壓縮和模糊處理

如果專案已經過精簡或模糊處理,則必須加入下列 ProGuard 規則,讓 Room 能夠找到資料庫定義的產生實作項目:

-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 版本。如果您想使用作業系統提供的 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 程式庫開發,後來才遷移至 KMP,以 API 相容性為重點。Room 的 KMP 版本與平台和 Android 專屬版本有所不同。以下列出並說明這些差異。

阻斷 DAO 函式

使用 Room for KMP 時,為非 Android 平台編譯的所有 DAO 函式都必須是 suspend 函式,但回應傳回類型 (例如 Flow) 除外。

// 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 程式庫中受益。為提供最佳功能,系統會針對在 KMP 專案中編譯的 DAO 強制執行 suspend 函式,但 Android 專屬 DAO 除外,以便維持與現有程式碼集的回溯相容性。

與 KMP 的功能差異

本節將說明 KMP 與 Android 平台版本的 Room 之間的功能差異。

@RawQuery DAO 函式

針對非 Android 平台編譯的函式,如果附有 @RawQuery 註解,就必須宣告 RoomRawQuery 類型的參數,而非 SupportSQLiteQuery

@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 不適用於 common,因此不適用於 Android 以外的平台。

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

我們預計在日後推出的 Room 版本中,新增對查詢回呼的支援。

用於設定 RoomDatabase 的 API 與查詢回呼 RoomDatabase.Builder.setQueryCallback 和回呼介面 RoomDatabase.QueryCallback 皆不屬於常見類別,因此無法在 Android 以外的其他平台上使用。

自動關閉資料庫

RoomDatabase.Builder.setAutoCloseTimeout 是用於在逾時後啟用自動關閉功能的 API,僅適用於 Android,無法在其他平台上使用。

預先封裝資料庫

以下 API 可用於使用現有資料庫 (即預先封裝的資料庫) 建立 RoomDatabase,但不包含在 common 中,因此無法在 Android 以外的其他平台上使用。這些 API 如下:

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

我們預計在日後推出的 Room 版本中,新增對預先封裝資料庫的支援。

多實體失效

啟用多個例項無效化的 API RoomDatabase.Builder.enableMultiInstanceInvalidation 僅適用於 Android,不適用於常見或其他平台。