Room (Kotlin Multiplatform)

Die Room-Persistenzbibliothek bietet eine Abstraktionsebene für SQLite, die einen robusteren Datenbankzugriff ermöglicht und gleichzeitig die volle Leistungsfähigkeit von SQLite nutzt. 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 mit Room in einer lokalen Datenbank 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 die Datei build.gradle.kts für Ihr KMP-Modul ein.

Definieren Sie die Abhängigkeiten in der Datei 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" }

Room-Gradle-Plug-in zum Konfigurieren von Room-Schemas und des KSP-Plug-ins hinzufügen

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 dem root-Block die KSP-Abhängigkeiten hinzu.dependencies Sie müssen alle Zielvorhaben hinzufügen, die in Ihrer App verwendet werden. 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 Verzeichnis für das Room-Schema. Weitere Informationen finden Sie unter Schemaspeicherort mit dem Room-Gradle-Plug-in festlegen.

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

Datenbankklassen definieren

Sie müssen eine mit @Database annotierte Datenbankklasse sowie DAOs und Entitäten im gemeinsamen Quellsatz 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 @Suppress("KotlinNoActualForExpect") unterdrücken können:

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

Definieren Sie als Nächstes entweder eine neue DAO-Schnittstelle oder verschieben Sie eine vorhandene in 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 nach 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 bei den Dateisystem-APIs in plattformspezifischen Quellsätzen enthalten sein muss.

Android

Unter Android wird der Datenbankstandort 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 NSFileManager an, der sich in der Regel 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)

Um die Datenbankinstanz zu erstellen, geben Sie 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 die RoomDatabase.Builder von einem der plattformspezifischen Konstruktoren erhalten haben, können Sie den Rest der Room-Datenbank im gemeinsamen Code zusammen mit der eigentlichen 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, welcher SQLite-Treiber von der Room-Datenbank verwendet werden soll. Diese Treiber unterscheiden sich je nach Zielplattform. In den vorherigen Code-Snippets wird BundledSQLiteDriver verwendet. Dies ist der empfohlene Treiber, der aus dem Quellcode kompilierte SQLite-Versionen enthält. So wird auf allen Plattformen die konsistenteste und aktuellste Version von SQLite bereitgestellt.

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

Wenn Sie NativeSQLiteDriver verwenden möchten, müssen Sie die 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 gemeinsam genutzten Anwendungs-Executors konfiguriert werden, die RoomDatabase.Builder.setQueryExecutor() verwenden, um Datenbankvorgänge auszuführen.

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

Reduzierung und Verschleierung

Wenn das Projekt minimiert 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 etwas zwischen den Plattformen und von der Android-spezifischen Version. Diese Unterschiede werden im Folgenden aufgeführt und beschrieben.

Von Support SQLite zu SQLite-Treiber 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 die RoomDatabase mit einer SupportSQLiteOpenHelper.Factory konfiguriert ist (z. B. keine SQLiteDriver festgelegt ist), verhält sich Room im „Kompatibilitätsmodus“, in dem sowohl die Support SQLite- als auch die SQLite Driver-APIs wie erwartet funktionieren. Dies ermöglicht inkrementelle Migrationen, sodass Sie nicht alle Support-SQLite-Verwendungen in einer einzigen Änderung in den SQLite-Treiber konvertieren müssen.

Migrationsunterklassen konvertieren

Migrationsunterklassen müssen zu den SQLite-Treiber-Entsprechungen migriert werden:

Kotlin Multiplatform

Migrationsunterklassen

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

Unterklassen der Spezifikation für die automatische Migration

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

Nur Android

Migrationsunterklassen

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

Unterklassen der Spezifikation für die automatische Migration

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

Convert-Datenbank-Callback

Datenbank-Callbacks müssen zu den SQLite-Treiber-Entsprechungen migriert werden:

Kotlin Multiplatform

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

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

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

Nur 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, die mit @RawQuery annotiert und 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 einem 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 Android

Rohabfrage definieren

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

Mit einem 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 werden. Eine Ausnahme bilden DAOs, die in androidMain implementiert sind, um die Abwärtskompatibilität mit der vorhandenen Codebasis aufrechtzuerhalten. Wenn Sie Room für KMP verwenden, müssen alle DAO-Funktionen, die für Nicht-Android-Plattformen kompiliert werden, suspend-Funktionen sein.

Kotlin Multiplatform

Abfragen pausieren

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

Transaktionen aussetzen

@Transaction
suspend fun transaction() {  }

Nur Android

Abfragen blockieren

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

Transaktionen blockieren

@Transaction
fun blockingTransaction() {  }

Reaktive Typen in Flow konvertieren

Nicht alle DAO-Funktionen müssen suspend-Funktionen sein. DAO-Funktionen, die reaktive Typen wie LiveData oder RxJavas Flowable zurückgeben, sollten nicht in suspend-Funktionen konvertiert werden. Einige Typen, z. B. LiveData, sind jedoch nicht 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 Android

Reaktive Typen wie LiveData oder Flowable von RxJava

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

Transaktions-APIs umstellen

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

Kotlin Multiplatform

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

Nur Android

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

Schreibtransaktionen

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

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

  • deferredTransaction: Die Transaktion erhält erst nach der ersten Schreibanweisung eine Sperre. Verwenden Sie diese Art von Transaktion als Optimierung, wenn Sie sich nicht sicher sind, ob innerhalb der Transaktion ein Schreibvorgang erforderlich ist. Wenn Sie beispielsweise eine Transaktion zum Löschen von Titeln aus einer Playlist starten, die nur einen Namen hat und 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 läuft.

Transaktionen lesen

Verwenden Sie Lesetransaktionen, um mehrmals konsistent aus der Datenbank zu lesen. Das kann beispielsweise der Fall sein, wenn Sie zwei oder mehr separate Abfragen haben und keine JOIN-Klausel verwenden. In Leser-Verbindungen sind nur verzögerte Transaktionen zulässig. Wenn Sie versuchen, eine sofortige oder exklusive Transaktion in einer Leser-Verbindung 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.

Rückruf bei Anfragen

Die folgenden APIs zum Konfigurieren von Rückrufen für Anfragen sind nicht in „common“ verfügbar und daher nicht auf anderen Plattformen als Android verfügbar.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

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

Die API zum Konfigurieren eines RoomDatabase mit einem Abfrage-Callback RoomDatabase.Builder.setQueryCallback sowie die Callback-Schnittstelle RoomDatabase.QueryCallback sind nicht in „common“ verfügbar und daher nicht auf anderen Plattformen als Android verfügbar.

Automatisches Schließen der Datenbank

Die API zum automatischen Schließen nach einem Zeitlimit, RoomDatabase.Builder.setAutoCloseTimeout, ist nur für Android verfügbar und nicht für andere Plattformen.

Datenbank vorab verpacken

Die folgenden APIs zum Erstellen eines RoomDatabase mit einer vorhandenen Datenbank (d.h. einer vorgefertigten Datenbank) sind nicht allgemein 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 vorab gepackte Datenbanken hinzuzufügen.

Entwertung mehrerer Instanzen

Die API zum Aktivieren der Multi-Instance-Invalidierung, RoomDatabase.Builder.enableMultiInstanceInvalidation, ist nur für Android verfügbar und nicht für andere Plattformen.