Ce document explique comment migrer une implémentation de Room existante vers une implémentation utilisant la multiplateforme Kotlin (KMP).
La migration des utilisations de Room dans un codebase Android existant vers un module KMP partagé commun peut varier considérablement en fonction des API Room utilisées ou si le codebase utilise déjà des coroutines. Cette section propose des conseils et des astuces lorsque vous tentez de migrer des utilisations de Room vers un module courant.
Il est important de vous familiariser d'abord avec les différences et les fonctionnalités manquantes entre la version Android de Room et la version KMP, ainsi que la configuration concernée. En substance, une migration réussie implique de refactoriser les utilisations des API SupportSQLite*
et de les remplacer par les API SQLite Driver, ainsi que de déplacer les déclarations Room (classe annotée @Database
, DAO, entités, etc.) dans un code commun.
Consultez à nouveau les informations suivantes avant de continuer:
Les sections suivantes décrivent les différentes étapes requises pour une migration réussie.
Passer du pilote SQLite d'assistance au pilote SQLite
Les API de androidx.sqlite.db
ne fonctionnent qu'avec Android, et toutes les utilisations doivent être refactorisées avec les API SQLite Driver. Pour assurer la rétrocompatibilité, et tant que RoomDatabase
est configuré avec un SupportSQLiteOpenHelper.Factory
(c'est-à-dire qu'aucun SQLiteDriver
n'est défini), Room se comporte en "mode de compatibilité", où les API de pilote SQLite et SQLite fonctionnent comme prévu. Cela active les migrations incrémentielles, ce qui vous évite d'avoir à convertir toutes vos utilisations de SQLite Support en pilote SQLite en une seule modification.
Les exemples suivants sont des utilisations courantes de Support SQLite et de leurs équivalents à partir du pilote SQLite:
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()
.
Elles sont également disponibles dans Room via runInTransaction()
. Migrez ces utilisations vers les API SQLite Driver.
Compatibilité avec SQLite (à partir de)
Effectuer une transaction (à l'aide de RoomDatabase
)
val database: RoomDatabase = ...
database.runInTransaction {
// perform database operations in transaction
}
Effectuer une transaction (à l'aide de SupportSQLiteDatabase
)
val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
// perform database operations in transaction
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
Pilote SQLite (vers)
Effectuer une transaction (à l'aide de RoomDatabase
)
val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
Effectuer une transaction (à l'aide de 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")
}
Plusieurs 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 la 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 la 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, par exemple dans les remplacements de rappel (onMigrate
, onCreate
, etc.). Si un RoomDatabase
est disponible, accédez à la connexion de base de données sous-jacente à 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 s'appuie sur des coroutines pour effectuer des opérations d'E/S sur le CoroutineContext
configuré. Cela signifie que vous devez migrer toutes les fonctions DAO bloquantes pour suspendre des fonctions.
Blocage de la fonction DAO (à partir de)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Suspension de la fonction DAO (à)
@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à de coroutines. Consultez la page Coroutines sur Android pour commencer à utiliser des coroutines dans votre codebase.
Convertir les types renvoyés réactifs en flux
Les fonctions DAO ne doivent pas toutes être des fonctions de suspension. Les fonctions DAO qui renvoient des types réactifs tels que LiveData
ou Flowable
de RxJava ne doivent pas être converties en fonctions de suspension. Toutefois, certains types, tels que LiveData
, ne sont pas compatibles avec KMP. Les fonctions DAO avec des types renvoyés réactifs doivent être migrées vers des flux de coroutines.
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 page Flux dans Android pour commencer à utiliser des flux dans votre codebase.
Définir un contexte de coroutine (facultatif)
Un RoomDatabase
peut éventuellement être configuré avec des exécuteurs d'applications partagés utilisant RoomDatabase.Builder.setQueryExecutor()
pour effectuer des opérations de base de données. Étant donné que les exécuteurs ne sont pas compatibles avec KMP, l'API setQueryExecutor()
de Room 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 aucun n'est défini, RoomDatabase
utilise Dispatchers.IO
par défaut.
Définir un pilote SQLite
Une fois que les utilisations de l'assistance SQLite ont été migrées vers les API du pilote SQLite, un pilote doit être configuré à l'aide de RoomDatabase.Builder.setDriver
. Le pilote recommandé est BundledSQLiteDriver
. Consultez la section Implémentations de pilotes pour obtenir une description des implémentations de pilotes disponibles.
Les SupportSQLiteOpenHelper.Factory
personnalisés configurés à l'aide de 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 les interfaces du pilote SQLite.
Déplacer des déclarations Room
Une fois la plupart des étapes de migration terminées, vous pouvez déplacer les définitions Room vers un ensemble de sources commun. Notez que les stratégies expect
/ actual
peuvent être utilisées pour déplacer progressivement les définitions associées à Room. Par exemple, si les fonctions DAO bloquantes ne peuvent pas toutes être migrées vers des fonctions de suspension, vous pouvez déclarer une interface annotée expect
@Dao
vide dans le code commun, mais contenant 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
}