將 Room 遷移至 Kotlin Multiplatform

本文說明如何將現有的 Room 實作遷移至單一聊天室 都採用 Kotlin Multiplatform (KMP)。

將現有 Android 程式碼集的 Room 使用情形遷移至通用的共用 KMP 根據所使用的 Room API,或是否要 但程式碼集已經在使用協同程式本節提供了一些指引和提示 ,以將 Room 的用量遷移至通用模組。

請務必先瞭解兩者的差異之處 Android 版和 KMP 版本的實用功能 相關的設定基本上,遷移成功牽涉到重構 SupportSQLite* API 的使用情形,並以 SQLite 驅動程式 API 取代 以及移動的 Room 宣告 (@Database 註解類別、DAO、 這些實體等項目)

請先複習下列資訊,再繼續操作:

以下各節將說明要達到這個目標所需的各種步驟

從 Support SQLite 遷移至 SQLite 驅動程式

androidx.sqlite.db 中的 API 僅適用於 Android,任何用途皆可 透過 SQLite 驅動程式 API 重構享有回溯相容性,且 RoomDatabase 設定為 SupportSQLiteOpenHelper.Factory (即 未設定 SQLiteDriver),則 Room 會在「相容模式」下運作在哪? 同時支援 SQLite 和 SQLite 驅動程式 API。這樣一來, 逐步進行遷移作業,讓您不必轉換所有支援服務 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()。 你也可以透過 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 的用法替換為 SQLiteConnection, 無法使用 RoomDatabase,例如在回呼覆寫中 (onMigrateonCreate 等)。如果有 RoomDatabase 可用,則存取基礎 使用 RoomDatabase.useReaderConnectionRoomDatabase.useWriterConnection 取代 RoomDatabase.openHelper.writtableDatabase

將封鎖 DAO 函式轉換為暫停函式

Room 的 KMP 版本依賴協同程式執行 I/O 針對已設定的 CoroutineContext 執行各項作業。這表示 就不需遷移任何會阻斷的 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 類型 (目標)

@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 驅動程式

將 Support SQLite 的使用情形遷移至 SQLite 驅動程式 API 後, 驅動程式是使用 RoomDatabase.Builder.setDriver 來設定。 建議的驅動程式為 BundledSQLiteDriver。請參閱「驅動因素實作」 可用驅動程式實作的說明。

自訂 SupportSQLiteOpenHelper.Factory 設定方式: KMP 不支援 RoomDatabase.Builder.openHelperFactory(), 自訂開啟輔助程式所提供的功能,必須重新採用 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
}