Menyiapkan database Room untuk KMP

Library persistensi Room menyediakan lapisan abstraksi di atas SQLite untuk memungkinkan akses database yang lebih andal sekaligus memanfaatkan seluruh kapabilitas SQLite. Halaman ini berfokus pada penggunaan Room dalam project Kotlin Multiplatform (KMP). Untuk mengetahui informasi selengkapnya tentang penggunaan Room, lihat Menyimpan data di database lokal menggunakan Room atau contoh resmi kami.

Menyiapkan dependensi

Untuk menyiapkan Room di project KMP, tambahkan dependensi untuk artefak dalam file build.gradle.kts untuk modul KMP Anda.

Tentukan dependensi dalam file libs.versions.toml:

[versions]
room3 = "3.1.0-alpha03"
sqlite = "2.7.1"
ksp = "<kotlinCompatibleKspVersion>"

[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }

# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }

Tambahkan Plugin Room Gradle untuk mengonfigurasi skema Room dan plugin KSP.

plugins {
  alias(libs.plugins.ksp)
  alias(libs.plugins.androidx.room3)
}

Tambahkan dependensi runtime Room dan library SQLite yang dipaketkan:

commonMain.dependencies {
  implementation(libs.androidx.room3.runtime)
  implementation(libs.androidx.sqlite.bundled)
}

// Optional when using Room SQLite Wrapper
androidMain.dependencies {
  implementation(libs.androidx.room3.sqlite.wrapper)
}

Tambahkan dependensi KSP ke blok dependencies root. Anda harus menambahkan semua target yang digunakan aplikasi Anda. Untuk informasi selengkapnya, lihat KSP dengan Kotlin Multiplatform.

dependencies {
    add("kspAndroid", libs.androidx.room3.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
    add("kspIosX64", libs.androidx.room3.compiler)
    add("kspIosArm64", libs.androidx.room3.compiler)
    // Add any other platform target you use in your project, for example kspDesktop
}

Tentukan direktori skema Room. Untuk informasi tambahan, lihat Menetapkan lokasi skema menggunakan Plugin Room Gradle.

room {
    schemaDirectory("$projectDir/schemas")
}

Menentukan class database

Anda perlu membuat class database yang dianotasi dengan @Database beserta DAO dan entity di dalam set sumber umum modul KMP bersama Anda. Menempatkan class ini di sumber umum akan memungkinkan class tersebut dibagikan di semua platform target.

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getDao(): TodoDao
}

// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

Saat Anda mendeklarasikan objek expect dengan antarmuka RoomDatabaseConstructor, compiler Room akan menghasilkan implementasi actual. Android Studio mungkin akan menampilkan peringatan berikut, yang dapat Anda abaikan dengan @Suppress("KotlinNoActualForExpect"):

Expected object 'AppDatabaseConstructor' has no actual declaration in module`

Selanjutnya, tentukan antarmuka DAO baru atau pindahkan antarmuka yang ada ke commonMain:

// shared/src/commonMain/kotlin/TodoDao.kt

@Dao
interface TodoDao {
    @Insert
    suspend fun insert(item: TodoEntity)

    @Query("SELECT count(*) FROM TodoEntity")
    suspend fun count(): Int

    @Query("SELECT * FROM TodoEntity")
    fun getAllAsFlow(): Flow<List<TodoEntity>>
}

Tentukan atau pindahkan entity Anda ke commonMain:

// shared/src/commonMain/kotlin/TodoEntity.kt

@Entity
data class TodoEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val content: String
)

Membuat builder database khusus platform

Anda perlu menentukan builder database untuk membuat instance Room di setiap platform. Ini adalah satu-satunya bagian API yang harus Anda tentukan dalam set sumber khusus platform, karena perbedaan dalam API sistem file.

Android

Di Android, Anda biasanya mendapatkan lokasi database menggunakan Context.getDatabasePath API. Untuk membuat instance database, tentukan Context dan jalur database.

// shared/src/androidMain/kotlin/Database.android.kt

fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
    val appContext = context.applicationContext
    val dbFile = appContext.getDatabasePath("my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        context = appContext,
        name = dbFile.absolutePath
    )
}

iOS

Untuk membuat instance database di iOS, berikan jalur database menggunakan NSFileManager, yang biasanya terletak di NSDocumentDirectory.

// shared/src/iosMain/kotlin/Database.ios.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = documentDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
    )
}

@OptIn(ExperimentalForeignApi::class)
private fun documentDirectory(): String {
    val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
        directory = NSDocumentDirectory,
        inDomain = NSUserDomainMask,
        appropriateForURL = null,
        create = false,
        error = null,
    )
    return requireNotNull(documentDirectory?.path)
}

Desktop JVM

Untuk membuat instance database, berikan jalur database menggunakan API Java atau Kotlin.

// shared/src/jvmMain/kotlin/Database.desktop.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

Membuat instance database

Setelah mendapatkan RoomDatabase.Builder dari salah satu konstruktor khusus platform, Anda dapat mengonfigurasi database Room lainnya dalam kode umum bersama dengan pembuatan instance database yang sebenarnya.

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
    return builder
        .setDriver(BundledSQLiteDriver())
        .setQueryCoroutineContext(Dispatchers.IO)
        .build()
}

Pilih driver SQLite

Cuplikan kode sebelumnya memanggil fungsi builder setDriver untuk menentukan driver SQLite yang harus digunakan database Room. Driver ini berbeda berdasarkan platform target. Cuplikan kode sebelumnya menggunakan BundledSQLiteDriver. Driver ini adalah driver yang direkomendasikan yang menyertakan SQLite yang dikompilasi dari sumber, yang menyediakan versi SQLite yang paling konsisten dan terbaru di semua platform.

Jika Anda ingin menggunakan SQLite yang disediakan OS, gunakan API setDriver di set sumber khusus platform yang menentukan driver khusus platform. Lihat Implementasi driver untuk deskripsi implementasi driver yang tersedia. Anda dapat menggunakan salah satu dari berikut ini:

Untuk menggunakan NativeSQLiteDriver, Anda harus memberikan opsi penaut -lsqlite3 agar aplikasi iOS ditautkan secara dinamis dengan SQLite sistem.

// shared/build.gradle.kts

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "TodoApp"
            isStatic = true
            // Required when using NativeSQLiteDriver
            linkerOpts.add("-lsqlite3")
        }
    }
}

Menetapkan konteks coroutine

Objek RoomDatabase di Android dapat dikonfigurasi secara opsional dengan eksekutor aplikasi bersama menggunakan RoomDatabase.Builder.setQueryExecutor untuk melakukan operasi database.

Karena eksekutor tidak kompatibel dengan KMP, setQueryExecutor API Room tidak tersedia di commonMain. Sebagai gantinya, objek RoomDatabase harus dikonfigurasi dengan CoroutineContext, yang dapat Anda tetapkan menggunakan RoomDatabase.Builder.setCoroutineContext. Jika Anda tidak menetapkan konteks, objek RoomDatabase akan ditetapkan secara default ke Dispatchers.IO.

Minifikasi dan obfuscation

Jika project diminimalkan atau di-obfuscate, Anda harus menyertakan aturan ProGuard berikut agar Room dapat menemukan implementasi yang dihasilkan dari definisi database:

-keep class * extends androidx.room3.RoomDatabase { <init>(); }

Bermigrasi ke Multiplatform Kotlin

Room awalnya dikembangkan sebagai library Android dan kemudian dimigrasikan ke KMP dengan fokus pada kompatibilitas API. Versi KMP Room agak berbeda antara platform dan dari versi khusus Android. Perbedaan ini dicantumkan dan dijelaskan sebagai berikut.

Bermigrasi dari Support SQLite ke Driver SQLite

Setiap penggunaan SupportSQLiteDatabase dan API lainnya di androidx.sqlite.db perlu direfaktorisasi dengan SQLite Driver API, karena API di androidx.sqlite.db hanya untuk Android (perhatikan paket yang berbeda dengan paket KMP).

Untuk kompatibilitas mundur, dan selama RoomDatabase dikonfigurasi dengan SupportSQLiteOpenHelper.Factory (misalnya, 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.

Menggunakan wrapper SQLite Room

Artefak androidx.room3:room3-sqlite-wrapper menyediakan API untuk menjembatani SQLiteDriver dan SupportSQLiteDatabase selama migrasi.

Untuk mendapatkan SupportSQLiteDatabase dari RoomDatabase yang dikonfigurasi dengan SQLiteDriver, gunakan fungsi ekstensi baru RoomDatabase.getSupportWrapper. Wrapper kompatibilitas ini membantu mempertahankan penggunaan SupportSQLiteDatabase yang ada, yang sering Anda peroleh dari RoomDatabase.openHelper.writableDatabase, sekaligus mengadopsi SQLiteDriver, terutama untuk codebase dengan penggunaan SupportSQLite API ekstensif yang ingin menggunakan BundledSQLiteDriver.

Mengonversi subclass migrasi

Subclass Migrations perlu dimigrasikan ke pasangan driver SQLite:

Multiplatform Kotlin

Subclass migrasi

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

Subclass spesifikasi migrasi otomatis

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

Khusus Android

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) {
    // …
  }
}

Mengonversi callback database

Callback database harus dimigrasikan ke pasangan driver SQLite:

Multiplatform Kotlin

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

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

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

Khusus Android

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

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

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

Mengonversi fungsi DAO @RawQuery

Fungsi yang dianotasi dengan @RawQuery yang dikompilasi untuk platform non-Android harus mendeklarasikan parameter jenis RoomRawQuery, bukan SupportSQLiteQuery.

Multiplatform Kotlin

Menentukan kueri mentah

@Dao
interface TodoDao {
    @RawQuery
    suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}

RoomRawQuery kemudian dapat digunakan untuk membuat kueri saat runtime:

suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
    val query = RoomRawQuery(
        sql = "SELECT * FROM TodoEntity WHERE title = ?",
        onBindStatement = {
            it.bindText(1, title.lowercase())
        }
    )

    return getDao().getTodos(query)
}

Khusus Android

Menentukan kueri mentah

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}

SimpleSQLiteQuery kemudian dapat digunakan untuk membuat kueri saat runtime:

suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = SimpleSQLiteQuery(
      query = "SELECT * FROM TodoEntity WHERE title = ?",
      bindArgs = arrayOf(title.lowercase())
  )
  return getTodos(query)
}

Mengonversi fungsi DAO pemblokiran

Room memanfaatkan library kotlinx.coroutines asinkron yang kaya fitur yang ditawarkan Kotlin untuk beberapa platform. Untuk fungsionalitas yang optimal, fungsi suspend diterapkan untuk DAO yang dikompilasi dalam project KMP, kecuali untuk DAO yang diimplementasikan di androidMain untuk mempertahankan kompatibilitas mundur dengan codebase yang ada. Saat menggunakan Room untuk KMP, semua fungsi DAO yang dikompilasi untuk platform non-Android harus berupa fungsi suspend.

Multiplatform Kotlin

Menangguhkan kueri

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

Menangguhkan transaksi

@Transaction
suspend fun transaction() {
    // ...
}

Khusus Android

Memblokir kueri

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

Memblokir transaksi

@Transaction
fun blockingTransaction() { … }

Mengonversi jenis reaktif ke Flow

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

Multiplatform Kotlin

Jenis reaktif Flows

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

Khusus Android

Jenis reaktif seperti LiveData atau Flowable RxJava

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

Mengonversi API transaksi

API transaksi database untuk Room KMP dapat membedakan antara transaksi penulisan (useWriterConnection) dan pembacaan (useReaderConnection).

Multiplatform Kotlin

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

Khusus Android

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

Transaksi tulis

Gunakan transaksi tulis untuk memastikan bahwa beberapa kueri menulis data secara atomik, sehingga pembaca dapat mengakses data secara konsisten. Anda dapat melakukannya menggunakan useWriterConnection dengan salah satu dari tiga jenis transaksi:

  • immediateTransaction: Dalam mode Write-Ahead Logging (WAL) (default), jenis transaksi ini memperoleh kunci saat dimulai, tetapi pembaca dapat terus membaca. Ini adalah pilihan yang lebih disukai untuk sebagian besar kasus.

  • deferredTransaction: Transaksi tidak akan memperoleh kunci hingga pernyataan tulis pertama. Gunakan jenis transaksi ini sebagai pengoptimalan jika Anda tidak yakin apakah operasi tulis akan diperlukan dalam transaksi. Misalnya, jika Anda memulai transaksi untuk menghapus lagu dari playlist yang hanya diberi nama playlist dan playlist tersebut tidak ada, operasi tulis (hapus) tidak diperlukan.

  • exclusiveTransaction: Mode ini berperilaku sama dengan immediateTransaction dalam mode WAL. Dalam mode penjurnalan lainnya, mode ini mencegah koneksi database lain membaca database saat transaksi sedang berlangsung.

Transaksi baca

Gunakan transaksi baca untuk membaca dari database secara konsisten beberapa kali. Misalnya, saat Anda memiliki dua atau beberapa kueri terpisah dan tidak menggunakan klausa JOIN. Hanya transaksi yang ditangguhkan yang diizinkan dalam koneksi pembaca. Upaya untuk memulai transaksi langsung atau eksklusif dalam koneksi pembaca akan menampilkan pengecualian, karena transaksi ini dianggap sebagai operasi 'tulis'.

val database: RoomDatabase = getRoomDatabase(builder)
database.useReaderConnection { transactor ->
    transactor.deferredTransaction {
        // perform database operations in transaction.
    }
}

Tidak tersedia di Multiplatform Kotlin

Beberapa API yang tersedia untuk Android tidak tersedia di Kotlin Multiplatform.

Callback kueri

API berikut untuk mengonfigurasi callback kueri tidak tersedia di common sehingga tidak tersedia di platform selain Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Menutup database secara otomatis

API untuk mengaktifkan penutupan otomatis setelah waktu tunggu habis, RoomDatabase.Builder.setAutoCloseTimeout, hanya tersedia di Android.

Database yang telah dikemas sebelumnya

API berikut untuk membuat RoomDatabase menggunakan database yang ada (database dalam bentuk paket) tidak tersedia secara umum dan oleh karena itu tidak tersedia di platform selain Android:

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

Invalidasi multi-instance

API untuk mengaktifkan invalidasi multi-instance, RoomDatabase.Builder.enableMultiInstanceInvalidation, hanya tersedia di Android dan tidak tersedia di platform umum atau platform lainnya.