Room(Kotlin 多平台)

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

设置依赖项

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

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

[versions]
room = "2.7.2"
sqlite = "2.5.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 代码块中。请注意,您需要添加应用使用的所有目标平台。如需了解详情,请参阅 KSP 与 Kotlin Multiplatform

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 模块的通用源代码集中创建一个带有 @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 不兼容,因此 Room 的 setQueryExecutor() API 在 commonMain 中不可用。相反,必须使用 CoroutineContext 配置 RoomDatabase 对象,而 CoroutineContext 可以使用 RoomDatabase.Builder.setCoroutineContext() 进行设置。如果未设置任何上下文,RoomDatabase 对象将默认使用 Dispatchers.IO

缩减和混淆

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

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

迁移到 Kotlin Multiplatform

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

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

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

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

转换迁移子类

迁移子类需要迁移到 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 函数

如果使用 @RawQuery 注释的函数是为非 Android 平台编译的,则需要声明类型为 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() {  }

将响应式类型转换为 Flow

并非所有 DAO 函数都需要是挂起函数。返回 LiveData 或 RxJava 的 Flowable 等响应式类型的 DAO 函数不应转换为挂起函数。不过,某些类型(例如 LiveData)与 KMP 不兼容。具有响应式返回类型的 DAO 函数必须迁移到协程 flow。

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 的 API(带有查询回调 RoomDatabase.Builder.setQueryCallback)以及回调接口 RoomDatabase.QueryCallback 在 common 中不可用,因此在 Android 以外的其他平台中也不可用。

自动关闭数据库

用于在超时后启用自动关闭功能的 API RoomDatabase.Builder.setAutoCloseTimeout 仅在 Android 上可用,在其他平台上不可用。

预打包数据库

以下 API 用于使用现有数据库(即预打包数据库)创建 RoomDatabase,但这些 API 在通用平台中不可用,因此在 Android 以外的其他平台中也不可用。这些 API 包括:

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

我们打算在未来版本的 Room 中添加对预打包数据库的支持。

多实例失效

用于启用多实例失效的 API RoomDatabase.Builder.enableMultiInstanceInvalidation 仅在 Android 上可用,在通用平台或其他平台上不可用。