Lorsque vous ajoutez ou modifiez des fonctionnalités dans votre application, vous devez modifier vos classes d'entités Room et les tables de base de données sous-jacentes afin de refléter ces modifications. Il est important de conserver les données utilisateur déjà présentes dans la base de données sur l'appareil lorsqu'une mise à jour d'application modifie le schéma de la base de données.
Room prend en charge les options automatisées et manuelles pour une migration incrémentielle. Les migrations automatiques fonctionnent pour la plupart des modifications de schéma de base, mais vous devrez peut-être définir manuellement les chemins de migration pour des modifications plus complexes.
Migrations automatisées
Pour déclarer une migration automatisée entre deux versions de base de données, ajoutez une
@AutoMigration annotation à la autoMigrations propriété dans
@Database :
// Database class before the version update. @Database( version = 1, entities = [User::class] ) abstract class AppDatabaseV1 : RoomDatabase() { abstract fun userDao(): UserDao } // Database class after the version update. @Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration(from = 1, to = 2) ] ) abstract class AppDatabaseV2 : RoomDatabase() { abstract fun userDao(): UserDao }
Spécifications de la migration automatique
Si Room détecte des modifications de schéma ambiguës et qu'il ne parvient pas à générer de plan de migration
sans informations supplémentaires, il génère une erreur au moment de la compilation et vous devez fournir une implémentation
AutoMigrationSpec. Cela se produit généralement lorsqu'une migration implique l'une des opérations suivantes :
- Supprimer ou renommer une table
- Supprimer ou renommer une colonne
Vous pouvez utiliser AutoMigrationSpec pour fournir les informations supplémentaires dont Room a besoin pour générer correctement les chemins de migration. Définissez une classe qui implémente AutoMigrationSpec dans votre classe RoomDatabase, puis annotez-la avec une ou plusieurs des annotations suivantes :
Si vous souhaitez utiliser l'implémentation de AutoMigrationSpec pour une migration automatisée, définissez la propriété spec dans l'annotation @AutoMigration correspondante :
@Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration ( from = 1, to = 2, spec = MigrationSpec1To2::class ) ] ) abstract class AppDatabaseWithSpec : RoomDatabase() { abstract fun userDao(): UserDao } @RenameTable(fromTableName = "User", toTableName = "AppUser") internal class MigrationSpec1To2 : AutoMigrationSpec
Si votre application doit effectuer plus de travail une fois la migration automatisée terminée, vous
pouvez implémenter onPostMigrate. Si vous implémentez cette fonction dans votre AutoMigrationSpec, Room l'appelle une fois la migration automatisée terminée.
Migrations manuelles
Si une migration implique des modifications de schéma complexes, il se peut que Room ne soit pas en mesure de générer automatiquement un chemin de migration approprié. Par exemple, si vous décidez de diviser les données d'une table en deux, Room ne peut pas déterminer comment effectuer cette division. Dans de tels cas, vous devez définir manuellement un
chemin de migration en implémentant une Migration classe.
Une classe Migration définit explicitement un chemin de migration entre une startVersion
et une endVersion en remplaçant la fonction migrate. Ajoutez
vos Migration classes à votre compilateur de base de données à l'aide de la
addMigrations fonction :
val MIGRATION_1_2 = object : Migration(1, 2) { override suspend fun migrate(connection: SQLiteConnection) { connection.executeSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " + "PRIMARY KEY(`id`))") } } val MIGRATION_2_3 = object : Migration(2, 3) { override suspend fun migrate(connection: SQLiteConnection) { connection.executeSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER") } } Room.databaseBuilder<ManualMigrationDatabase>(applicationContext, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build()
Lorsque vous définissez des chemins de migration, vous pouvez utiliser des migrations automatisées pour certaines versions et des migrations manuelles pour d'autres. Si vous définissez à la fois une migration automatisée et une migration manuelle pour la même version, Room utilise la migration manuelle.
Tester les migrations
Les migrations sont souvent complexes. Une migration définie de manière incorrecte peut entraîner le plantage de votre application. Pour préserver la stabilité de votre application, testez vos migrations. Room fournit un artefact Maven room3-testing pour faciliter le processus de test des migrations automatisées et manuelles. Pour que cet artefact fonctionne, vous devez d'abord exporter le schéma de votre base de données.
Exporter des schémas
Room exporte les informations de schéma de votre base de données dans un fichier JSON au moment de la compilation. Les fichiers JSON exportés représentent l'historique des schémas de votre base de données. Stockez ces fichiers dans votre système de contrôle des versions afin de pouvoir recréer des versions antérieures de la base de données à des fins de test et pour permettre la génération de migrations automatisées.
Définir l'emplacement du schéma à l'aide du plug-in Room Gradle
Pour spécifier le répertoire du schéma, appliquez le plug-in Room Gradle et utilisez l'
room3 extension.
Groovy
plugins {
id 'androidx.room3'
}
room3 {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room3")
}
room3 {
schemaDirectory("$projectDir/schemas")
}
Si le schéma de votre base de données diffère selon la variante, le type de produit ou le type de compilation, vous devez spécifier différents emplacements à l'aide de la configuration schemaDirectory plusieurs fois, chacun avec un variantMatchName comme premier argument. Chaque configuration peut correspondre à une ou plusieurs variantes en fonction d'une simple comparaison avec le nom de la variante.
Assurez-vous qu'elles sont exhaustives et couvrent toutes les variantes. Vous pouvez également inclure un schemaDirectory() sans variantMatchName pour gérer les variantes qui ne correspondent à aucune des autres configurations. Par exemple, dans une application avec deux types de produits (demo et full) et deux types de compilation (debug et release), les configurations suivantes sont valides :
Groovy
room3 {
// Applies to 'demoDebug' only
schemaDirectory "demoDebug", "$projectDir/schemas/demoDebug"
// Applies to 'demoDebug' and 'demoRelease'
schemaDirectory "demo", "$projectDir/schemas/demo"
// Applies to 'demoDebug' and 'fullDebug'
schemaDirectory "debug", "$projectDir/schemas/debug"
// Applies to variants that aren't matched by other configurations.
schemaDirectory "$projectDir/schemas"
}
Kotlin
room3 {
// Applies to 'demoDebug' only
schemaDirectory("demoDebug", "$projectDir/schemas/demoDebug")
// Applies to 'demoDebug' and 'demoRelease'
schemaDirectory("demo", "$projectDir/schemas/demo")
// Applies to 'demoDebug' and 'fullDebug'
schemaDirectory("debug", "$projectDir/schemas/debug")
// Applies to variants that aren't matched by other configurations.
schemaDirectory("$projectDir/schemas")
}
Définir l'emplacement du schéma à l'aide de l'option de processeur d'annotations
Si vous n'utilisez pas le plug-in Room Gradle, définissez l'emplacement du schéma à l'aide de l'option de processeur d'annotations room.schemaLocation.
Gradle utilise les fichiers de ce répertoire comme entrées et sorties pour certaines tâches Gradle.
Pour garantir l'exactitude et les performances des builds incrémentiels et mis en cache, vous devez utiliser
Gradle's CommandLineArgumentProvider pour informer Gradle de
ce répertoire.
Commencez par copier la classe RoomSchemaArgProvider suivante dans le fichier de compilation Gradle de votre module. La fonction asArguments de l'exemple de classe transmet room.schemaLocation=${schemaDir.path} à KSP. Si vous utilisez KAPT et javac, remplacez cette valeur par -Aroom.schemaLocation=${schemaDir.path}.
Groovy
class RoomSchemaArgProvider implements CommandLineArgumentProvider {
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
File schemaDir
RoomSchemaArgProvider(File schemaDir) {
this.schemaDir = schemaDir
}
@Override
Iterable<String> asArguments() {
return ["room.schemaLocation=${schemaDir.path}".toString()]
}
}
Kotlin
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File
) : CommandLineArgumentProvider {
override fun asArguments(): Iterable<String> {
return listOf("room.schemaLocation=${schemaDir.path}")
}
}
Configurez ensuite les options de compilation pour utiliser RoomSchemaArgProvider avec le répertoire de schéma spécifié :
Groovy
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
Kotlin
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
Tester une seule migration
Avant de pouvoir tester vos migrations, ajoutez l'artefact androidx.room3:room3-testing à vos dépendances de test, puis ajoutez l'emplacement du schéma exporté en tant que répertoire d'éléments :
Groovy
android { ... sourceSets { // Adds exported schema location as test app assets if not using // the Room Gradle Plugin. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... androidTestImplementation "androidx.room3:room3-testing:3.0.1" }
Kotlin
android { ... sourceSets { // Adds exported schema location as test app assets if not using // the Room Gradle Plugin. getByName("androidTest").assets.srcDir("$projectDir/schemas") } } dependencies { ... testImplementation("androidx.room3:room3-testing:3.0.1") }
Le package de test fournit une MigrationTestHelper classe, qui peut lire
les fichiers de schéma exportés. Le package implémente également l'interface JUnit4
TestRule pour gérer les bases de données créées.
L'exemple suivant présente un test pour une seule migration :
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" private val instrumentation = InstrumentationRegistry.getInstrumentation() @get:Rule val helper = MigrationTestHelper( instrumentation = instrumentation, databaseClass = MigrationDb::class, driver = AndroidSQLiteDriver(), file = instrumentation.targetContext.getDatabasePath(TEST_DB), ) @Test fun migrate1To2() = runTest { val connection = helper.createDatabase(1) // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. connection.execSQL("INSERT INTO User (id, name) VALUES (1, 'John Doe')") connection.close() // Re-open the database with version 2 and provide MIGRATION_1_2 val migratedConnection = helper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2)) // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. val hasData = migratedConnection.prepare("SELECT COUNT(*) FROM User").use { it.step() it.getLong(0) > 0 } assertTrue("Expected data was not migrated", hasData) migratedConnection.close() } }
Tester toutes les migrations
Bien que vous puissiez tester une seule migration incrémentielle, vous devez inclure un test qui couvre toutes les migrations définies pour la base de données de votre application. Cela permet de s'assurer qu'il n'y a pas de différence entre une instance de base de données récemment créée et une instance antérieure qui a suivi les chemins de migration définis.
L'exemple suivant présente un test pour toutes les migrations définies :
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" private val instrumentation = InstrumentationRegistry.getInstrumentation() // Array of all migrations. private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( instrumentation = instrumentation, databaseClass = MigrationDb::class, driver = AndroidSQLiteDriver(), file = instrumentation.targetContext.getDatabasePath(TEST_DB), ) @Test fun migrateAll() = runTest { // Create earliest version of the database. val connection = helper.createDatabase(1) connection.close() // Create latest version of the database. val db = Room.databaseBuilder<AppDatabase>(instrumentation.targetContext, TEST_DB) .setDriver(AndroidSQLiteDriver()) .addMigrations(*ALL_MIGRATIONS) .build() // Open the database, Room validates the schema once all migrations // execute. db.useReaderConnection { connection -> // Perform additional validation } db.close() } }
Gérer correctement les chemins de migration manquants
Si Room ne parvient pas à trouver un chemin de migration pour mettre à niveau une base de données existante sur un
appareil, une IllegalStateException se produit. S'il
est acceptable de perdre des données existantes lorsqu'un chemin de migration est manquant, appelez
la fallbackToDestructiveMigration fonction de compilateur lorsque vous créez
la base de données :
Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name") .fallbackToDestructiveMigration() .build()
Cette fonction configure Room pour recréer de manière destructive les tables de la base de données de votre application lorsqu'une migration incrémentielle est nécessaire en l'absence de chemin de migration défini.
Pour n'avoir recours à la recréation destructive que dans certaines situations, utilisez l'une des alternatives suivantes à fallbackToDestructiveMigration :
- Si des versions spécifiques de votre historique des schémas génèrent des erreurs que vous ne pouvez pas résoudre
avec les chemins de migration, utilisez
fallbackToDestructiveMigrationFromplutôt. Cette fonction indique que vous souhaitez que Room ait uniquement recours à la recréation destructive lors de la migration à partir de versions spécifiques. - Si vous souhaitez que Room ait uniquement recours à la recréation destructive lors de la migration
d'une version de base de données ultérieure vers une version antérieure, utilisez
fallbackToDestructiveMigrationOnDowngradeplutôt.