A medida que agregas y cambias funciones de tu app, debes modificar las clases de entidad de Room y las tablas de base de datos subyacentes para reflejar esos cambios. Es importante conservar los datos del usuario que ya están en la base de datos del dispositivo cuando una actualización de la app cambia el esquema de la base de datos.
Room admite opciones manuales y automáticas para la migración incremental. Las migraciones automáticas funcionan para la mayoría de los cambios de esquema básicos, pero es posible que debas definir rutas de migración de forma manual si se requieren cambios más complejos.
Migraciones automáticas
Para declarar una migración automática entre dos versiones de base de datos, agrega una anotación @AutoMigration
a la propiedad autoMigrations
en @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 { ... }
Especificaciones de la migración automática
Si Room detecta cambios de esquema ambiguos y no puede generar un plan de migración sin más entradas, arrojará un error de tiempo de compilación y te solicitará que implementes un AutoMigrationSpec
.
Por lo general, esto ocurre cuando una migración involucra una de las siguientes opciones:
- Borrar una tabla o cambiarle el nombre
- Borrar una columna o cambiarle el nombre
Puedes usar AutoMigrationSpec
para darle a Room la información adicional que necesita para generar correctamente rutas de migración. Define una clase estática que implemente AutoMigrationSpec
en tu clase RoomDatabase
y agrega una o más de las siguientes opciones de anotación:
Si deseas usar la implementación de AutoMigrationSpec
para una migración automatizada, configura la propiedad spec
en la anotación @AutoMigration
correspondiente:
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 la app necesita realizar más trabajo una vez que se completa la migración automática, puedes implementar onPostMigrate()
.
Si implementas este método en tu AutoMigrationSpec
, Room lo llamará después de que se complete la migración automática.
Migraciones manuales
En los casos en los que una migración implica cambios de esquema complejos, es posible que Room no pueda generar automáticamente una ruta de migración adecuada. Por ejemplo, si decides dividir los datos de una tabla en dos, Room no sabrá cómo realizar esta división. En casos como estos, debes definir manualmente una ruta de migración mediante la implementación de una clase Migration
.
Cada clase Migration
define de forma explícita una ruta de migración entre una startVersion
y una endVersion
anulando el método Migration.migrate()
. Agrega las clases Migration
definidas al compilador de bases de datos mediante el método 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();
Cuando defines las rutas de migración, puedes usar las migraciones automáticas para algunas versiones y las manuales en otras. Si defines una migración automática y una manual para la misma versión, Room usará la manual.
Cómo probar migraciones
Las migraciones suelen ser complejas, y una migración definida de forma incorrecta puede provocar que falle tu app. Para preservar la estabilidad de tu app, debes probar las migraciones. Room proporciona un artefacto Maven room-testing
para ayudar en el proceso de prueba de las migraciones automáticas y manuales. Para que este artefacto funcione, primero debes exportar el esquema de la base de datos.
Cómo exportar esquemas
Room puede exportar la información del esquema de la base de datos a un archivo JSON durante el tiempo de compilación. Los archivos JSON exportados representan el historial de esquemas de la base de datos. Tienda estos archivos en tu sistema de control de versión para que Room pueda crear versiones anteriores de la base de datos con fines de prueba y para habilitar la generación de migración automática.
Cómo establecer la ubicación del esquema con el complemento de Gradle de Room
Si usas la versión de Room 2.6.0 o una versión posterior, puedes aplicar el
Complemento de Gradle de Room y usa el
room
para especificar el directorio del esquema.
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Si el esquema de tu base de datos difiere según la variante, el tipo o la compilación
, debes especificar diferentes ubicaciones mediante el parámetro schemaDirectory()
configuración varias veces, cada una con una variantMatchName
como primera
argumento. Cada configuración puede coincidir con una o más variantes en función de un control
comparación con el nombre de la variante.
Asegúrate de que sean exhaustivas y abarquen todas las variantes. También puedes incluir
schemaDirectory()
sin un variantMatchName
para manejar variantes que no coinciden
por cualquiera de las otras configuraciones. Por ejemplo, en una app con dos modelos
las variantes demo
y full
, y dos tipos de compilación debug
y release
, el
siguientes son configuraciones válidas:
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")
}
Cómo establecer la ubicación del esquema con la opción del procesador de anotaciones
Si usas la versión 2.5.2 o anterior de Room, o si no usas
El complemento de Gradle de Room, configura la ubicación del esquema con room.schemaLocation
.
opción de procesador de anotaciones.
Los archivos incluidos en este directorio se usan como entrada y salida para algunas tareas de Gradle.
Para la precisión y el rendimiento de las compilaciones incrementales y almacenadas en caché, debes usar
de Gradle
CommandLineArgumentProvider
para informar a Gradle sobre este directorio.
Primero, copia la clase RoomSchemaArgProvider
que se muestra a continuación en el archivo
Archivo de compilación de Gradle. El método asArguments()
de la clase de muestra pasa
room.schemaLocation=${schemaDir.path}
a KSP
. Si usas KAPT
y
javac
, cambia este valor a -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}")
}
}
Luego, configura las opciones de compilación para usar RoomSchemaArgProvider
con el
directorio del esquema especificado:
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"))
)
}
}
}
}
Cómo probar una sola migración
Para poder probar las migraciones, debes agregar el artefacto Maven androidx.room:room-testing
de Room a tus dependencias de prueba y la ubicación del esquema exportado como una carpeta de elementos:
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") }
El paquete de prueba proporciona una clase MigrationTestHelper
, que puede leer archivos de esquema exportados. El paquete también implementa la interfaz TestRule
de JUnit4, por lo que puede administrar las bases de datos creadas.
En el siguiente ejemplo, se muestra una prueba para una sola migración:
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. } }
Cómo probar todas las migraciones
Aunque es posible probar una sola migración incremental, se recomienda que incluyas una prueba que abarque todas las migraciones definidas para la base de datos de tu app. Esto garantiza que no haya discrepancias entre una instancia de base de datos creada recientemente y una instancia anterior que siguió las rutas de migración definidas.
En el siguiente ejemplo, se muestra una prueba para todas las migraciones definidas:
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}; }
Cómo resolver de manera óptima la falta de rutas de migración
Si Room no puede encontrar una ruta de migración para actualizar una base de datos existente en un dispositivo a la versión actual, se produce una IllegalStateException
. Si no es un problema perder datos existentes cuando falta una ruta de migración, llama al método de compilador fallbackToDestructiveMigration()
cuando crees la base de datos:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Este método le indica a Room que vuelva a crear las tablas de la base de datos en tu app de manera destructiva cuando necesite realizar una migración incremental sin una ruta de migración definida.
Si solo quieres que Room recurra a la recreación destructiva en determinadas situaciones, existen algunas alternativas para fallbackToDestructiveMigration()
:
- Si las versiones específicas de tu historial de esquema causan errores que no puedes resolver con las rutas de migración, usa
fallbackToDestructiveMigrationFrom()
en su lugar. Ese método indica que quieres recurrir a la recreación destructiva solo cuando migras desde versiones específicas. - Si deseas que Room recurra a la recreación destructiva solo cuando migres de una versión de base de datos posterior a una anterior, usa
fallbackToDestructiveMigrationOnDowngrade()
en su lugar.
Cómo controlar el valor predeterminado de la columna al actualizar a Room 2.2.0
En Room 2.2.0 y versiones posteriores, puedes definir un valor predeterminado para una columna con la anotación @ColumnInfo(defaultValue = "...")
.
En las versiones anteriores a la 2.2.0, la única manera de definir un valor predeterminado para una columna es definirlo directamente en una instrucción de SQL ejecutada, lo cual crea un valor predeterminado que Room no conoce. Esto significa que, si una versión de Room anterior a la 2.2.0 crea una base de datos, cuando actualices la app a la versión 2.2.0, es posible que debas proporcionar una ruta de migración especial para los valores predeterminados existentes que definiste sin usar las APIs de Room.
Por ejemplo, supongamos que la versión 1 de una base de datos define una entidad 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; }
Supongamos también que la versión 2 de la misma base de datos agrega una columna NOT NULL
nueva y define una ruta de migración de la versión 1 a la versión 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 ''"); } };
Esto genera una discrepancia en la tabla subyacente entre las actualizaciones y las instalaciones nuevas de la app. Como el valor predeterminado de la columna tag
solo se declara en la ruta de migración de la versión 1 a la 2, los usuarios que instalaron la app a partir de la versión 2 no tienen el valor predeterminado de tag
en el esquema de la base de datos.
En las versiones de Room anteriores a la 2.2.0, esta discrepancia no genera problemas. Sin embargo, si luego se actualiza la app a Room 2.2.0 o una versión posterior, y cambia la clase de entidad Song
para incluir un valor predeterminado para tag
con la anotación @ColumnInfo
, entonces, Room podrá ver esa discrepancia, lo que generará validaciones de esquema fallidas.
Para asegurarte de que no haya discrepancias en el esquema de la base para todos los usuarios cuando se declaren los valores predeterminados de la columna en tus rutas de migración anteriores, haz lo siguiente la primera vez que actualices tu app para usar Room 2.2.0 o una versión posterior:
- Declara valores predeterminados de columna en sus respectivas clases de entidad mediante la anotación
@ColumnInfo
. - Aumenta el número de versión de la base de datos en uno.
- Define una ruta de migración a la nueva versión, que implemente la estrategia para soltar y recrear, para agregar los valores predeterminados necesarios a las columnas existentes.
En el siguiente ejemplo, se muestra este proceso:
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"); } };