将 Room 迁移到 Kotlin Multiplaform

本文档介绍了如何将现有 Room 实现迁移到使用 Kotlin 多平台 (KMP) 的 Room 实现。

将现有 Android 代码库中的 Room 使用情况迁移到通用共享 KMP 模块的难度可能很大,具体取决于使用的 Room API 或代码库是否已使用协程。本部分针对尝试将 Room 的使用迁移到通用模块提供了一些指导和提示。

请务必先熟悉 Android 版本 Room 与 KMP 版本之间的区别和缺少的功能,以及所涉及的设置。实质上,成功迁移涉及重构 SupportSQLite* API 的用法,将其替换为 SQLite 驱动程序 API,并将 Room 声明(带 @Database 注解的类、DAO、实体等)移到通用代码中。

继续操作前,请再次查看以下信息:

以下部分将介绍成功迁移所需的各种步骤。

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

androidx.sqlite.db 中的 API 仅适用于 Android,其任何用法都需要使用 SQLite 驱动程序 API 进行重构。为了实现向后兼容性,并且只要 RoomDatabase 配置了 SupportSQLiteOpenHelper.Factory(即未设置 SQLiteDriver),Room 就会处于“兼容模式”,在该模式下,支持 SQLite 和 SQLite 驱动程序 API 都会按预期运行。这样可以实现增量迁移,因此您无需在一次更改中将支持 SQLite 使用情况全部转换为 SQLite 驱动程序。

以下示例是支持 SQLite 及其对应 SQLite 驱动程序的常见用法:

支持 SQLite(来自)

执行没有结果的查询

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

执行包含结果但没有参数的查询

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

使用结果和参数执行查询

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

SQLite 驱动程序(目标)

执行没有结果的查询

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

执行包含结果但没有参数的查询

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

使用结果和参数执行查询

connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
  statement.bindInt(1, id)
  if (statement.step()) {
    // row found, read columns
  } else {
    // row not found
  }
}

数据库事务 API 可通过 beginTransaction()setTransactionSuccessful()endTransaction() 直接在 SupportSQLiteDatabase 中使用。您也可以使用 runInTransaction() 通过 Room 使用它们。请将这些用法迁移到 SQLite 驱动程序 API。

支持 SQLite(来自)

执行事务(使用 RoomDatabase

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

执行事务(使用 SupportSQLiteDatabase

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

SQLite 驱动程序(目标)

执行事务(使用 RoomDatabase

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

执行事务(使用 SQLiteConnection

val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
  // perform database operations in transaction
  connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
  connection.execSQL("ROLLBACK TRANSACTION")
}

各种回调替换也需要迁移到对应的驱动程序替换项:

支持 SQLite(来自)

迁移子类

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

自动迁移规范子类

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

数据库回调子类

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // ...
  }
}

SQLite 驱动程序(目标)

迁移子类

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // ...
  }
}

自动迁移规范子类

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // ...
  }
}

数据库回调子类

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

总而言之,当 RoomDatabase 不可用时(例如在回调替换项(onMigrateonCreate 等)中时,请将 SQLiteDatabase 的使用替换为 SQLiteConnection。如果有 RoomDatabase,则使用 RoomDatabase.useReaderConnectionRoomDatabase.useWriterConnection(而非 RoomDatabase.openHelper.writtableDatabase)访问底层数据库连接。

将阻塞 DAO 函数转换为挂起函数

Room 的 KMP 版本依赖协程对配置的 CoroutineContext 执行 I/O 操作。这意味着,您需要将任何阻塞 DAO 函数迁移到挂起函数。

阻塞 DAO 函数(来自)

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

挂起 DAO 函数(目标)

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

如果现有代码库尚未整合协程,则将现有 DAO 阻塞函数迁移到挂起函数可能会很复杂。如需开始在代码库中使用协程,请参阅 Android 中的协程

将响应式返回值类型转换为 Flow

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

不兼容的 KMP 类型(来源)

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

兼容的 KMP 类型 (to)

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

请参阅 Android 中的数据流,开始在代码库中使用数据流。

设置协程上下文(可选)

可以视情况为 RoomDatabase 配置共享应用执行器,使用 RoomDatabase.Builder.setQueryExecutor() 来执行数据库操作。由于执行器与 KMP 不兼容,因此 Room 的 setQueryExecutor() API 不适用于常见源代码。相反,必须为 RoomDatabase 配置 CoroutineContext。您可以使用 RoomDatabase.Builder.setCoroutineContext() 设置上下文,如果未设置,则 RoomDatabase 将默认使用 Dispatchers.IO

设置 SQLite 驱动程序

将支持 SQLite 用法迁移到 SQLite 驱动程序 API 后,必须使用 RoomDatabase.Builder.setDriver 配置驱动程序。建议使用 BundledSQLiteDriver 驱动程序。如需了解可用的驱动程序实现,请参阅驱动程序实现

使用 RoomDatabase.Builder.openHelperFactory() 配置的自定义 SupportSQLiteOpenHelper.Factory 在 KMP 中不受支持,自定义 Open Helper 提供的功能需要使用 SQLite 驱动程序接口重新实现。

“会议室”声明

完成大多数迁移步骤后,即可将 Room 定义移至通用源代码集。请注意,expect / actual 策略可用于逐步迁移与 Room 相关的定义。例如,如果并非所有阻塞 DAO 函数都可以迁移到挂起函数,则可以声明一个带有 expect @Dao 注解的接口,该接口在通用代码中为空,但包含 Android 中的阻塞函数。

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

@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
  abstract fun getBlockingDao(): BlockingTodoDao
}

@Dao
interface TodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    suspend fun count(): Int
}

@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt

@Dao
actual interface BlockingTodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    fun count(): Int
}