从 Room 迁移到 Kotlin Multiplatform

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

将现有 Android 代码库中的 Room 用例迁移到通用的共享 KMP 模块的难度可能差别很大,具体取决于所使用的 Room API 或 代码库已经使用协程了。本部分提供了一些指导和提示 。

请务必先熟悉二者的差异和缺失 Android 版 Room 和 KMP 版本之间的功能, 所涉及的设置从本质上讲,成功的迁移涉及重构 使用 SupportSQLite* API 并将其替换为 SQLite Driver API 以及移动 Room 声明(带 @Database 注解的类、DAO、 实体等)转换成通用代码。

在继续之前,请回顾以下信息:

后续部分将说明成功执行广告系列所需要 迁移。

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

androidx.sqlite.db 中的 API 仅适用于 Android,任何使用都需要 使用 SQLite 驱动程序 API 进行了重构。为了确保向后兼容性,并且只要 RoomDatabase 配置了 SupportSQLiteOpenHelper.Factory(即 未设置 SQLiteDriver),则 Room 在“兼容模式”下运行其中 支持 SQLite 和 SQLite 驱动程序 API 都能按预期运行。这样一来, 增量迁移,这样您就无需转换所有 Support SQLite 对 SQLite 驱动程序的用法进行了全面更改。

以下示例是 Support 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 可直接在 SupportSQLiteDatabase 中使用, beginTransaction()setTransactionSuccessful()endTransaction()。 您也可以在 Room 中使用 runInTransaction() 使用这些标签。迁移这些 对 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) {
    // ...
  }
}

总而言之,当出现以下情况时,请将 SQLiteDatabase 替换为 SQLiteConnectionRoomDatabase 不可用,例如在回调替换中(onMigrateonCreate 等)。如果有 RoomDatabase 可用,则访问 使用 RoomDatabase.useReaderConnectionRoomDatabase.useWriterConnection(原价) RoomDatabase.openHelper.writtableDatabase

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

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

阻塞 DAO 函数 (from)

@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 类型(目标)

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

请参阅 Android 中的 Flow,了解如何在您的 代码库。

设置协程上下文(可选)

可以选择为 RoomDatabase 配置共享应用 使用 RoomDatabase.Builder.setQueryExecutor() 执行数据库的执行器 操作。由于执行程序与 KMP 不兼容,因此 Room 的 setQueryExecutor() API 不适用于常见来源。相反,RoomDatabase 必须 配置了 CoroutineContext。您可以使用 RoomDatabase.Builder.setCoroutineContext() - 如果未设置任何值,则 RoomDatabase 将默认使用 Dispatchers.IO

设置 SQLite 驱动程序

将 Support SQLite 用法迁移到 SQLite Driver API 后, 驱动程序必须使用 RoomDatabase.Builder.setDriver 进行配置。通过 推荐的驱动程序为 BundledSQLiteDriver。如需了解以下内容,请参阅驱动程序实现 可用驱动程序实现的说明。

使用以下方式配置的自定义 SupportSQLiteOpenHelper.Factory KMP 不支持 RoomDatabase.Builder.openHelperFactory(), 自定义 Open 帮助程序提供的功能将需要通过 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
}