Room (Multiplatform Kotlin)

Library persistensi Room memberikan lapisan abstraksi pada SQLite untuk memungkinkan akses database yang lebih stabil sambil memanfaatkan kemampuan penuh SQLite. Halaman ini berfokus pada penggunaan Room dalam project Kotlin Multiplatform (KMP). Untuk mengetahui informasi selengkapnya tentang cara menggunakan 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]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"

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

# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }

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

Tambahkan Plugin Gradle Room untuk mengonfigurasi skema Room dan plugin KSP

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

Tambahkan dependensi runtime Room dan library SQLite yang di-bundle:

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

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

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

dependencies {
    add("kspAndroid", libs.androidx.room.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    add("kspIosX64", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.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 bersama dengan 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 mengeluarkan peringatan berikut, yang dapat Anda nonaktifkan dengan @Suppress("KotlinNoActualForExpect"):

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

Selanjutnya, tentukan antarmuka DAO baru atau pindahkan antarmuka yang sudah 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 entitas 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 pembuat database untuk membuat instance Room di setiap platform. Ini adalah satu-satunya bagian API yang harus ada di set sumber khusus platform karena perbedaan dalam API sistem file.

Android

Di Android, lokasi database biasanya diperoleh melalui API Context.getDatabasePath(). Untuk membuat instance database, tentukan Context bersama dengan 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,
    )
}

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

JVM (Desktop)

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,
    )
}

Buat instance database

Setelah mendapatkan RoomDatabase.Builder dari salah satu konstruktor khusus platform, Anda dapat mengonfigurasi database Room lainnya dalam kode umum bersama dengan instansiasi database 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-beda berdasarkan platform target. Cuplikan kode sebelumnya menggunakan BundledSQLiteDriver. Ini adalah driver yang direkomendasikan yang mencakup 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 Penerapan driver untuk mengetahui deskripsi penerapan driver yang tersedia. Anda dapat menggunakan salah satu opsi berikut:

Untuk menggunakan NativeSQLiteDriver, Anda harus memberikan opsi linker -lsqlite3 sehingga aplikasi iOS secara dinamis ditautkan 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 (Opsional)

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

Karena executor tidak kompatibel dengan KMP, API setQueryExecutor() Room tidak tersedia di commonMain. Sebagai gantinya, objek RoomDatabase harus dikonfigurasi dengan CoroutineContext, yang dapat ditetapkan menggunakan RoomDatabase.Builder.setCoroutineContext(). Jika tidak ada konteks yang ditetapkan, objek RoomDatabase akan menggunakan Dispatchers.IO secara default.

Minifikasi dan pengaburan

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

-keep class * extends androidx.room.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 di antara platform dan dari versi khusus Android. Perbedaan tersebut tercantum dan dijelaskan sebagai berikut.

Bermigrasi dari Support SQLite ke Driver SQLite

Penggunaan SupportSQLiteDatabase dan API lainnya di androidx.sqlite.db harus di-refactor 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' di mana API Support SQLite dan SQLite Driver berfungsi seperti yang diharapkan. Hal ini memungkinkan migrasi inkremental sehingga Anda tidak perlu mengonversi semua penggunaan Support SQLite ke Driver SQLite dalam satu perubahan.

Mengonversi Subclass Migrasi

Subkelas migrasi perlu dimigrasikan ke rekanan driver SQLite:

Multiplatform Kotlin

Subkelas migrasi

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

Subkelas spesifikasi migrasi otomatis

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

Khusus Android

Subkelas migrasi

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

Subkelas spesifikasi migrasi otomatis

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

Callback database konversi

Callback database perlu dimigrasikan ke rekanan driver SQLite:

Multiplatform Kotlin

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

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

  override 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>
}

Kemudian, RoomRawQuery 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 todoDao().getTodos(query)
}

Khusus Android

Menentukan kueri mentah

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

Kemudian, SimpleSQLiteQuery 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 fungsi yang optimal, fungsi suspend diterapkan untuk DAO yang dikompilasi dalam project KMP, dengan pengecualian DAO yang diterapkan di androidMain untuk mempertahankan kompatibilitas mundur dengan kode dasar 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>>

API konversi transaksi

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

Multiplatform Kotlin

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

Khusus Android

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

Menulis transaksi

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 mendapatkan kunci saat dimulai, tetapi pembaca dapat terus membaca. Ini adalah pilihan yang lebih disukai untuk sebagian besar kasus.

  • deferredTransaction: Transaksi tidak akan mendapatkan kunci hingga pernyataan penulisan 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 hanya dengan nama playlist dan playlist tersebut tidak ada, maka tidak ada operasi penulisan (penghapusan) yang diperlukan.

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

Membaca transaksi

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

val database: RoomDatabase = 
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 umum dan dengan demikian tidak tersedia di platform selain Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Kami berencana menambahkan dukungan untuk callback kueri di versi Room mendatang.

API untuk mengonfigurasi RoomDatabase dengan callback kueri RoomDatabase.Builder.setQueryCallback beserta antarmuka callback RoomDatabase.QueryCallback tidak tersedia di umum dan oleh karena itu tidak tersedia di platform selain Android.

Menutup Database Otomatis

API untuk mengaktifkan penutupan otomatis setelah waktu tunggu berakhir, RoomDatabase.Builder.setAutoCloseTimeout, hanya tersedia di Android dan tidak tersedia di platform lain.

Database yang telah dipaketkan

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

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

Kami berencana menambahkan dukungan untuk database dalam bentuk paket di Room versi mendatang.

Pembatalan Multi-Instance

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