将 Room 迁移到 Kotlin Multiplatform

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

将现有 Android 代码库中的 Room 用法迁移到公共共享 KMP 模块,其难度差异很大,具体取决于所使用的 Room API 或代码库是否已使用协程。本节提供了将 Room 用法迁移到公共模块的一些指导和技巧。

首先熟悉 Android 版 Room 和 KMP 版 Room 之间的差异和缺失功能以及相关的设置非常重要。本质上,成功的迁移涉及重构 SupportSQLite* API 的用法,并将其替换为 SQLite Driver API,同时将 Room 声明(@Database 注释类、DAO、实体等)移动到通用代码中。

继续之前,请重新查看以下信息

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

从 Support SQLite 迁移到 SQLite Driver

androidx.sqlite.db 中的 API 仅限 Android 使用,任何用法都需要使用 SQLite Driver API 进行重构。为了向后兼容,只要 RoomDatabase 配置了 SupportSQLiteOpenHelper.Factory(即没有设置 SQLiteDriver),Room 就会以“兼容模式”运行,其中 Support SQLite 和 SQLite Driver API 都能按预期工作。这使得增量迁移成为可能,您无需在一次更改中将所有 Support SQLite 用法转换为 SQLite Driver。

以下是 Support SQLite 的常见用法及其对应的 SQLite Driver 用法示例

Support 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 Driver(新用法)

执行无结果的查询

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 可直接通过 SupportSQLiteDatabasebeginTransaction()setTransactionSuccessful()endTransaction() 方法使用。它们也可以通过 Room 的 runInTransaction() 方法使用。将这些用法迁移到 SQLite Driver API。

Support 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 Driver(新用法)

执行事务(使用 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")
}

各种回调覆盖方法也需要迁移到对应的驱动程序实现

Support SQLite(原用法)

Migration 子类

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

Auto migration specification 子类

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

Database callback 子类

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

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

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

SQLite Driver(新用法)

Migration 子类

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

Auto migration specification 子类

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

Database callback 子类

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 函数转换为 suspend 函数

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

阻塞式 DAO 函数(原用法)

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

Suspending DAO 函数(新用法)

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

如果现有代码库尚未集成协程,将现有 DAO 阻塞函数迁移到 suspend 函数可能会很复杂。请参阅Android 中的协程以开始在您的代码库中使用协程。

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

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

不兼容的 KMP 类型(原用法)

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

兼容的 KMP 类型(新用法)

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

请参阅Android 中的流以开始在您的代码库中使用流。

设置协程上下文(可选)

可以使用 RoomDatabase.Builder.setQueryExecutor() 可选地配置 RoomDatabase,以使用共享应用执行器执行数据库操作。由于执行器与 KMP 不兼容,Room 的 setQueryExecutor() API 不适用于通用源。因此,必须使用 CoroutineContext 配置 RoomDatabase。可以使用 RoomDatabase.Builder.setCoroutineContext() 设置上下文,如果未设置,RoomDatabase 将默认使用 Dispatchers.IO

设置 SQLite Driver

将 Support SQLite 用法迁移到 SQLite Driver API 后,必须使用 RoomDatabase.Builder.setDriver 配置驱动程序。推荐的驱动程序是 BundledSQLiteDriver。请参阅驱动程序实现以了解可用驱动程序实现的说明。

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

移动 Room 声明

完成大部分迁移步骤后,可以将 Room 定义移动到通用源集。请注意,可以使用 expect / actual 策略增量地移动与 Room 相关的定义。例如,如果并非所有阻塞式 DAO 函数都可以迁移到 suspend 函数,则可以在通用代码中声明一个空的 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
}