Room-Datenbank für KMP einrichten

Die Room-Persistenzbibliothek bietet eine Abstraktionsebene über SQLite, um einen robusteren Datenbankzugriff zu ermöglichen und gleichzeitig die volle Leistung von SQLite zu nutzen. Auf dieser Seite geht es um die Verwendung von Room in Kotlin Multiplatform (KMP) -Projekten. Weitere Informationen zur Verwendung von Room finden Sie unter Daten in einer lokalen Datenbank mit Room speichern oder in unseren offiziellen Beispielen.

Abhängigkeiten einrichten

Wenn Sie Room in Ihrem KMP-Projekt einrichten möchten, fügen Sie die Abhängigkeiten für die Artefakte in der Datei build.gradle.kts für Ihr KMP-Modul hinzu.

Definieren Sie die Abhängigkeiten in der Datei libs.versions.toml:

[versions]
room = "2.8.4"
sqlite = "2.6.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" }

Fügen Sie das Room-Gradle-Plug-in hinzu, um Room-Schemas und das KSP Plug-in zu konfigurieren.

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

Fügen Sie die Room-Laufzeitabhängigkeit und die gebündelte SQLite-Bibliothek hinzu:

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

Fügen Sie die KSP-Abhängigkeiten dem Stammblock dependencies hinzu. Sie müssen alle Ziele hinzufügen, die Ihre App verwendet. Weitere Informationen finden Sie unter KSP mit 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
}

Definieren Sie das Room-Schemaverzeichnis. Weitere Informationen finden Sie unter Schemaspeicherort mit dem Room-Gradle-Plug-in festlegen.

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

Datenbankklassen definieren

Sie müssen eine Datenbankklasse mit der Annotation @Database sowie DAOs und Entitäten im gemeinsamen Source-Set Ihres freigegebenen KMP-Moduls erstellen. Wenn Sie diese Klassen in gemeinsamen Quellen platzieren, können sie auf allen Zielplattformen verwendet werden.

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

Wenn Sie ein expect-Objekt mit der Schnittstelle RoomDatabaseConstructor deklarieren, generiert der Room-Compiler die actual-Implementierungen. In Android Studio wird möglicherweise die folgende Warnung angezeigt, die Sie mit unterdrücken können: @Suppress("KotlinNoActualForExpect")

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

Definieren Sie als Nächstes entweder eine neue DAO-Schnittstelle oder verschieben Sie eine vorhandene nach 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>>
}

Definieren oder verschieben Sie Ihre Entitäten in commonMain:

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

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

Plattformspezifischen Datenbank-Builder erstellen

Sie müssen einen Datenbank-Builder definieren, um Room auf jeder Plattform zu instanziieren. Dies ist der einzige Teil der API, der aufgrund der Unterschiede in den Dateisystem-APIs in plattformspezifischen Quellsätzen enthalten sein muss.

Android

Unter Android wird der Datenbankspeicherort in der Regel über die Context.getDatabasePath() API abgerufen. Geben Sie zum Erstellen der Datenbankinstanz einen Context zusammen mit dem Datenbankpfad an.

// 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

Um die Datenbankinstanz unter iOS zu erstellen, geben Sie einen Datenbankpfad mit dem NSFileManager an, der sich normalerweise in NSDocumentDirectory befindet.

// 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)

Geben Sie zum Erstellen der Datenbankinstanz einen Datenbankpfad mit Java- oder Kotlin-APIs an.

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

Datenbank instanziieren

Sobald Sie den RoomDatabase.Builder von einem der plattformspezifischen Konstruktoren erhalten haben, können Sie den Rest der Room-Datenbank im gemeinsamen Code zusammen mit der tatsächlichen Datenbankinstanziierung konfigurieren.

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

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

SQLite-Treiber auswählen

Im vorherigen Code-Snippet wird die Builder-Funktion setDriver aufgerufen, um zu definieren, welchen SQLite-Treiber die Room-Datenbank verwenden soll. Diese Treiber unterscheiden sich je nach Zielplattform. In den vorherigen Code-Snippets wird BundledSQLiteDriver verwendet. Dies ist der empfohlene Treiber, der aus der Quelle kompilierte SQLite enthält und die konsistenteste und aktuellste Version von SQLite auf allen Plattformen bietet.

Wenn Sie das vom Betriebssystem bereitgestellte SQLite verwenden möchten, verwenden Sie die API setDriver in den plattformspezifischen Quellsätzen, die einen plattformspezifischen Treiber angeben. Unter Treiberimplementierungen finden Sie Beschreibungen der verfügbaren Treiber implementierungen. Sie haben folgende Möglichkeiten:

Wenn Sie NativeSQLiteDriver verwenden möchten, müssen Sie eine Linker-Option -lsqlite3 angeben, damit die iOS-App dynamisch mit dem System-SQLite verknüpft wird.

// 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")
        }
    }
}

Coroutine-Kontext festlegen (optional)

Ein RoomDatabase-Objekt unter Android kann optional mit freigegebenen Anwendungsexecutors konfiguriert werden, indem RoomDatabase.Builder.setQueryExecutor() verwendet wird, um Datenbankvorgänge auszuführen.

Da Executors nicht mit KMP kompatibel sind, ist die setQueryExecutor()-API von Room in commonMain nicht verfügbar. Stattdessen muss das RoomDatabase-Objekt mit einem CoroutineContext konfiguriert werden, der mit RoomDatabase.Builder.setCoroutineContext() festgelegt werden kann. Wenn kein Kontext festgelegt ist, verwendet das RoomDatabase-Objekt standardmäßig Dispatchers.IO.

Minifizierung und Verschleierung

Wenn das Projekt minifiziert oder verschleiert ist, müssen Sie die folgende ProGuard-Regel einfügen, damit Room die generierte Implementierung der Datenbankdefinition finden kann:

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

Zu Kotlin Multiplatform migrieren

Room wurde ursprünglich als Android-Bibliothek entwickelt und später zu KMP migriert, wobei der Schwerpunkt auf der API-Kompatibilität lag. Die KMP-Version von Room unterscheidet sich je nach Plattform und von der Android-spezifischen Version. Diese Unterschiede werden im Folgenden aufgeführt und beschrieben.

Von Support SQLite zu SQLite Driver migrieren

Alle Verwendungen von SupportSQLiteDatabase und anderen APIs in androidx.sqlite.db müssen mit SQLite Driver APIs refaktoriert werden, da die APIs in androidx.sqlite.db nur für Android verfügbar sind (beachten Sie das andere Paket als das KMP-Paket).

Aus Gründen der Abwärtskompatibilität und solange RoomDatabase mit einer SupportSQLiteOpenHelper.Factory konfiguriert ist (z. B. kein SQLiteDriver festgelegt ist), verhält sich Room im Kompatibilitätsmodus, in dem sowohl Support SQLite- als auch SQLite Driver APIs wie erwartet funktionieren. Dies ermöglicht inkrementelle Migrationen, sodass Sie nicht alle Verwendungen von Support SQLite in einer einzigen Änderung zu SQLite Driver konvertieren müssen.

Room SQLite Wrapper verwenden (optional)

Das Artefakt androidx.room:room-sqlite-wrapper bietet APIs, um während der Migration eine Brücke zwischen SQLiteDriver und SupportSQLiteDatabase zu schlagen.

Wenn Sie eine SupportSQLiteDatabase aus einer RoomDatabase abrufen möchten, die mit einem SQLiteDriver konfiguriert ist, verwenden Sie die neue Erweiterungsfunktion RoomDatabase.getSupportWrapper(). Dieser Kompatibilitäts-Wrapper hilft dabei, vorhandene Verwendungen von SupportSQLiteDatabase (oft von RoomDatabase.openHelper.writableDatabase abgerufen) beizubehalten, während SQLiteDriver übernommen wird. Dies ist besonders nützlich für Codebasen mit umfangreichen SupportSQLite API-Verwendungen, die BundledSQLiteDriver verwenden möchten.

Unterklassen für Migrationen konvertieren

Unterklassen für Migrationen müssen zu den entsprechenden SQLite-Treibern migriert werden:

Kotlin Multiplatform

Unterklassen für Migrationen

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

Unterklassen für automatische Migrationsspezifikationen

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

Nur für Android

Unterklassen für Migrationen

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

Unterklassen für automatische Migrationsspezifikationen

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

Datenbank-Callback konvertieren

Datenbank-Callbacks müssen zu den entsprechenden SQLite-Treibern migriert werden:

Kotlin Multiplatform

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

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

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

Nur für Android

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

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

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

@RawQuery-DAO-Funktionen konvertieren

Für Funktionen mit der Annotation @RawQuery, die für Nicht-Android-Plattformen kompiliert werden, muss ein Parameter vom Typ RoomRawQuery anstelle von SupportSQLiteQuery deklariert werden.

Kotlin Multiplatform

Rohabfrage definieren

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

Mit RoomRawQuery kann dann zur Laufzeit eine Abfrage erstellt werden:

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

Nur für Android

Rohabfrage definieren

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

Mit SimpleSQLiteQuery kann dann zur Laufzeit eine Abfrage erstellt werden:

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

Blockierende DAO-Funktionen konvertieren

Room profitiert von der funktionsreichen asynchronen kotlinx.coroutines-Bibliothek, die Kotlin für mehrere Plattformen bietet. Für eine optimale Funktionalität werden suspend-Funktionen für DAOs erzwungen, die in einem KMP-Projekt kompiliert wurden. Eine Ausnahme bilden DAOs, die in androidMain implementiert wurden, um die Abwärtskompatibilität mit der vorhandenen Codebasis beizubehalten. Wenn Sie Room für KMP verwenden, müssen alle DAO-Funktionen, die für Nicht-Android-Plattformen kompiliert wurden, suspend-Funktionen sein.

Kotlin Multiplatform

Abfragen anhalten

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

Transaktionen anhalten

@Transaction
suspend fun transaction() {  }

Nur für Android

Blockierende Abfragen

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

Blockierende Transaktionen

@Transaction
fun blockingTransaction() {  }

Reaktive Typen zu Flow konvertieren

Nicht alle DAO-Funktionen müssen „suspend“-Funktionen sein. DAO-Funktionen, die reaktive Typen wie LiveData oder Flowable von RxJava zurückgeben, sollten nicht in „suspend“-Funktionen konvertiert werden. Einige Typen wie LiveData sind jedoch nicht mit KMP kompatibel. DAO-Funktionen mit reaktiven Rückgabetypen müssen zu Coroutine-Flows migriert werden.

Kotlin Multiplatform

Reaktive Typen Flows

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

Nur für Android

Reaktive Typen wie LiveData oder Flowable von RxJava

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

Transaktions-APIs konvertieren

Datenbanktransaktions-APIs für Room KMP können zwischen Schreibtransaktionen (useWriterConnection) und Lesetransaktionen (useReaderConnection) unterscheiden.

Kotlin Multiplatform

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

Nur für Android

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

Schreibtransaktionen

Verwenden Sie Schreibtransaktionen, um sicherzustellen, dass mehrere Abfragen Daten atomar schreiben, sodass Leser konsistent auf die Daten zugreifen können. Dazu können Sie useWriterConnection mit einem der drei Transaktionstypen verwenden:

  • immediateTransaction: Im WAL-Modus (Write-Ahead Logging) (Standard) wird bei dieser Art von Transaktion beim Start eine Sperre erworben, aber Leser können weiterhin lesen. Dies ist in den meisten Fällen die bevorzugte Wahl.

  • deferredTransaction: Die Transaktion erwirbt erst bei der ersten Schreibanweisung eine Sperre. Verwenden Sie diese Art von Transaktion als Optimierung, wenn Sie nicht sicher sind, ob innerhalb der Transaktion ein Schreibvorgang erforderlich ist. Wenn Sie beispielsweise eine Transaktion starten, um Songs aus einer Playlist zu löschen, und nur der Name der Playlist angegeben ist und die Playlist nicht vorhanden ist, ist kein Schreibvorgang (Löschen) erforderlich.

  • exclusiveTransaction: Dieser Modus verhält sich im WAL-Modus identisch mit immediateTransaction. In anderen Journaling-Modi wird verhindert, dass andere Datenbankverbindungen die Datenbank lesen, während die Transaktion ausgeführt wird.

Lesetransaktionen

Verwenden Sie Lesetransaktionen, um mehrmals konsistent aus der Datenbank zu lesen. Das ist beispielsweise der Fall, wenn Sie zwei oder mehr separate Abfragen haben und keine JOIN-Klausel verwenden. In Leseverbindungen sind nur verzögerte Transaktionen zulässig. Wenn Sie versuchen, in einer Leseverbindung eine sofortige oder exklusive Transaktion zu starten, wird eine Ausnahme ausgelöst, da diese als Schreibvorgänge betrachtet werden.

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

Nicht in Kotlin Multiplatform verfügbar

Einige der APIs, die für Android verfügbar waren, sind in Kotlin Multiplatform nicht verfügbar.

Abfrage-Callback

Die folgenden APIs zum Konfigurieren von Abfrage-Callbacks sind in „Common“ nicht verfügbar und daher auch nicht auf anderen Plattformen als Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Wir planen, in einer zukünftigen Version von Room Unterstützung für Abfrage-Callbacks hinzuzufügen.

Die API zum Konfigurieren einer RoomDatabase mit einem Abfrage-Callback RoomDatabase.Builder.setQueryCallback sowie die Callback-Oberfläche RoomDatabase.QueryCallback sind in „Common“ nicht verfügbar und daher auch nicht auf anderen Plattformen als Android verfügbar.

Automatisches Schließen der Datenbank

Die API zum Aktivieren des automatischen Schließens nach einem Timeout, RoomDatabase.Builder.setAutoCloseTimeout, ist nur unter Android verfügbar und nicht auf anderen Plattformen.

Vorkonfigurierte Datenbank

Die folgenden APIs zum Erstellen einer RoomDatabase mit einer vorhandenen Datenbank (d.h. einer vorkonfigurierten Datenbank) sind in „Common“ nicht verfügbar und daher auch nicht auf anderen Plattformen als Android. Diese APIs sind:

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

Wir planen, in einer zukünftigen Version von Room Unterstützung für vorkonfigurierte Datenbanken hinzuzufügen.

Entwertung für mehrere Instanzen

Die API zum Aktivieren der Entwertung im Multi-Instanz-Modus, RoomDatabase.Builder.enableMultiInstanceInvalidation, ist nur unter Android verfügbar und nicht in „Common“ oder auf anderen Plattformen.