Memigrasikan Room ke Multiplaform Kotlin

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

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

Penting untuk terlebih dahulu memahami perbedaan dan fitur yang hilang antara Room versi Android dan versi KMP beserta penyiapan yang digunakan. Pada intinya, migrasi yang berhasil melibatkan pemfaktoran ulang penggunaan SupportSQLite* API dan menggantinya dengan SQLite Driver API serta memindahkan deklarasi Room (class beranotasi @Database, DAO, entity, dan sebagainya) ke dalam kode umum.

Lihat kembali informasi berikut sebelum melanjutkan:

Bagian selanjutnya menjelaskan berbagai langkah yang diperlukan agar migrasi berhasil.

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 ditetapkan), Room akan berperilaku dalam 'mode kompatibilitas' dengan Support SQLite dan SQLite Driver API berfungsi seperti yang diharapkan. Hal ini memungkinkan migrasi inkremental sehingga Anda tidak perlu mengonversi semua penggunaan SQLite Dukungan ke Driver SQLite dalam satu perubahan.

Contoh berikut adalah penggunaan umum Support SQLite dan penggunaan SQLite Driver:

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 (ke)

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(). Tersedia juga melalui Room menggunakan runInTransaction(). Migrasikan penggunaan ini ke 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 (ke)

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 perlu dimigrasikan ke versi 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 (ke)

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 koneksi database pokok menggunakan RoomDatabase.useReaderConnection dan RoomDatabase.useWriterConnection, bukan RoomDatabase.openHelper.writtableDatabase.

Mengonversi fungsi DAO pemblokir untuk menangguhkan fungsi

Room versi KMP mengandalkan coroutine untuk melakukan operasi I/O pada CoroutineContext yang dikonfigurasi. Ini berarti Anda harus memigrasikan fungsi DAO yang memblokir 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>

Migrasi fungsi pemblokiran DAO yang sudah ada ke fungsi penangguhan dapat menjadi rumit jika codebase yang ada belum menggabungkan coroutine. Lihat Coroutine di Android untuk mulai menggunakan coroutine di codebase Anda.

Mengonversi jenis nilai yang ditampilkan reaktif menjadi Flow

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

Jenis KMP tidak kompatibel (dari)

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

Jenis KMP yang kompatibel (ke)

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

Lihat Flow di Android untuk mulai menggunakan Flow di codebase Anda.

Menetapkan konteks Coroutine (Opsional)

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

Menetapkan Driver SQLite

Setelah penggunaan SQLite Dukungan dimigrasikan ke SQLite Driver API, driver harus dikonfigurasi menggunakan RoomDatabase.Builder.setDriver. Driver yang direkomendasikan adalah BundledSQLiteDriver. Lihat Implementasi driver untuk deskripsi implementasi driver yang tersedia.

SupportSQLiteOpenHelper.Factory kustom yang dikonfigurasi menggunakan RoomDatabase.Builder.openHelperFactory() tidak didukung di KMP, fitur yang disediakan oleh open helper kustom harus diimplementasikan kembali dengan antarmuka Driver SQLite.

Memindahkan pernyataan Room

Setelah sebagian besar langkah migrasi selesai, Anda dapat memindahkan definisi Room ke set sumber umum. Perhatikan bahwa strategi expect / actual dapat digunakan untuk memindahkan definisi terkait Room secara bertahap. Misalnya, jika tidak semua fungsi DAO pemblokir dapat dimigrasikan untuk menangguhkan fungsi, Anda dapat mendeklarasikan antarmuka teranotasi 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
}