Room(Kotlin 多平台)

Room 持久性库在 SQLite 之上提供了一个抽象层,以实现更强大的数据库访问,同时充分利用 SQLite 的全部功能。本页面重点介绍如何在 Kotlin 多平台 (KMP) 项目中使用 Room。如需详细了解如何使用 Room,请参阅使用 Room 将数据保存到本地数据库或我们的官方示例

设置依赖项

要在 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
)

请注意,您可以选择使用实际 / 预期声明来创建平台特定的 Room 实现。例如,您可以添加一个在公共代码中使用 expect 定义的平台特定 DAO,然后在平台特定的源集中使用额外的查询指定 actual 定义。

创建数据库构建器

您需要定义一个数据库构建器,以便在每个平台上实例化 Room。由于文件系统 API 的差异,这是唯一需要位于平台特定源集中的 API 部分。例如,在 Android 中,数据库位置通常通过 Context.getDatabasePath() API 获取,而在 iOS 中,数据库位置则使用 NSFileManager 获取。

Android

要创建数据库实例,请指定一个 Context 和数据库路径。

// 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,该 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 函数

当在 KMP 中使用 Room 时,为非 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 的功能差异

本节描述了 Room 的 KMP 和 Android 平台版本之间功能的不同之处。

@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 在公共代码中不可用,因此在 Android 以外的平台中也无法使用。

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

我们计划在 Room 的未来版本中添加对查询回调的支持。

用于使用查询回调 RoomDatabase.Builder.setQueryCallback 配置 RoomDatabase 的 API 以及回调接口 RoomDatabase.QueryCallback 在公共代码中不可用,因此在 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 上可用,在公共代码或其他平台中不可用。