本文档介绍如何将现有的 Room 实现迁移到使用 Kotlin 多平台 (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 直接在 SupportSQLiteDatabase
中提供,带有 beginTransaction()
、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(从)
迁移子类
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 Driver(到)
迁移子类
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
不可用时(例如在回调覆盖中,如onMigrate
、onCreate
等),请将SQLiteDatabase
的用法替换为SQLiteConnection
。如果RoomDatabase
可用,则使用RoomDatabase.useReaderConnection
和RoomDatabase.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函数都需要是挂起函数。返回反应式类型(例如LiveData
或RxJava的Flowable
)的DAO函数不应该转换为挂起函数。但是,某些类型(例如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,开始在您的代码库中使用Flow。
设置协程上下文(可选)
可以使用RoomDatabase.Builder.setQueryExecutor()
用共享应用程序执行器配置RoomDatabase
(可选)以执行数据库操作。由于执行器与KMP不兼容,因此Room的setQueryExecutor()
API不适用于公共源代码。相反,必须使用CoroutineContext
配置RoomDatabase
。可以使用RoomDatabase.Builder.setCoroutineContext()
设置上下文,如果没有设置,则RoomDatabase
将默认使用Dispatchers.IO
。
设置SQLite驱动程序
将Support SQLite的用法迁移到SQLite驱动程序API后,必须使用RoomDatabase.Builder.setDriver
配置驱动程序。推荐的驱动程序是BundledSQLiteDriver
。有关可用驱动程序实现的说明,请参见驱动程序实现。
使用RoomDatabase.Builder.openHelperFactory()
配置的自定义SupportSQLiteOpenHelper.Factory
在KMP中不受支持,自定义开放帮助程序提供的功能需要使用SQLite驱动程序接口重新实现。
移动Room声明
完成大多数迁移步骤后,可以将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
}