本文件說明如何將現有的 Room 實作遷移至使用 Kotlin Multiplatform (KMP) 的實作。
視使用的 Room API 或程式碼集是否已使用協同程式,將現有 Android 程式碼集中的 Room 使用情形遷移至通用共用 KMP 模組,難度可能大不相同。本節提供了一些指南和提示,協助您將 Room 的使用情形遷移至通用模組。
請務必先熟悉 Room 與 KMP 版本在 Android 版本和 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 Driver 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
}
}
可直接在 SupportSQLiteDatabase
中使用資料庫交易 API,搭配 beginTransaction()
、setTransactionSuccessful()
和 endTransaction()
。你也可以使用「runInTransaction()
」透過 Room 查看這些配件。請將這些使用方式遷移至 SQLite Driver 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
時,請將 SQLiteDatabase
的用法替換為 SQLiteConnection
,例如在回呼覆寫 (onMigrate
、onCreate
等) 中。如果 RoomDatabase
可用,請使用 RoomDatabase.useReaderConnection
和 RoomDatabase.useWriterConnection
(而非 RoomDatabase.openHelper.writtableDatabase
) 存取基礎資料庫連線。
將封鎖 DAO 函式轉換為暫停函式
KMP 版本的 Room 仰賴協同程式,在已設定的 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 中的協同程式」,開始在程式碼集中使用協同程式。
將回應式傳回類型轉換為流程
並非所有 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 中的流程,開始在程式碼集使用 Flows。
設定協同程式結構定義 (選用)
您可以選擇將 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 中不支援,需要使用 SQLite 驅動程式介面重新實作自訂開啟輔助程式提供的功能。
移動 Room 宣告
完成大部分遷移步驟後,您可以將 Room 定義移至常用來源集。請注意,expect
/ actual
策略可用來逐步移動 Room 相關定義。舉例來說,如果並非所有封鎖的 DAO 函式都能遷移至暫停函式,則可宣告在通用程式碼中為空白,但在 Android 中加入封鎖函式的 expect
@Dao
註解介面。
// 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
}