Biblioteka trwałości Room zapewnia warstwę abstrakcji nad SQLite, aby umożliwić bardziej niezawodny dostęp do bazy danych przy jednoczesnym wykorzystaniu pełnej mocy SQLite. Ta strona zawiera informacje o korzystaniu z Room w projektach Kotlin Multiplatform (KMP). Więcej informacji o korzystaniu z Room znajdziesz w artykule Zapisywanie danych w lokalnej bazie danych za pomocą Room lub w 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]
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" }
Dodaj wtyczkę Room Gradle, aby skonfigurować schematy Room i wtyczkę KSP.
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room)
}
Dodaj zależność środowiska wykonawczego Room i dołączoną bibliotekę SQLite:
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)
}
Dodaj zależności KSP do bloku root dependencies
. Pamiętaj, że 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.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
}
Określ katalog schematu pokoju. Więcej informacji znajdziesz w artykule Ustawianie lokalizacji schematu za pomocą wtyczki Room Gradle.
room {
schemaDirectory("$projectDir/schemas")
}
Definiowanie klas bazy danych
Musisz utworzyć klasę bazy danych z adnotacją @Database
oraz obiekty DAO i encje w wspólnym zestawie źródeł udostępnionego modułu KMP. Umieszczenie tych klas w 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
z interfejsem RoomDatabaseConstructor
, kompilator Room wygeneruje implementacje actual
. Android Studio może wyświetlić to ostrzeżenie, które możesz pominąć 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ś podmioty 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 narzędzia do tworzenia bazy danych specyficznego dla platformy
Aby utworzyć instancję Room na każdej platformie, musisz zdefiniować konstruktora bazy danych. Jest to jedyna część interfejsu API, która musi znajdować się w zestawach źródeł specyficznych dla platformy ze względu na różnice w interfejsach API systemu plików.
Android
Na Androidzie lokalizacja bazy danych jest zwykle uzyskiwana za pomocą interfejsu Context.getDatabasePath()
. Aby utworzyć instancję bazy danych, podaj Context
wraz ze ś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,
)
}
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)
Aby utworzyć instancję bazy danych, podaj ścieżkę do bazy danych za pomocą interfejsów API w językach Java lub 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,
)
}
Tworzenie instancji bazy danych
Po uzyskaniu RoomDatabase.Builder
z jednego z konstruktorów specyficznych dla platformy możesz skonfigurować pozostałą część 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ę konstruktora setDriver
, aby określić, jakiego sterownika SQLite ma używać baza danych Room. Te sterowniki 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. Zapewnia on najbardziej spójną i aktualną wersję SQLite na wszystkich platformach.
Jeśli chcesz używać SQLite dostarczanego przez system operacyjny, użyj interfejsu API setDriver
w zestawach źródeł specyficznych dla platformy, które określają sterownik specyficzny dla platformy. Opisy dostępnych implementacji sterowników znajdziesz w sekcji Implementacje sterowników. Możesz użyć jednej z tych opcji:
AndroidSQLiteDriver
w kręguandroidMain
NativeSQLiteDriver
w kręguiosMain
Aby używać NativeSQLiteDriver
, musisz podać opcję linkera -lsqlite3
, aby aplikacja na iOS dynamicznie łączyła się z systemową bazą danych 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 korutyny (opcjonalnie)
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ą obiektu CoroutineContext
, który można ustawić za pomocą obiektu RoomDatabase.Builder.setCoroutineContext()
. Jeśli nie ustawisz kontekstu, obiekt RoomDatabase
domyślnie będzie używać Dispatchers.IO
.
Minifikacja i zaciemnianie
Jeśli projekt jest zminimalizowany lub zaciemniony, musisz uwzględnić tę regułę ProGuard, aby biblioteka Room mogła znaleźć wygenerowaną implementację definicji bazy danych:
-keep class * extends androidx.room.RoomDatabase { <init>(); }
Migracja do Kotlin Multiplatform
Biblioteka Room została pierwotnie opracowana jako biblioteka Androida, a później przeniesiona do KMP z naciskiem na zgodność interfejsu API. Wersja Room w KMP różni się nieco między platformami i od wersji przeznaczonej na Androida. Różnice te zostały wymienione i opisane poniżej.
Migracja z SQLite Support do sterownika SQLite
Wszelkie użycia SupportSQLiteDatabase
i innych interfejsów API w androidx.sqlite.db
należy zmodyfikować 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).
Ze względu na zgodność wsteczną i dopóki RoomDatabase
jest skonfigurowany z SupportSQLiteOpenHelper.Factory
(np. nie jest ustawiony SQLiteDriver
), Room działa w „trybie zgodności”, w którym interfejsy API Support SQLite i SQLite Driver działają zgodnie z oczekiwaniami. Umożliwia to migracje przyrostowe, dzięki czemu nie musisz konwertować wszystkich zastosowań SQLite Support na sterownik SQLite w ramach jednej zmiany.
Przekształcanie podklas migracji
Podklasy migracji należy przenieść do odpowiedników sterownika SQLite:
Kotlin Multiplatform
Podklasy migracji
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// …
}
}
Podklasy specyfikacji automatycznej migracji
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override 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 należy przenieść do odpowiedników sterownika SQLite:
Kotlin Multiplatform
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(connection: SQLiteConnection) {
// …
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// …
}
override 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 @RawQuery
DAO
Funkcje oznaczone adnotacją @RawQuery
, które są kompilowane na platformy inne niż Android, muszą deklarować parametr typu RoomRawQuery
zamiast SupportSQLiteQuery
.
Kotlin Multiplatform
Zdefiniuj zapytanie w formie nieprzetworzonej
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}
Za pomocą RoomRawQuery
można następnie 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 todoDao().getTodos(query)
}
Tylko Android
Zdefiniuj zapytanie w formie nieprzetworzonej
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}
Za pomocą SimpleSQLiteQuery
można następnie 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 funkcji blokujących DAO
Room korzysta z bogatej w funkcje asynchronicznej biblioteki kotlinx.coroutines
, którą Kotlin udostępnia na wielu platformach. Aby zapewnić optymalną funkcjonalność, suspend
funkcje są wymuszane w przypadku obiektów DAO skompilowanych w projekcie KMP, z wyjątkiem obiektów DAO zaimplementowanych w androidMain
, aby zachować wsteczną zgodność z istniejącą bazą kodu. W przypadku korzystania z biblioteki Room w KMP wszystkie funkcje DAO skompilowane na platformy inne niż Android muszą być funkcjami suspend
.
Kotlin Multiplatform
Wstrzymywanie zapytań
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
Wstrzymywanie transakcji
@Transaction
suspend fun transaction() { … }
Tylko Android
Zapytania dotyczące blokowania
@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. Niektóre typy, np. LiveData
, nie są jednak zgodne z KMP. Funkcje DAO z reaktywnymi typami zwracanymi muszą zostać przeniesione do przepływów współprogramów.
Kotlin Multiplatform
Typy reaktywneFlows
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
Tylko Android
Typy reaktywne, takie jak LiveData
lub Flowable
w RxJava.
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Interfejsy API konwersji transakcji
Interfejsy API transakcji w bazie danych w przypadku Room KMP mogą rozróżniać transakcje zapisu (useWriterConnection
) i odczytu (useReaderConnection
).
Kotlin Multiplatform
val database: RoomDatabase = …
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 w sposób niepodzielny, dzięki czemu czytelnicy mogą stale uzyskiwać dostęp do danych. Możesz to zrobić za pomocą
useWriterConnection
w przypadku dowolnego z 3 rodzajów transakcji:
immediateTransaction
: w trybie Write-Ahead Logging (WAL) (domyślnym) ten typ transakcji uzyskuje blokadę po rozpoczęciu, ale czytelnicy mogą nadal odczytywać dane. Jest to preferowana opcja w większości przypadków.deferredTransaction
: transakcja nie uzyska blokady do czasu pierwszego polecenia 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ę usuwania utworów z playlisty, podając tylko jej nazwę, a playlista nie istnieje, nie będzie potrzebna żadna operacja zapisu (usuwania).exclusiveTransaction
: ten tryb działa identycznie jakimmediateTransaction
w trybie WAL. W innych trybach dziennika zapobiega odczytywaniu bazy danych przez inne połączenia z bazą danych podczas trwania 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 przypadku połączeń z czytnikiem dozwolone są tylko transakcje odroczone. Próba rozpoczęcia natychmiastowej lub wyłącznej transakcji w połączeniu z czytnikiem spowoduje zgłoszenie wyjątku, ponieważ są to operacje „zapisu”.
val database: RoomDatabase = …
database.useReaderConnection { transactor ->
transactor.deferredTransaction {
// perform database operations in transaction
}
}
Niedostępne w Kotlin Multiplatform
Niektóre interfejsy API 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 module common, a tym samym nie są dostępne na platformach innych niż Android.
RoomDatabase.Builder.setQueryCallback
RoomDatabase.QueryCallback
W przyszłej wersji biblioteki Room planujemy dodać obsługę wywołania zwrotnego zapytania.
Interfejs API do konfigurowania RoomDatabase
z wywołaniem zwrotnym zapytaniaRoomDatabase.Builder.setQueryCallback
wraz z interfejsem wywołania zwrotnegoRoomDatabase.QueryCallback
nie są dostępne w przypadku platformy Common, a tym samym nie są dostępne na innych platformach niż Android.
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 i nie jest dostępny na innych platformach.
Wstępnie spakowana baza danych
Te interfejsy API do tworzenia RoomDatabase
za pomocą istniejącej bazy danych (czyli gotowej bazy danych) nie są dostępne w przypadku typowych platform, a tym samym nie są dostępne na platformach innych niż Android. Są to te interfejsy API:
RoomDatabase.Builder.createFromAsset
RoomDatabase.Builder.createFromFile
RoomDatabase.Builder.createFromInputStream
RoomDatabase.PrepackagedDatabaseCallback
W przyszłej wersji biblioteki Room planujemy dodać obsługę gotowych baz danych.
Unieważnianie wielu instancji
Interfejs API do włączania unieważniania w wielu instancjachRoomDatabase.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 JavaScript jest wyłączony.
- Ćwiczenia z programowania dotyczące przenoszenia istniejących aplikacji do Room KMP
- Ćwiczenia z programowania dotyczące KMP
- Zapisywanie danych w lokalnej bazie danych za pomocą biblioteki Room