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
}