In diesem Dokument wird beschrieben, wie Sie eine vorhandene Room-Implementierung zu einer Implementierung migrieren, die Kotlin Multiplatform (KMP) verwendet.
Der Schwierigkeitsgrad der Migration von Raumnutzungen in einer vorhandenen Android-Codebasis zu einem gemeinsamen freigegebenen KMP-Modul kann je nach den verwendeten Room APIs oder davon, ob die Codebasis bereits Coroutines verwendet, stark variieren. In diesem Abschnitt finden Sie einige Anleitungen und Tipps, wenn Sie versuchen, die Nutzung von Room zu einem gemeinsamen Modul zu migrieren.
Machen Sie sich zuerst mit den Unterschieden und fehlenden Funktionen zwischen der Android-Version von Room und der KMP-Version sowie mit der Einrichtung vertraut. Im Wesentlichen beinhaltet eine erfolgreiche Migration, die Nutzung der SupportSQLite*
APIs zu refaktorieren und durch SQLite-Treiber-APIs zu ersetzen sowie Room-Deklarationen (@Database
-annotierte Klasse, DAOs, Entitäten usw.) in gemeinsamen Code zu verschieben.
Lesen Sie die folgenden Informationen, bevor Sie fortfahren:
In den nächsten Abschnitten werden die verschiedenen Schritte für eine erfolgreiche Migration beschrieben.
Von Support-SQLite zum SQLite-Treiber migrieren
Die APIs in androidx.sqlite.db
sind ausschließlich für Android verfügbar und jegliche Nutzungen müssen mit SQLite-Treiber APIs refaktoriert werden. Aus Gründen der Abwärtskompatibilität und solange RoomDatabase
mit einem SupportSQLiteOpenHelper.Factory
konfiguriert ist (d.h. es ist kein SQLiteDriver
festgelegt), verhält sich Room im "Kompatibilitätsmodus", in dem sowohl die Unterstützung von SQLite- als auch die SQLite-Treiber-APIs wie erwartet funktionieren. Dies ermöglicht inkrementelle Migrationen, sodass Sie nicht alle Ihre Support-SQLite-Nutzungen in einer einzigen Änderung in den SQLite-Treiber konvertieren müssen.
Die folgenden Beispiele zeigen gängige Anwendungsbereiche von Support SQLite und die entsprechenden SQLite-Treiber:
Unterstützung von SQLite (von)
Abfrage ohne Ergebnis ausführen
val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")
Abfrage mit Ergebnis, aber ohne Argumente ausführen
val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
while (cusor.moveToNext()) {
// read columns
cursor.getInt(0)
cursor.getString(1)
}
}
Abfrage mit Ergebnis und Argumenten ausführen
database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
if (cursor.moveToNext()) {
// row found, read columns
} else {
// row not found
}
}
SQLite-Treiber (to)
Abfrage ohne Ergebnis ausführen
val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")
Abfrage mit Ergebnis, aber ohne Argumente ausführen
val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
while (statement.step()) {
// read columns
statement.getInt(0)
statement.getText(1)
}
}
Abfrage mit Ergebnis und Argumenten ausführen
connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
statement.bindInt(1, id)
if (statement.step()) {
// row found, read columns
} else {
// row not found
}
}
Datenbank-Transaktions-APIs sind direkt in SupportSQLiteDatabase
mit beginTransaction()
, setTransactionSuccessful()
und endTransaction()
verfügbar.
Sie sind auch über „Room“ mit runInTransaction()
verfügbar. Migrieren Sie diese Anwendungen zu SQLite Driver APIs.
Unterstützung von SQLite (von)
Transaktion ausführen (mit RoomDatabase
)
val database: RoomDatabase = ...
database.runInTransaction {
// perform database operations in transaction
}
Transaktion ausführen (mit SupportSQLiteDatabase
)
val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
// perform database operations in transaction
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
SQLite-Treiber (to)
Transaktion ausführen (mit RoomDatabase
)
val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
Transaktion ausführen (mit SQLiteConnection
)
val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
// perform database operations in transaction
connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
connection.execSQL("ROLLBACK TRANSACTION")
}
Verschiedene Callback-Überschreibungen müssen auch zu ihren Treiber-Entsprechungen migriert werden:
Unterstützung von SQLite (von)
Unterklassen für Migration
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// ...
}
}
Unterklassen für die automatische Migrationsspezifikation
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// ...
}
}
Unterklassen für Datenbank-Callbacks
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(db: SupportSQLiteDatabase) {
// ...
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// ...
}
override fun onOpen(db: SupportSQLiteDatabase) {
// ...
}
}
SQLite-Treiber (to)
Unterklassen für Migration
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// ...
}
}
Unterklassen für die automatische Migrationsspezifikation
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// ...
}
}
Unterklassen für Datenbank-Callbacks
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(connection: SQLiteConnection) {
// ...
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// ...
}
override fun onOpen(connection: SQLiteConnection) {
// ...
}
}
Zusammenfassend lässt sich sagen, dass die Verwendung von SQLiteDatabase
durch SQLiteConnection
ersetzt wird, wenn ein RoomDatabase
nicht verfügbar ist, z. B. in Callback-Überschreibungen (onMigrate
, onCreate
usw.). Wenn ein RoomDatabase
verfügbar ist, greifen Sie mit RoomDatabase.useReaderConnection
und RoomDatabase.useWriterConnection
anstelle von RoomDatabase.openHelper.writtableDatabase
auf die zugrunde liegende Datenbankverbindung zu.
Blockierende DAO-Funktionen in Sperren von Funktionen umwandeln
Die KMP-Version von Room benötigt Koroutinen, um E/A-Vorgänge für die konfigurierte CoroutineContext
auszuführen. Dies bedeutet, dass Sie alle blockierenden DAO-Funktionen migrieren müssen, um Funktionen anzuhalten.
DAO-Funktion blockieren (von)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
DAO-Funktion anhalten (to)
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
Das Migrieren vorhandener DAO-Blockierfunktionen in Anhalten von Funktionen kann kompliziert sein, wenn die vorhandene Codebasis nicht bereits Koroutinen enthält. Informationen zum Verwenden von Koroutinen in Ihrer Codebasis finden Sie unter Koroutinen in Android.
Reaktive Rückgabetypen in Ablauf umwandeln
Nicht alle DAO-Funktionen müssen Aussetzerfunktionen sein. DAO-Funktionen, die reaktive Typen wie LiveData
oder Flowable
von RxJava zurückgeben, sollten nicht in Aussetzenfunktionen konvertiert werden. Einige Typen wie LiveData
sind jedoch nicht KMP-kompatibel. DAO-Funktionen mit reaktiven Rückgabetypen müssen zu Koroutinenabläufen migriert werden.
Inkompatibler KMP-Typ (von)
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Kompatibler KMP-Typ (zu)
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
Informationen zum Einstieg in die Verwendung von Abläufen in Ihrer Codebasis finden Sie unter Abläufe in Android.
Koroutinenkontext festlegen (optional)
Ein RoomDatabase
kann optional mit freigegebenen Anwendungs-Executors konfiguriert werden, indem RoomDatabase.Builder.setQueryExecutor()
zum Ausführen von Datenbankvorgängen verwendet wird. Da Executors nicht KMP-kompatibel sind, ist die setQueryExecutor()
API von Room nicht für allgemeine Quellen verfügbar. Stattdessen muss RoomDatabase
mit einer CoroutineContext
konfiguriert werden. Ein Kontext kann mit RoomDatabase.Builder.setCoroutineContext()
festgelegt werden. Wenn keiner festgelegt ist, verwendet RoomDatabase
standardmäßig Dispatchers.IO
.
SQLite-Treiber einrichten
Nachdem die Support-SQLite-Nutzungen zu SQLite-Treiber-APIs migriert wurden, muss ein Treiber mit RoomDatabase.Builder.setDriver
konfiguriert werden. Der empfohlene Treiber ist BundledSQLiteDriver
. Eine Beschreibung der verfügbaren Treiberimplementierungen finden Sie unter Treiberimplementierungen.
Benutzerdefinierte SupportSQLiteOpenHelper.Factory
, die mit RoomDatabase.Builder.openHelperFactory()
konfiguriert wurden, werden in KMP nicht unterstützt. Die vom benutzerdefinierten Open Helper bereitgestellten Features müssen mit SQLite-Treiberschnittstellen noch einmal implementiert werden.
Raumdeklarationen verschieben
Nachdem die meisten Migrationsschritte abgeschlossen sind, können Sie die Raumdefinitionen in einen gemeinsamen Quellsatz verschieben. Beachten Sie, dass die Strategien expect
oder actual
verwendet werden können, um raumbezogene Definitionen schrittweise zu verschieben. Wenn beispielsweise nicht alle blockierenden DAO-Funktionen zum Anhalten von Funktionen migriert werden können, ist es möglich, eine expect
@Dao
-annotierte Schnittstelle zu deklarieren, die im allgemeinen Code leer ist, aber blockierende Funktionen in Android enthält.
// shared/src/commonMain/kotlin/Database.kt
@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getDao(): TodoDao
abstract fun getBlockingDao(): BlockingTodoDao
}
@Dao
interface TodoDao {
@Query("SELECT count(*) FROM TodoEntity")
suspend fun count(): Int
}
@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt
@Dao
actual interface BlockingTodoDao {
@Query("SELECT count(*) FROM TodoEntity")
fun count(): Int
}