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:
AndroidSQLiteDriver
inandroidMain
NativeSQLiteDriver
iniosMain
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 mitimmediateTransaction
. 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.
Empfehlungen für dich
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Bestehende Apps zum Room KMP-Codelab migrieren
- Codelab „Mit KMP starten“
- Daten mit Room in einer lokalen Datenbank speichern