为 KMP 设置 Room 数据库

Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。本页重点介绍如何在 Kotlin Multiplatform (KMP) 项目中使用 Room。如需详细了解如何使用 Room,请参阅使用 Room 将数据保存到本地数据库或 我们的 官方示例

设置依赖项

如需在 KMP 项目中设置 Room,请为 KMP 模块的 build.gradle.kts 文件中的工件添加依赖项。

libs.versions.toml 文件中定义依赖项:

[versions]
room3 = "3.0.1"
sqlite = "2.7.0"
ksp = "<kotlinCompatibleKspVersion>"

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

# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }

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

添加 Room Gradle 插件以配置 Room 架构和 KSP 插件

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

添加 Room 运行时依赖项和捆绑的 SQLite 库:

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

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

将 KSP 依赖项添加到 dependencies 块。您需要添加应用使用的所有目标。如需了解详情,请参阅将 KSP 与 Kotlin Multiplatform搭配使用。

dependencies {
    add("kspAndroid", libs.androidx.room3.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
    add("kspIosX64", libs.androidx.room3.compiler)
    add("kspIosArm64", libs.androidx.room3.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 版本。

如果您想使用操作系统提供的 SQLite,请在指定特定于平台的驱动程序的特定于平台的源代码集中使用 setDriver API。如需了解可用驱动程序 实现的说明,请参阅 驱动程序实现。您可以使用以下任一选项:

如需使用 NativeSQLiteDriver,您需要提供链接器选项 -lsqlite3,以便 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")
        }
    }
}

设置协程上下文

Android 上的 RoomDatabase 对象可以选择使用 RoomDatabase.Builder.setQueryExecutor 配置共享 应用执行器,以执行 数据库操作。

由于执行器与 KMP 不兼容,因此 commonMain 中不提供 Room 的 setQueryExecutor API。相反,必须使用 CoroutineContext 配置 RoomDatabase 对象,您可以使用 RoomDatabase.Builder.setCoroutineContext 进行设置。如果您未设置上下文,RoomDatabase 对象将默认使用 Dispatchers.IO

缩减和混淆

如果项目经过 缩减或混淆处理,您必须添加 以下 ProGuard 规则,以便 Room 可以找到生成的 数据库定义实现:

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

迁移到 Kotlin Multiplatform

Room 最初是作为 Android 库开发的,后来迁移到了 KMP,重点是 API 兼容性。Room 的 KMP 版本在不同平台之间以及与 Android 专用版本之间存在一些差异。这些差异如下所列和说明。

从支持 SQLite 迁移到 SQLite 驱动程序

SupportSQLiteDatabaseandroidx.sqlite.db 中的其他 API 的任何用法都需要使用 SQLite 驱动程序 API 进行重构, 因为 androidx.sqlite.db 中的 API 仅适用于 Android(请注意与 KMP 软件包不同的软件包)。

为了实现向后兼容性,只要 RoomDatabase 配置了 SupportSQLiteOpenHelper.Factory(例如,未设置 SQLiteDriver),Room 就会以“兼容模式”运行,其中支持 SQLite 和 SQLite 驱动程序 API 均按预期运行。这样可以实现增量迁移,因此您无需在一次更改中将所有支持 SQLite 用法转换为 SQLite 驱动程序。

使用 Room SQLite 封装容器

androidx.room3:room3-sqlite-wrapper 工件提供 API,以便在迁移期间在 SQLiteDriverSupportSQLiteDatabase 之间建立桥梁。

如需从配置了 SQLiteDriverRoomDatabase 获取 SupportSQLiteDatabase,请使用新的扩展函数 RoomDatabase.getSupportWrapper。此兼容性封装容器有助于维护 现有用法(您通常从 RoomDatabase.openHelper.writableDatabase 获取),同时采用 SQLiteDriver, 尤其适用于具有大量 SupportSQLite API 用法且想要 使用 BundledSQLiteDriver 的代码库。SupportSQLiteDatabase

转换迁移子类

迁移子类需要迁移到 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 注解的函数需要声明类型为 RoomRawQuery 的参数,而不是 SupportSQLiteQuery

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 库。为了获得最佳功能,对于在 KMP 项目中编译的 DAO,系统会强制使用 suspend 函数,但 androidMain 中实现的 DAO 除外,以保持与现有代码库的向后兼容性。将 Room 用于 KMP 时,针对非 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() {  }

将响应式类型转换为流

并非所有 DAO 函数都需要是挂起函数。返回响应式类型(例如 LiveData 或 RxJava 的 Flowable)的 DAO 函数不应转换为挂起函数。不过,某些类型(例如 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
}

写入事务

使用写入事务可确保多个查询以原子方式写入数据,以便读取器可以一致地访问数据。您可以使用 useWriterConnection 以及以下三种事务类型中的任何一种来执行此操作:

  • immediateTransaction:在 预写式日志 (WAL) 模式 (默认)下,此类事务会在启动时获取锁,但 读取器可以继续读取。在大多数情况下,这是首选方案。

  • deferredTransaction:事务在第一个写入语句之前不会获取锁。如果您不确定事务中是否需要写入操作,请使用此类事务进行优化。例如,如果您启动事务以仅根据播放列表的名称从播放列表中删除歌曲,并且播放列表不存在,则不需要写入(删除)操作。

  • exclusiveTransaction:此模式在 WAL 模式下的行为与 immediateTransaction 相同。在其他日志记录模式下,它会阻止其他数据库连接在事务进行期间读取数据库。

读取事务

使用读取事务可以一致地从数据库中读取多次。例如,当您有两个或多个单独的查询且未使用 JOIN 子句时。读取器连接中仅允许延迟事务。尝试在读取器连接中启动立即事务或独占事务会抛出异常,因为这些事务被视为“写入”操作。

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的 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 上可用,在通用代码或其他平台中不可用。