Biblioteka trwałości danych Room zapewnia warstwę abstrakcji nad SQLite, aby umożliwić bardziej niezawodny dostęp do bazy danych przy jednoczesnym wykorzystaniu pełnej mocy SQLite. Na tej stronie skupiamy się na używaniu Room w projektach Kotlin Multiplatform (KMP). Więcej informacji o korzystaniu z biblioteki Room znajdziesz w artykule Zapisywanie danych w lokalnej bazie danych za pomocą biblioteki Room lub w naszych oficjalnych przykładach.
Konfigurowanie zależności
Aby skonfigurować Room w projekcie KMP, dodaj zależności artefaktów w pliku build.gradle.kts modułu KMP.
Zdefiniuj zależności w pliku 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" }
Dodaj wtyczkę Room Gradle, aby skonfigurować schematy Room i wtyczkę KSP.
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room3)
}
Dodaj zależność środowiska wykonawczego Room i dołączoną bibliotekę SQLite:
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)
}
Dodaj zależności KSP do głównego bloku dependencies. Musisz dodać wszystkie cele, których używa Twoja aplikacja. Więcej informacji znajdziesz w artykule KSP z 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
}
Zdefiniuj katalog schematu Room. Więcej informacji znajdziesz w artykule Ustawianie lokalizacji schematu za pomocą wtyczki Room Gradle.
room {
schemaDirectory("$projectDir/schemas")
}
Definiowanie klas baz danych
Musisz utworzyć klasę bazy danych z adnotacją @Database oraz obiekty DAO i encje w zbiorze źródeł wspólnych udostępnionego modułu KMP. Umieszczenie tych klas we wspólnych źródłach umożliwi ich udostępnianie na wszystkich platformach docelowych.
// 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 }
Gdy zadeklarujesz obiekt expect za pomocą interfejsu RoomDatabaseConstructor, kompilator Room wygeneruje implementacje actual. Android Studio może wyświetlić to ostrzeżenie, które możesz ukryć za pomocą @Suppress("KotlinNoActualForExpect"):
Expected object 'AppDatabaseConstructor' has no actual declaration in module`
Następnie zdefiniuj nowy interfejs DAO lub przenieś istniejący do 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>> }
Zdefiniuj lub przenieś elementy do commonMain:
// shared/src/commonMain/kotlin/TodoEntity.kt @Entity data class TodoEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val content: String )
Tworzenie kreatorów baz danych dla konkretnych platform
Aby utworzyć instancję Room na każdej platformie, musisz zdefiniować kreator bazy danych. To jedyna część interfejsu API, którą musisz zdefiniować w zbiorach źródeł dla konkretnych platform ze względu na różnice w interfejsach API systemów plików.
Android
Na Androidzie lokalizację bazy danych zwykle uzyskuje się za pomocą interfejsu API Context.getDatabasePath. Aby utworzyć instancję bazy danych, określ Context i ścieżkę bazy danych.
// 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
Aby utworzyć instancję bazy danych na iOS, podaj ścieżkę bazy danych za pomocą NSFileManager, która zwykle znajduje się w 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) }
JVM na komputery
Aby utworzyć instancję bazy danych, podaj ścieżkę bazy danych za pomocą interfejsów API Javy lub Kotlina.
// 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,
)
}
Tworzenie instancji bazy danych
Po uzyskaniu RoomDatabase.Builder z jednego z konstruktorów dla konkretnej platformy możesz skonfigurować resztę bazy danych Room we wspólnym kodzie wraz z rzeczywistym utworzeniem instancji bazy danych.
// shared/src/commonMain/kotlin/Database.kt fun getRoomDatabase( builder: RoomDatabase.Builder<AppDatabase> ): AppDatabase { return builder .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) .build() }
Wybierz sterownik SQLite
Poprzedni fragment kodu wywołuje funkcję kreatora setDriver, aby określić, jakiego sterownika SQLite powinna używać baza danych Room. Sterowniki te różnią się w zależności od platformy docelowej. Poprzednie fragmenty kodu używają BundledSQLiteDriver.
Jest to zalecany sterownik, który zawiera SQLite skompilowany ze źródła, co zapewnia najbardziej spójną i aktualną wersję SQLite na wszystkich platformach.
Jeśli chcesz używać bazy SQLite dostarczanej przez system operacyjny, użyj interfejsu API setDriver w zbiorach źródeł dla konkretnej platformy, które określają sterownik dla konkretnej platformy. Opisy dostępnych implementacji sterowników znajdziesz w sekcji Implementacje sterowników. Możesz użyć jednej z tych opcji:
AndroidSQLiteDriverw:androidMainNativeSQLiteDriverw:iosMain
Aby używać NativeSQLiteDriver, musisz podać opcję tagu łączącego -lsqlite3, aby aplikacja na iOS dynamicznie łączyła się z systemem SQLite.
// 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")
}
}
}
Ustawianie kontekstu współprogramu
Obiekt RoomDatabase na Androidzie można opcjonalnie skonfigurować za pomocą współdzielonych wykonawców aplikacji, używając RoomDatabase.Builder.setQueryExecutor do wykonywania operacji na bazie danych.
Ponieważ wykonawcy nie są zgodni z KMP, interfejs API setQueryExecutor Room nie jest dostępny w commonMain. Zamiast tego obiekt RoomDatabase musi być skonfigurowany za pomocą CoroutineContext, który możesz ustawić za pomocą RoomDatabase.Builder.setCoroutineContext. Jeśli nie ustawisz kontekstu, obiekt RoomDatabase będzie domyślnie ustawiony na Dispatchers.IO.
Minifikacja i zaciemnianie
Jeśli projekt jest zminimalizowany lub zaciemniony, musisz uwzględnić tę regułę ProGuard, aby Room mógł znaleźć wygenerowaną implementację definicji bazy danych:
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
Migracja do Kotlin Multiplatform
Room został pierwotnie opracowany jako biblioteka Androida, a później przeniesiony do KMP z naciskiem na zgodność interfejsu API. Wersja KMP Room różni się nieco między platformami i od wersji na Androida. Różnice te są wymienione i opisane poniżej.
Migracja z pomocniczego SQLite na sterownik SQLite
Wszystkie przypadki użycia SupportSQLiteDatabase i innych interfejsów API w androidx.sqlite.db muszą zostać poddane refaktoryzacji za pomocą interfejsów API sterownika SQLite, ponieważ interfejsy API w androidx.sqlite.db są przeznaczone tylko na Androida (zwróć uwagę na inny pakiet niż pakiet KMP).
Aby zachować zgodność wsteczną, o ile RoomDatabase jest skonfigurowany z SupportSQLiteOpenHelper.Factory (np. nie ustawiono SQLiteDriver), Room działa w „trybie zgodności”, w którym zarówno interfejsy API Support SQLite, jak i SQLite Driver działają zgodnie z oczekiwaniami. Umożliwia to migracje przyrostowe, dzięki czemu nie musisz konwertować wszystkich zastosowań Support SQLite na sterownik SQLite w ramach jednej zmiany.
Używanie kodu opakowującego SQLite w Room
Artefakt androidx.room3:room3-sqlite-wrapper udostępnia interfejsy API, które podczas migracji łączą SQLiteDriver i SupportSQLiteDatabase.
Aby uzyskać SupportSQLiteDatabase z RoomDatabase skonfigurowanego za pomocą SQLiteDriver, użyj nowej funkcji rozszerzenia RoomDatabase.getSupportWrapper. Ta otoczka zgodności pomaga zachować dotychczasowe zastosowania SupportSQLiteDatabase, które często uzyskuje się z RoomDatabase.openHelper.writableDatabase, podczas wdrażania SQLiteDriver, zwłaszcza w przypadku baz kodu z rozległymi zastosowaniami interfejsu API SupportSQLite, które chcą używać BundledSQLiteDriver.
Przekształcanie podklas migracji
Podklasy migracji muszą zostać przeniesione do odpowiedników sterowników SQLite:
Kotlin Multiplatform
Podklasy migracji
object Migration_1_2 : Migration(1, 2) { override suspend fun migrate(connection: SQLiteConnection) { // ... } }
Podklasy specyfikacji automatycznej migracji
class AutoMigrationSpec_1_2 : AutoMigrationSpec { override suspend fun onPostMigrate(connection: SQLiteConnection) { // ... } }
Tylko Android
Podklasy migracji
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// …
}
}
Podklasy specyfikacji automatycznej migracji
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// …
}
}
Wywołanie zwrotne konwersji bazy danych
Wywołania zwrotne bazy danych muszą zostać przeniesione do odpowiedników sterowników SQLite:
Kotlin Multiplatform
object MyRoomCallback : RoomDatabase.Callback() { override suspend fun onCreate(connection: SQLiteConnection) { // ... } override suspend fun onDestructiveMigration(connection: SQLiteConnection) { // ... } override suspend fun onOpen(connection: SQLiteConnection) { // ... } }
Tylko Android
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
// …
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// …
}
override fun onOpen(db: SupportSQLiteDatabase) {
// …
}
}
Konwertowanie funkcji DAO @RawQuery
Funkcje z adnotacją @RawQuery, które są kompilowane na platformy inne niż platforma Androida, będą musiały deklarować parametr typu RoomRawQuery zamiast SupportSQLiteQuery.
Kotlin Multiplatform
Definiowanie nieprzetworzonego zapytania
@Dao interface TodoDao { @RawQuery suspend fun getTodos(query: RoomRawQuery): List<TodoEntity> }
Następnie można użyć RoomRawQuery, aby utworzyć zapytanie w czasie działania:
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) }
Tylko Android
Definiowanie nieprzetworzonego zapytania
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}
Następnie można użyć SimpleSQLiteQuery, aby utworzyć zapytanie w czasie działania:
suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
val query = SimpleSQLiteQuery(
query = "SELECT * FROM TodoEntity WHERE title = ?",
bindArgs = arrayOf(title.lowercase())
)
return getTodos(query)
}
Konwertowanie blokujących funkcji DAO
Room korzysta z bogatej w funkcje asynchronicznej biblioteki kotlinx.coroutines, którą Kotlin oferuje na wielu platformach. Aby zapewnić optymalne działanie, funkcje suspend są wymuszane w przypadku obiektów DAO skompilowanych w projekcie KMP, z wyjątkiem obiektów DAO zaimplementowanych w androidMain, aby zachować zgodność wsteczną z istniejącą bazą kodu. W przypadku korzystania z Room dla KMP wszystkie funkcje DAO skompilowane na platformy inne niż platforma Androida muszą być funkcjami suspend.
Kotlin Multiplatform
Zawieszam zapytania
@Query("SELECT * FROM Todo") suspend fun getAllTodos(): List<Todo>
zawieszanie transakcji,
@Transaction suspend fun transaction() { // ... }
Tylko Android
Blokowanie zapytań
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Blokowanie transakcji
@Transaction
fun blockingTransaction() { … }
Konwertowanie typów reaktywnych na Flow
Nie wszystkie funkcje DAO muszą być funkcjami zawieszania. Funkcje DAO, które zwracają typy reaktywne, takie jak LiveData lub Flowable RxJava, nie powinny być konwertowane na funkcje zawieszające. Jednak niektóre typy, takie jak LiveData, nie są zgodne z KMP. Funkcje DAO z reaktywnymi zwracanymi typami muszą zostać przeniesione do przepływów współprogramów.
Kotlin Multiplatform
Typy reaktywne: Flows
@Query("SELECT * FROM Todo") fun getTodosFlow(): Flow<List<Todo>>
Tylko Android
Typy reaktywne, takie jak LiveData lub Flowable RxJava
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Konwertowanie interfejsów API transakcji
Interfejsy API transakcji bazy danych dla Room KMP mogą rozróżniać transakcje zapisu (useWriterConnection) i odczytu (useReaderConnection).
Kotlin Multiplatform
val database: RoomDatabase = getRoomDatabase(builder) database.useWriterConnection { transactor -> transactor.immediateTransaction { // perform database operations in transaction. } }
Tylko Android
val database: RoomDatabase = …
database.withTransaction {
// perform database operations in transaction
}
Transakcje zapisu
Używaj transakcji zapisu, aby mieć pewność, że wiele zapytań zapisuje dane niepodzielnie, dzięki czemu czytelnicy mogą stale uzyskiwać dostęp do danych. Możesz to zrobić za pomocą useWriterConnection, korzystając z jednego z 3 typów transakcji:
immediateTransaction: w trybie zapisywania logów z wyprzedzeniem (WAL) (domyślnym) ten typ transakcji uzyskuje blokadę po uruchomieniu, ale czytelnicy mogą nadal czytać. Jest to preferowany wybór w większości przypadków.deferredTransaction: transakcja nie uzyska blokady do czasu pierwszej instrukcji zapisu. Używaj tego typu transakcji jako optymalizacji, gdy nie masz pewności, czy w ramach transakcji będzie potrzebna operacja zapisu. Jeśli na przykład rozpoczniesz transakcję, aby usunąć utwory z playlisty, podając tylko jej nazwę, a playlista nie istnieje, nie jest wymagana żadna operacja zapisu (usunięcia).exclusiveTransaction: ten tryb działa tak samo jakimmediateTransactionw trybie WAL. W innych trybach kronikowania uniemożliwia innym połączeniom z bazą danych odczytywanie bazy danych podczas transakcji.
Transakcje odczytu
Używaj transakcji odczytu, aby wielokrotnie odczytywać dane z bazy danych w spójny sposób. Na przykład gdy masz co najmniej 2 osobne zapytania i nie używasz klauzuli JOIN. W połączeniach z czytelnikami dozwolone są tylko transakcje odroczone. Próba rozpoczęcia natychmiastowej lub wyłącznej transakcji w połączeniu z czytnikiem spowoduje wyjątek, ponieważ są one uważane za operacje „zapisu”.
val database: RoomDatabase = getRoomDatabase(builder) database.useReaderConnection { transactor -> transactor.deferredTransaction { // perform database operations in transaction. } }
Niedostępne w Kotlin Multiplatform
Niektóre interfejsy API, które były dostępne na Androidzie, nie są dostępne w Kotlin Multiplatform.
Wywołanie zwrotne zapytania
Te interfejsy API do konfigurowania wywołań zwrotnych zapytań nie są dostępne w wersji wspólnej, więc nie są dostępne na platformach innych niż Android.
RoomDatabase.Builder.setQueryCallbackRoomDatabase.QueryCallback
Automatyczne zamykanie bazy danych
Interfejs API umożliwiający automatyczne zamykanie po upływie limitu czasu, RoomDatabase.Builder.setAutoCloseTimeout, jest dostępny tylko na Androidzie.
Gotowa baza danych
Te interfejsy API do tworzenia RoomDatabase przy użyciu istniejącej bazy danych (bazy danych w pakiecie) nie są dostępne w kodzie wspólnym, więc nie można ich używać na platformach innych niż Android:
RoomDatabase.Builder.createFromAssetRoomDatabase.Builder.createFromFileRoomDatabase.Builder.createFromInputStreamRoomDatabase.PrepackagedDatabaseCallback
Unieważnianie wielu instancji
Interfejs API umożliwiający unieważnianie w trybie podzielonym, RoomDatabase.Builder.enableMultiInstanceInvalidation, jest dostępny tylko na Androidzie i nie jest dostępny na platformach wspólnych ani innych.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Ćwiczenia z programowania dotyczące migracji istniejących aplikacji do Room KMP
- Ćwiczenia z programowania dotyczące KMP
- Zapisywanie danych w lokalnej bazie danych za pomocą biblioteki Room