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 annotation @AutoMigration
à la propriété autoMigrations
dans @Database
:
Kotlin
// Database class before the version update. @Database( version = 1, entities = [User::class] ) abstract class AppDatabase : RoomDatabase() { ... } // Database class after the version update. @Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration (from = 1, to = 2) ] ) abstract class AppDatabase : RoomDatabase() { ... }
Java
// Database class before the version update. @Database( version = 1, entities = {User.class} ) public abstract class AppDatabase extends RoomDatabase { ... } // Database class after the version update. @Database( version = 2, entities = {User.class}, autoMigrations = { @AutoMigration (from = 1, to = 2) } ) public abstract class AppDatabase extends RoomDatabase { ... }
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 demande d'implémenter une 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 statique 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 :
Kotlin
@Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration ( from = 1, to = 2, spec = AppDatabase.MyAutoMigration::class ) ] ) abstract class AppDatabase : RoomDatabase() { @RenameTable(fromTableName = "User", toTableName = "AppUser") class MyAutoMigration : AutoMigrationSpec ... }
Java
@Database( version = 2, entities = {AppUser.class}, autoMigrations = { @AutoMigration ( from = 1, to = 2, spec = AppDatabase.MyAutoMigration.class ) } ) public abstract class AppDatabase extends RoomDatabase { @RenameTable(fromTableName = "User", toTableName = "AppUser") static class MyAutoMigration implements 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 méthode 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 classe Migration
.
Une classe Migration
définit explicitement un chemin de migration entre une startVersion
et une endVersion
en remplaçant la méthode Migration.migrate()
. Ajoutez vos classes Migration
à votre compilateur de base de données à l'aide de la méthode addMigrations()
:
Kotlin
val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " + "PRIMARY KEY(`id`))") } } val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER") } } Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
Java
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " + "`name` TEXT, PRIMARY KEY(`id`))"); } }; static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE Book " + " ADD COLUMN pub_year INTEGER"); } }; Room.databaseBuilder(getApplicationContext(), MyDb.class, "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 room-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 peut exporter 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. Magasin ces fichiers dans votre système de contrôle des versions afin que Room puisse créer des versions antérieures la base de données à des fins de test et pour permettre la génération de migration automatique.
Définir l'emplacement du schéma à l'aide du plug-in Room Gradle
Si vous utilisez Room 2.6.0 ou une version ultérieure, vous pouvez appliquer le
plug-in Room Gradle et utilisez le
room
pour spécifier le répertoire du schéma.
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Si le schéma de votre base de données diffère en fonction de la variante, du type ou de la compilation
vous devez spécifier des emplacements différents à l'aide de l'schemaDirectory()
configuration plusieurs fois, chacune avec un variantMatchName
comme premier
. Chaque configuration peut correspondre à une ou plusieurs variantes en fonction de critères
une 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 sans correspondance
par l'une des autres configurations. Par exemple, dans une application avec deux versions
types demo
et full
, ainsi que deux types de compilation debug
et release
,
sont les configurations valides:
Groovy
room {
// 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
room {
// 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 utilisez la version 2.5.2 ou une version antérieure de Room, ou si vous n'utilisez pas la
Plug-in Gradle de Room, définissez l'emplacement du schéma à l'aide de room.schemaLocation
du processeur d'annotations.
Les fichiers de ce répertoire sont utilisés comme entrées et sorties pour certaines tâches Gradle.
Pour garantir l'exactitude et les performances des compilations incrémentielles et mises en cache, vous devez utiliser
de Gradle
CommandLineArgumentProvider
pour informer Gradle de ce répertoire.
Tout d'abord, copiez la classe RoomSchemaArgProvider
ci-dessous dans le fichier
fichier de compilation Gradle. La méthode 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() {
// Note: If you're using KAPT and javac, change the line below to
// return ["-Aroom.schemaLocation=${schemaDir.path}".toString()].
return ["room.schemaLocation=${schemaDir.path}".toString()]
}
}
Kotlin
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File
) : CommandLineArgumentProvider {
override fun asArguments(): Iterable<String> {
// Note: If you're using KAPT and javac, change the line below to
// return listOf("-Aroom.schemaLocation=${schemaDir.path}").
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
// For KSP, configure using KSP extension:
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
// For javac or KAPT, configure using android DSL:
android {
...
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
compilerArgumentProviders(
new RoomSchemaArgProvider(new File(projectDir, "schemas"))
)
}
}
}
}
Kotlin
// For KSP, configure using KSP extension:
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
// For javac or KAPT, configure using android DSL:
android {
...
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
compilerArgumentProviders(
RoomSchemaArgProvider(File(projectDir, "schemas"))
)
}
}
}
}
Tester une seule migration
Avant de pouvoir tester vos migrations, ajoutez l'artefact Maven androidx.room:room-testing
de Room à vos dépendances de test, puis ajoutez l'emplacement du schéma exporté en tant que dossier d'éléments :
build.gradle
Groovy
android { ... sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... androidTestImplementation "androidx.room:room-testing:2.6.1" }
Kotlin
android { ... sourceSets { // Adds exported schema location as test app assets. getByName("androidTest").assets.srcDir("$projectDir/schemas") } } dependencies { ... testImplementation("androidx.room:room-testing:2.6.1") }
Le package de test fournit une classe MigrationTestHelper
, qui peut lire les fichiers de schéma exportés. Le package implémente également l'interface TestRule
de JUnit4 pour lui permettre de gérer les bases de données créées.
L'exemple suivant présente un test pour une seule migration :
Kotlin
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), MigrationDb::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory() ) @Test @Throws(IOException::class) fun migrate1To2() { var db = helper.createDatabase(TEST_DB, 1).apply { // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. execSQL(...) // Prepare for the next version. close() } // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
Java
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), MigrationDb.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrate1To2() throws IOException { SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. db.execSQL(...); // Prepare for the next version. db.close(); // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2); // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
Tester toutes les migrations
Bien qu'il soit possible de tester une seule migration incrémentielle, nous vous recommandons d'inclure un test qui couvre toutes les migrations définies pour la base de données de votre application. Cela garantit qu'il n'y a pas de différence entre une instance de base de données récemment créée et une instance plus ancienne qui a suivi les chemins de migration définis.
L'exemple suivant présente un test pour toutes les migrations définies :
Kotlin
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" // Array of all migrations. private val ALL_MIGRATIONS = arrayOf( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory() ) @Test @Throws(IOException::class) fun migrateAll() { // Create earliest version of the database. helper.createDatabase(TEST_DB, 1).apply { close() } // Open latest version of the database. Room validates the schema // once all migrations execute. Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java, TEST_DB ).addMigrations(*ALL_MIGRATIONS).build().apply { openHelper.writableDatabase.close() } } }
Java
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrateAll() throws IOException { // Create earliest version of the database. SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); db.close(); // Open latest version of the database. Room validates the schema // once all migrations execute. AppDatabase appDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().getTargetContext(), AppDatabase.class, TEST_DB) .addMigrations(ALL_MIGRATIONS).build(); appDb.getOpenHelper().getWritableDatabase(); appDb.close(); } // Array of all migrations. private static final Migration[] ALL_MIGRATIONS = new Migration[]{ MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4}; }
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 méthode de compilateur fallbackToDestructiveMigration()
lorsque vous créez la base de données :
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Cette méthode indique à Room de 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.
Si vous souhaitez que Room ait uniquement recours à la recréation destructive dans certaines situations, il existe quelques alternatives à 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 plutôt
fallbackToDestructiveMigrationFrom()
. Cette méthode 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 plutôt
fallbackToDestructiveMigrationOnDowngrade()
.
Gérer les valeurs par défaut des colonnes lors de la mise à niveau vers Room 2.2.0
À partir de la version 2.2.0, vous pouvez définir une valeur par défaut pour une colonne à l'aide de l'annotation @ColumnInfo(defaultValue = "...")
.
Dans les versions antérieures à la version 2.2.0, le seul moyen de définir une valeur par défaut pour une colonne consiste à la définir directement dans une instruction SQL exécutée, qui crée une valeur par défaut dont Room n'a pas connaissance. Cela signifie que si une base de données a initialement été créée par une version antérieure à Room 2.2.0, la mise à niveau de votre application pour utiliser Room 2.2.0 peut nécessiter un chemin de migration spécial pour les valeurs par défaut existantes que vous avez définies sans utiliser les API Room.
Par exemple, supposons que la version 1 d'une base de données définisse une entité Song
:
Kotlin
// Song entity, database version 1, Room 2.1.0. @Entity data class Song( @PrimaryKey val id: Long, val title: String )
Java
// Song entity, database version 1, Room 2.1.0. @Entity public class Song { @PrimaryKey final long id; final String title; }
Supposons également que la version 2 de la même base de données ajoute une nouvelle colonne NOT NULL
et définit un chemin de migration de la version 1 vers la version 2 :
Kotlin
// Song entity, database version 2, Room 2.1.0. @Entity data class Song( @PrimaryKey val id: Long, val title: String, val tag: String // Added in version 2. ) // Migration from 1 to 2, Room 2.1.0. val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''") } }
Java
// Song entity, database version 2, Room 2.1.0. @Entity public class Song { @PrimaryKey final long id; final String title; @NonNull final String tag; // Added in version 2. } // Migration from 1 to 2, Room 2.1.0. static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''"); } };
Cela entraîne une différence dans la table sous-jacente entre les mises à jour et les nouvelles installations de l'appli. La valeur par défaut de la colonne tag
étant uniquement déclarée dans le chemin de migration de la version 1 à la version 2, les utilisateurs qui installent l'appli à partir de la version 2 ne disposent pas de la valeur par défaut pour tag
dans leur schéma de base de données.
Dans les versions antérieures à Room 2.2.0, cette différence est sans conséquence. Toutefois, si l'application est mise à niveau ultérieurement pour utiliser Room 2.2.0 ou une version ultérieure et modifie la classe d'entité Song
pour inclure une valeur par défaut pour tag
à l'aide de l'annotation @ColumnInfo
, Room peut voir cette différence. Cela entraîne l'échec des validations de schéma.
Pour garantir la cohérence du schéma de base de données pour tous les utilisateurs lorsque des valeurs par défaut des colonnes sont déclarées dans vos chemins de migration précédents, procédez comme suit lors de la première mise à niveau de votre application pour qu'elle utilise Room 2.2.0 ou une version ultérieure :
- Déclarez les valeurs par défaut des colonnes dans leurs classes d'entités respectives à l'aide de l'annotation
@ColumnInfo
. - Augmentez le numéro de version de la base de données d'une unité.
- Définissez un chemin de migration vers la nouvelle version qui implémente la stratégie de suppression et de recréation pour ajouter les valeurs par défaut nécessaires aux colonnes existantes.
L'exemple suivant illustre ce processus :
Kotlin
// Migration from 2 to 3, Room 2.2.0. val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL(""" CREATE TABLE new_Song ( id INTEGER PRIMARY KEY NOT NULL, name TEXT, tag TEXT NOT NULL DEFAULT '' ) """.trimIndent()) database.execSQL(""" INSERT INTO new_Song (id, name, tag) SELECT id, name, tag FROM Song """.trimIndent()) database.execSQL("DROP TABLE Song") database.execSQL("ALTER TABLE new_Song RENAME TO Song") } }
Java
// Migration from 2 to 3, Room 2.2.0. static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE new_Song (" + "id INTEGER PRIMARY KEY NOT NULL," + "name TEXT," + "tag TEXT NOT NULL DEFAULT '')"); database.execSQL("INSERT INTO new_Song (id, name, tag) " + "SELECT id, name, tag FROM Song"); database.execSQL("DROP TABLE Song"); database.execSQL("ALTER TABLE new_Song RENAME TO Song"); } };