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
@AutoMigration anotación a la autoMigrations propiedad en
@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 }
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 deberás proporcionar una
AutoMigrationSpec implementación. 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 que implemente AutoMigrationSpec en tu clase RoomDatabase y anótala con una o más de las siguientes opciones:
Si deseas usar la implementación de AutoMigrationSpec para una migración automatizada, configura la propiedad spec en la anotación @AutoMigration correspondiente:
@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 tu app necesita hacer más trabajo después de que se complete la migración automática, puedes implementar onPostMigrate. Si implementas esta función en tu AutoMigrationSpec, Room la 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 podrá determinar el modo en que se debe realizar esta división. En casos como estos, debes definir manualmente una
ruta de migración mediante la implementación de una Migration clase.
Una clase Migration define de forma explícita una ruta de migración entre una startVersion
y una endVersion anulando la función migrate. Agrega
tus Migration clases al compilador de bases de datos mediante la
addMigrations función:
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()
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 room3-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 exporta 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. Debes almacenar estos archivos en tu sistema de control de versión para poder volver a crear versiones anteriores de la base de datos para realizar pruebas y admitir la generación de migraciones automáticas.
Cómo establecer la ubicación del esquema con el complemento de Gradle de Room
Para especificar el directorio del esquema, aplica el complemento de Gradle de Room y usa la
room3 extensión.
Groovy
plugins {
id 'androidx.room3'
}
room3 {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room3")
}
room3 {
schemaDirectory("$projectDir/schemas")
}
Si el esquema de tu base de datos difiere según la variante, el tipo de producto o el tipo de compilación, debes especificar diferentes ubicaciones usando la configuración schemaDirectory varias veces, cada una con un variantMatchName como primer argumento. Cada configuración puede coincidir con una o más variantes según la comparación simple con el nombre de la variante.
Asegúrate de que sean exhaustivas y abarquen todas las variantes. También puedes incluir un schemaDirectory() sin un variantMatchName para controlar las variantes que no coinciden con ninguna de las otras configuraciones. Por ejemplo, en una app con dos tipos de compilación demo y full, y dos tipos de compilación debug y release, las siguientes son configuraciones válidas:
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")
}
Cómo establecer la ubicación del esquema con la opción del procesador de anotaciones
Si no usas el complemento de Gradle de Room, establece la ubicación del esquema con la opción del procesador de anotaciones room.schemaLocation.
Gradle usa archivos en este directorio como entradas y salidas para algunas tareas de Gradle.
Para la corrección y el rendimiento de las compilaciones incrementales y almacenadas en caché, debes usar
Gradle's CommandLineArgumentProvider para informar a Gradle sobre
este directorio.
Primero, copia la siguiente clase RoomSchemaArgProvider en el archivo de compilación de Gradle de tu módulo. La función 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() {
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}")
}
}
Luego, configura las opciones de compilación para usar RoomSchemaArgProvider con el directorio del esquema especificado:
Groovy
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
Kotlin
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
Cómo probar una sola migración
Para poder probar las migraciones, debes agregar el artefacto androidx.room3:room3-testing a tus dependencias de prueba y la ubicación del esquema exportado como un directorio de elementos:
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") }
El paquete de prueba proporciona una clase MigrationTestHelper, que puede leer
archivos de esquema exportados. El paquete también implementa la interfaz JUnit4
TestRule para administrar las bases de datos creadas.
En el siguiente ejemplo, se muestra una prueba para una sola migración:
@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() } }
Cómo probar todas las migraciones
Aunque es posible probar una migración incremental única, 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:
@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() } }
Cómo resolver de manera óptima las rutas de migración que faltan
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 un IllegalStateException. Si
no es un problema perder datos existentes cuando falta una ruta de migración, llama
a la función de compilador fallbackToDestructiveMigration cuando crees
la base de datos:
Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name") .fallbackToDestructiveMigration() .build()
Esta función configura Room para 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.
Para recurrir a la recreación destructiva solo en ciertas situaciones, usa una de las siguientes 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
fallbackToDestructiveMigrationFromen 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
fallbackToDestructiveMigrationOnDowngradeen su lugar.