À medida que você adiciona e muda recursos no app, é necessário modificar as classes de entidade do Room e as tabelas de banco de dados para refletir essas mudanças. É importante preservar os dados do usuário que já estão no banco de dados do dispositivo quando uma atualização de app muda o esquema dele.
O Room oferece suporte a opções automatizadas e manuais para a migração incremental. As migrações automáticas funcionam para a maioria das mudanças básicas de esquema, mas pode ser necessário definir manualmente os caminhos de migração em mudanças mais complexas.
Migrações automatizadas
Para declarar uma migração automática entre duas versões do banco de dados, adicione uma
@AutoMigration anotação na autoMigrations propriedade em
@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 }
Especificações da migração automática
Se o Room detectar mudanças de esquema ambíguas e não for possível gerar um plano de migração
sem receber outras entradas, ele vai gerar um erro durante a compilação e solicitar que você forneça uma
AutoMigrationSpec implementação. Isso normalmente ocorre quando uma migração envolve uma destas ações:
- Excluir ou renomear uma tabela.
- Excluir ou renomear uma coluna.
Você pode usar a AutoMigrationSpec para fornecer ao Room as outras informações necessárias para gerar caminhos de migração corretamente. Defina uma classe que implemente a AutoMigrationSpec na classe RoomDatabase e inclua uma ou mais destas anotações:
Para usar a implementação de AutoMigrationSpec em uma migração automática, defina a propriedade spec na anotação @AutoMigration correspondente:
@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
Se o app precisar fazer mais trabalho após a conclusão da migração automática, você
pode implementar onPostMigrate. Se você implementar essa função na AutoMigrationSpec, o Room vai chamá-la após a conclusão da migração automática.
Migrações manuais
Nos casos em que uma migração envolve mudanças de esquema complexas, é possível que o Room não consiga gerar um caminho de migração adequado de forma automática. Por exemplo, se você for dividir os dados de uma tabela em duas, o Room não conseguirá determinar como realizar essa divisão. Nessas situações, é necessário definir manualmente um
caminho de migração implementando uma Migration classe.
Uma classe Migration define explicitamente um caminho de migração entre um startVersion
e um endVersion, substituindo a função migrate. Adicione
suas Migration classes ao builder do banco de dados usando a
addMigrations função:
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()
Ao definir os caminhos de migração, é possível usar migrações automáticas para algumas versões e migrações manuais em outras. Caso você defina uma migração automática e uma manual para a mesma versão, o Room usa a migração manual.
Testar migrações
As migrações costumam ser complexas, e uma migração definida de forma incorreta pode causar falhas no seu app. Para preservar a estabilidade, é preciso testar suas migrações. O Room fornece um artefato Maven room3-testing para auxiliar no processo de teste de migrações automáticas e manuais. No entanto, para que esse artefato funcione, é necessário exportar o esquema do banco de dados.
Exportar esquemas
O Room exporta as informações do esquema do banco de dados para um arquivo JSON durante a compilação. Os arquivos JSON exportados representam o histórico do esquema do banco de dados. Armazene esses arquivos no sistema de controle de versão para recriar versões anteriores do banco de dados para testes e oferecer suporte à geração de migração automática.
Definir o local do esquema usando o plug-in do Gradle para Room
Para especificar o diretório do esquema, aplique o plug-in do Gradle para Room e use a
room3 extensão.
Groovy
plugins {
id 'androidx.room3'
}
room3 {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room3")
}
room3 {
schemaDirectory("$projectDir/schemas")
}
Se o esquema do banco de dados for diferente com base na variante, na variação ou no tipo de build, especifique locais diferentes usando a configuração schemaDirectory várias vezes, cada uma com um variantMatchName como o primeiro argumento. Cada configuração pode corresponder a uma ou mais variantes com base em uma comparação simples com o nome da variante.
Verifique se elas são exaustivas e abrangem todas as variantes. Você também pode incluir um schemaDirectory() sem um variantMatchName para processar variantes que não correspondam a nenhuma das outras configurações. Por exemplo, em um app com duas variações de build demo e full e dois tipos de build debug e release, as seguintes configurações são 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")
}
Definir o local do esquema usando a opção do processador de anotações
Se você não estiver usando o plug-in do Gradle para Room, defina o local do esquema usando a opção do processador de anotações room.schemaLocation.
O Gradle usa arquivos nesse diretório como entradas e saídas para algumas tarefas do Gradle.
Para a correção e a performance de builds incrementais e em cache, use
o CommandLineArgumentProvider do Gradle para informar o Gradle sobre
esse diretório.
Primeiro, copie a classe RoomSchemaArgProvider a seguir para o arquivo de build do Gradle do módulo. A função asArguments na classe de exemplo transmite room.schemaLocation=${schemaDir.path} para KSP. Se você estiver usando KAPT e javac, mude esse valor para -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}")
}
}
Em seguida, configure as opções de compilação para usar o RoomSchemaArgProvider com o diretório de esquema especificado:
Groovy
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
Kotlin
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
Testar uma única migração
Antes de testar suas migrações, adicione o artefato androidx.room3:room3-testing às dependências de teste e adicione o local do esquema exportado como um diretório de recursos:
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") }
O pacote de testes fornece uma MigrationTestHelper classe, que pode ler
arquivos de esquema exportados. O pacote também implementa a interface do JUnit4
TestRule para gerenciar os bancos de dados criados.
O exemplo a seguir demonstra um teste para uma única migração.
@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() } }
Testar todas as migrações
Embora seja possível testar uma única migração incremental, é recomendável incluir um teste que abranja todas as migrações definidas para o banco de dados do seu app. Isso garante que não haja discrepâncias entre uma instância de banco de dados recém-criada e outra antiga que seguiu os caminhos de migração definidos.
O exemplo a seguir demonstra um teste para todas as migrações 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() } }
Processar os caminhos de migração ausentes corretamente
Se o Room não encontrar um caminho de migração para fazer upgrade de um banco de dados em um
dispositivo para a versão atual, vai ocorrer um IllegalStateException. Se
for aceitável perder dados quando um caminho de migração estiver ausente, chame
a função do builder fallbackToDestructiveMigration ao criar
o banco de dados:
Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name") .fallbackToDestructiveMigration() .build()
Essa função configura o Room para recriar de forma destrutiva as tabelas no banco de dados do app quando ele precisar executar uma migração incremental em que não há um caminho de migração definido.
Para voltar à recriação destrutiva apenas em determinadas situações, use uma das seguintes alternativas para fallbackToDestructiveMigration:
- Se ocorrerem erros em versões específicas do histórico de esquema que não possam ser resolvidos
com caminhos de migração, use
fallbackToDestructiveMigrationFromem vez disso. Essa função indica que você quer que o Room use a recriação destrutiva apenas ao migrar de versões específicas. - Se quiser que o Room use a recriação destrutiva apenas ao migrar
de uma versão de banco de dados mais recente para uma anterior, use
fallbackToDestructiveMigrationOnDowngradeem vez disso.