Memigrasikan Room ke Multiplatform Kotlin

Dokumen ini menjelaskan cara memigrasikan implementasi Room yang ada ke yang menggunakan Multiplatform Kotlin (KMP).

Memigrasikan penggunaan Room dalam codebase Android yang ada ke KMP bersama umum modul dapat sangat bervariasi tingkat kesulitannya bergantung pada Room API yang digunakan atau apakah codebase sudah menggunakan Coroutine. Bagian ini menawarkan beberapa panduan dan tips saat mencoba memigrasikan penggunaan Room ke modul umum.

Penting untuk terlebih dahulu membiasakan diri dengan perbedaan dan tidak ada antara Room versi Android dan versi KMP beserta pengaturan yang diperlukan. Pada intinya, migrasi yang berhasil memerlukan pemfaktoran ulang penggunaan SupportSQLite* API dan menggantinya dengan SQLite Driver API beserta pemindahan deklarasi Room (class teranotasi @Database, DAO, entitas, dan seterusnya) ke dalam kode umum.

Tinjau kembali informasi berikut sebelum melanjutkan:

Bagian selanjutnya menjelaskan berbagai langkah yang diperlukan untuk migrasi.

Bermigrasi dari Support SQLite ke Driver SQLite

API di androidx.sqlite.db hanya untuk Android, dan setiap penggunaan harus difaktorkan ulang dengan SQLite Driver API. Untuk kompatibilitas mundur, dan selama RoomDatabase dikonfigurasi dengan SupportSQLiteOpenHelper.Factory (yaitu tidak ada SQLiteDriver yang disetel), Room berperilaku dalam 'mode kompatibilitas' dengan Mendukung SQLite dan SQLite Driver API berfungsi seperti yang diharapkan. Hal ini memungkinkan migrasi inkremental sehingga Anda tidak perlu mengonversi semua Support SQLite sebelumnya pada SQLite Driver dalam satu perubahan.

Contoh berikut adalah penggunaan umum Support SQLite dan SQLite Rekan pengemudi:

Mendukung SQLite (dari)

Menjalankan kueri tanpa hasil

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

Menjalankan kueri dengan hasil, tetapi tanpa argumen

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

Menjalankan kueri dengan hasil dan argumen

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

Driver SQLite (untuk)

Menjalankan kueri tanpa hasil

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

Menjalankan kueri dengan hasil, tetapi tanpa argumen

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

Menjalankan kueri dengan hasil dan argumen

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 transaksi database tersedia langsung di SupportSQLiteDatabase dengan beginTransaction(), setTransactionSuccessful(), dan endTransaction(). Item tersebut juga tersedia melalui Room menggunakan runInTransaction(). Migrasikan ini sebelumnya untuk SQLite Driver API.

Mendukung SQLite (dari)

Melakukan transaksi (menggunakan RoomDatabase)

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

Melakukan transaksi (menggunakan SupportSQLiteDatabase)

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

Driver SQLite (untuk)

Melakukan transaksi (menggunakan RoomDatabase)

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Melakukan transaksi (menggunakan 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")
}

Berbagai penggantian callback juga harus dimigrasikan ke pasangan drivernya:

Mendukung SQLite (dari)

Subclass migrasi

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

Subclass spesifikasi migrasi otomatis

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

Subclass callback database

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

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

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

Driver SQLite (untuk)

Subclass migrasi

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

Subclass spesifikasi migrasi otomatis

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

Subclass callback database

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

Ringkasnya, ganti penggunaan SQLiteDatabase, dengan SQLiteConnection saat RoomDatabase tidak tersedia, seperti dalam penggantian callback (onMigrate, onCreate, dll.). Jika RoomDatabase tersedia, akses resource yang mendasarinya koneksi database menggunakan RoomDatabase.useReaderConnection dan RoomDatabase.useWriterConnection, bukan RoomDatabase.openHelper.writtableDatabase.

Mengonversi fungsi DAO pemblokir untuk menangguhkan fungsi

Versi KMP Room mengandalkan coroutine untuk melakukan I/O operasi pada CoroutineContext yang dikonfigurasi. Ini berarti bahwa Anda memigrasikan fungsi DAO pemblokir apa pun untuk menangguhkan fungsi.

Memblokir fungsi DAO (dari)

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

Menangguhkan fungsi DAO (ke)

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

Memigrasikan fungsi pemblokiran DAO yang ada untuk menangguhkan fungsi dapat rumit jika codebase yang ada belum menyertakan coroutine. Lihat Coroutine di Android untuk mulai menggunakan coroutine di codebase Anda.

Mengonversi jenis nilai yang ditampilkan reaktif menjadi Alur

Tidak semua fungsi DAO harus berupa fungsi penangguhan. Fungsi DAO yang menampilkan jenis reaktif seperti LiveData atau Flowable RxJava tidak boleh dikonversi fungsi penangguhan. Namun, beberapa jenis seperti LiveData bukan KMP yang kompatibel. Fungsi DAO dengan jenis nilai yang ditampilkan reaktif harus dimigrasikan ke coroutine.

Jenis KMP yang tidak kompatibel (dari)

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

Jenis KMP yang kompatibel (hingga)

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

Lihat Alur di Android untuk mulai menggunakan Alur di saat ini.

Menetapkan konteks Coroutine (Opsional)

Secara opsional, RoomDatabase dapat dikonfigurasi dengan aplikasi bersama eksekutor menggunakan RoomDatabase.Builder.setQueryExecutor() untuk menjalankan database operasional bisnis. Karena eksekutor tidak kompatibel dengan KMP, setQueryExecutor() Room API tidak tersedia untuk sumber umum. Sebagai gantinya, RoomDatabase harus dikonfigurasi dengan CoroutineContext. Konteks dapat diatur menggunakan RoomDatabase.Builder.setCoroutineContext(), jika tidak ada yang ditetapkan, maka RoomDatabase secara default akan menggunakan Dispatchers.IO.

Menetapkan Driver SQLite

Setelah penggunaan Support SQLite telah dimigrasikan ke SQLite Driver API, driver harus dikonfigurasi menggunakan RoomDatabase.Builder.setDriver. Tujuan driver yang direkomendasikan adalah BundledSQLiteDriver. Lihat Penerapan driver untuk deskripsi penerapan driver yang tersedia.

SupportSQLiteOpenHelper.Factory kustom dikonfigurasi menggunakan RoomDatabase.Builder.openHelperFactory() tidak didukung di KMP, yang disediakan oleh {i>open helper<i} khusus perlu diimplementasikan kembali dengan Antarmuka Driver SQLite.

Deklarasi Pindahkan Room

Setelah sebagian besar langkah migrasi selesai, Anda dapat memindahkan Room definisi ke set sumber umum. Perhatikan bahwa strategi expect / actual dapat digunakan untuk memindahkan definisi terkait Room secara bertahap. Misalnya, jika tidak semua fungsi DAO yang memblokir dapat dimigrasikan untuk menangguhkan fungsi, mendeklarasikan antarmuka expect @Dao yang kosong dalam kode umum, tetapi berisi fungsi pemblokiran di 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
}