Ce document explique comment migrer une implémentation Room existante vers une autre qui utilise la multiplateforme Kotlin (KMP).
Migrer les utilisations de Room dans un codebase Android existant vers un KMP partagé commun la difficulté du module peut varier considérablement selon les API Room utilisées ou si le codebase utilise déjà des coroutines. Cette section fournit des conseils et des astuces lorsque vous essayez de migrer des utilisations de Room vers un module commun.
Il est important de vous familiariser d'abord avec les différences et les
entre la version Android de Room et la version KMP, ainsi que
la configuration impliquée. Concrètement, une migration réussie implique la refactorisation
utilisations des API SupportSQLite*
et les remplacer par les API SQLite Driver
ainsi que les déclarations de Room (classe annotée @Database
, DAO,
entités, etc.) en code commun.
Relisez les informations suivantes avant de continuer:
Les sections suivantes décrivent les différentes étapes requises pour réussir la migration.
Passer du pilote SQLite au pilote SQLite
Les API dans androidx.sqlite.db
sont disponibles uniquement sur Android, et toutes les utilisations doivent être
refactorisé avec les API SQLite Driver. Pour assurer la rétrocompatibilité, et tant que
le RoomDatabase
est configuré avec un SupportSQLiteOpenHelper.Factory
(par exemple,
qu'aucun SQLiteDriver
n'est défini), Room se comporte alors en "mode de compatibilité" où
Les API Support SQLite et SQLite Driver fonctionnent comme prévu. Cela permet
des migrations incrémentielles afin de ne pas avoir à convertir
au pilote SQLite en une seule modification.
Les exemples suivants illustrent des utilisations courantes de l'assistance SQLite et de leur SQLite Équivalents conducteurs:
Compatibilité avec SQLite (à partir de)
Exécuter une requête sans résultat
val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")
Exécuter une requête avec un résultat mais sans argument
val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
while (cusor.moveToNext()) {
// read columns
cursor.getInt(0)
cursor.getString(1)
}
}
Exécuter une requête avec un résultat et des arguments
database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
if (cursor.moveToNext()) {
// row found, read columns
} else {
// row not found
}
}
Pilote SQLite (vers)
Exécuter une requête sans résultat
val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")
Exécuter une requête avec un résultat mais sans argument
val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
while (statement.step()) {
// read columns
statement.getInt(0)
statement.getText(1)
}
}
Exécuter une requête avec un résultat et des arguments
connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
statement.bindInt(1, id)
if (statement.step()) {
// row found, read columns
} else {
// row not found
}
}
Les API de transaction de base de données sont disponibles directement dans SupportSQLiteDatabase
avec
beginTransaction()
, setTransactionSuccessful()
et endTransaction()
.
Ils sont également disponibles dans Room avec runInTransaction()
. Migrer ces éléments
vers les API SQLite Driver.
Compatibilité avec SQLite (à partir de)
Effectuer une transaction (avec RoomDatabase
)
val database: RoomDatabase = ...
database.runInTransaction {
// perform database operations in transaction
}
Effectuer une transaction (avec SupportSQLiteDatabase
)
val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
// perform database operations in transaction
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
Pilote SQLite (vers)
Effectuer une transaction (avec RoomDatabase
)
val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
Effectuer une transaction (avec 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")
}
Différents remplacements de rappel doivent également être migrés vers leurs équivalents pilotes:
Compatibilité avec SQLite (à partir de)
Sous-classes de migration
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// ...
}
}
Sous-classes de spécification de migration automatique
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// ...
}
}
Sous-classes de rappel de base de données
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(db: SupportSQLiteDatabase) {
// ...
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// ...
}
override fun onOpen(db: SupportSQLiteDatabase) {
// ...
}
}
Pilote SQLite (vers)
Sous-classes de migration
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// ...
}
}
Sous-classes de spécification de migration automatique
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// ...
}
}
Sous-classes de rappel de base de données
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(connection: SQLiteConnection) {
// ...
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// ...
}
override fun onOpen(connection: SQLiteConnection) {
// ...
}
}
Pour résumer, remplacez les utilisations de SQLiteDatabase
par SQLiteConnection
lorsqu'un
RoomDatabase
n'est pas disponible, comme dans les remplacements de rappel (onMigrate
,
onCreate
, etc.). Si un RoomDatabase
est disponible, accédez à l'instance
connexion à la base de données à l'aide de RoomDatabase.useReaderConnection
et
RoomDatabase.useWriterConnection
au lieu de
RoomDatabase.openHelper.writtableDatabase
Convertir les fonctions DAO bloquantes en fonctions de suspension
La version KMP de Room repose sur des coroutines pour effectuer des E/S.
des opérations sur le CoroutineContext
configuré. Cela signifie que vous
n'avez pas besoin de migrer toutes les fonctions DAO bloquantes vers les fonctions de suspension.
Blocage de la fonction DAO (de)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Suspension de la fonction DAO (vers)
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
La migration des fonctions de blocage DAO existantes vers des fonctions de suspension peut être compliquée si le codebase existant n'intègre pas déjà des coroutines. Consultez Coroutines sur Android pour découvrir comment utiliser des coroutines. dans votre codebase.
Convertir les types renvoyés réactifs en flux
Toutes les fonctions DAO n'ont pas besoin d'être des fonctions de suspension. Les fonctions DAO qui renvoient
Les types réactifs tels que LiveData
ou Flowable
de RxJava ne doivent pas être convertis
des fonctions de suspension. Toutefois, certains types, comme LiveData
, ne sont pas des KMP
compatibles. Les fonctions DAO avec des types renvoyés réactifs doivent être migrées vers
flux de coroutine.
Type de KMP incompatible (à partir de)
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Type de KMP compatible (vers)
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
Reportez-vous à la section Flux dans Android pour découvrir comment utiliser les flux dans votre de votre codebase.
Définir un contexte de coroutine (facultatif)
Vous pouvez éventuellement configurer un RoomDatabase
avec une application partagée
exécuteurs utilisant RoomDatabase.Builder.setQueryExecutor()
pour exécuter la base de données
opérations. Comme les exécuteurs ne sont pas compatibles avec KMP, le setQueryExecutor()
de Room
L'API n'est pas disponible pour les sources courantes. À la place, RoomDatabase
doit
être configuré avec un CoroutineContext
. Un contexte peut être défini à l'aide de
RoomDatabase.Builder.setCoroutineContext()
, si aucune valeur n'est définie, le
RoomDatabase
utilisera Dispatchers.IO
par défaut.
Configurer un pilote SQLite
Une fois que les utilisations de Support SQLite ont été migrées vers les API SQLite Driver,
le pilote doit être configuré avec RoomDatabase.Builder.setDriver
. La
Le conducteur recommandé est BundledSQLiteDriver
. Consultez Implémentations de pilotes pour
des implémentations de pilotes disponibles.
SupportSQLiteOpenHelper.Factory
personnalisé configuré avec
Les RoomDatabase.Builder.openHelperFactory()
ne sont pas compatibles avec KMP,
les fonctionnalités fournies par l'assistant ouvert personnalisé devront être réimplémentées avec
Interfaces du pilote SQLite.
Déplacer les déclarations Room
Une fois la plupart des étapes de migration terminées, vous pouvez déplacer Room
à un ensemble de sources commun. Notez que les stratégies expect
/ actual
peuvent
être utilisée pour déplacer progressivement les définitions liées à Room. Par exemple, si les
les fonctions DAO bloquantes peuvent être migrées vers des fonctions de suspension, il est possible de
déclarer une interface annotée expect
@Dao
vide dans le code commun, mais
contient des fonctions de blocage dans Android.
// 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
}