À 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 anotação
@AutoMigration
na
propriedade autoMigrations
em @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 { ... }
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ê
implemente uma
AutoMigrationSpec
.
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 estática 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:
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 { } ... }
Caso o app precise executar mais tarefas depois de concluir a migração automática, é possível
implementar
onPostMigrate()
.
Se você implementar esse método na AutoMigrationSpec
, o Room vai chamar o método após
a 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. Nesses casos, é necessário definir manualmente
um caminho de migração, implementando uma
classe Migration
.
Uma classe Migration
define explicitamente um caminho de migração entre uma
startVersion
e uma endVersion
, substituindo o
método
Migration.migrate()
. Adicione as classes Migration
ao builder do banco de dados usando
o
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();
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 room-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 pode exportar 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. Armazenamento esses arquivos no seu sistema de controle de versões, para que o Room crie versões anteriores o banco de dados para fins de teste e para permitir a geração de migração automática.
Definir o local do esquema usando o plug-in do Gradle para Room
Se você estiver usando a versão 2.6.0 ou mais recente do Room, será possível aplicar as
Plug-in do Gradle para Room e use as
room
para especificar o diretório do esquema.
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Se o esquema do banco de dados for diferente com base na variante, variação ou build
específico, é preciso especificar locais diferentes usando o método schemaDirectory()
várias vezes, cada uma com um variantMatchName
como a primeira
. Cada configuração pode corresponder a uma ou mais variantes com base
comparação com o nome da variante.
Verifique se todas elas estão completas e abrangem todas as variantes. Você também pode incluir um
schemaDirectory()
sem um variantMatchName
para processar variantes sem correspondência
por qualquer uma das outras configurações. Por exemplo, em um app com dois build
variações demo
e full
e dois tipos de build debug
e release
, a
estas são as configurações 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")
}
Definir o local do esquema usando a opção do processador de anotações
Se você estiver usando a versão 2.5.2 ou anterior do Room ou se não estiver usando a
Plug-in do Gradle para Room, defina o local do esquema usando o room.schemaLocation
.
opção do processador de anotações.
Os arquivos nesse diretório são usados como entradas e saídas para algumas tarefas do Gradle.
Para precisão e desempenho de builds incrementais e armazenados em cache, você precisa usar
do Gradle.
CommandLineArgumentProvider
para informar o Gradle sobre esse diretório.
Primeiro, copie a classe RoomSchemaArgProvider
mostrada abaixo no arquivo
Arquivo de build do Gradle. O método asArguments()
na classe de amostra 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() {
// 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}")
}
}
Em seguida, configure as opções de compilação para usar o RoomSchemaArgProvider
com o
do 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"))
)
}
}
}
}
Testar uma única migração
Antes de testar suas migrações, adicione o
artefato Maven androidx.room:room-testing
do Room às dependências de
teste e adicione o local do esquema exportado como uma pasta de recursos:
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") }
O pacote de testes fornece uma classe
MigrationTestHelper
,
que pode ler arquivos de esquema exportados. O pacote também implementa a interface
JUnit4
TestRule
para gerenciar os bancos de dados criados.
O exemplo abaixo demonstra um teste para uma única migração.
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. } }
Testar todas as migrações
Embora seja possível testar uma única migração incremental, recomendamos incluir um teste que abranja todas as migrações definidas para o banco de dados do 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.
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}; }
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 uma
IllegalStateException
. Se
for aceitável perder dados quando um caminho de migração estiver ausente, chame
o método do builder
fallbackToDestructiveMigration()
ao criar o banco de dados:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Esse método instrui o Room a 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.
Se você quiser que o Room volte a usar a recriação destrutiva em algumas situações,
confira estas 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
fallbackToDestructiveMigrationFrom()
. Esse método 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
fallbackToDestructiveMigrationOnDowngrade()
.
Processar os valores padrão da coluna ao fazer upgrade para o Room 2.2.0
No Room 2.2.0 e versões mais recentes, é possível definir um valor padrão para uma coluna usando
a anotação
@ColumnInfo(defaultValue = "...")
.
Nas versões anteriores à 2.2.0, a única maneira de definir um valor padrão para uma
coluna é fazer isso diretamente em uma instrução SQL executada, que cria um
valor padrão desconhecido pelo Room. Isso significa que, se um banco de dados tiver sido
criado originalmente por uma versão do Room anterior à versão 2.2.0, o upgrade do app para
usar o Room 2.2.0 talvez precise que você disponibilize um caminho de migração especial para
valores padrão definidos sem usar as APIs dele.
Por exemplo, suponha que a versão 1 de um banco de dados defina uma entidade 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; }
Suponha também que a versão 2 do mesmo banco de dados adicione uma nova coluna NOT NULL
e defina um caminho de migração da versão 1 para a versão 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 ''"); } };
Isso causa uma discrepância na tabela entre as atualizações e as novas
instalações do app. Como o valor padrão da coluna tag
só é
declarado no caminho de migração da versão 1 para a versão 2, os usuários que
instalaram o app a partir da versão 2 não têm o valor padrão de tag
no esquema do banco de dados.
Nas versões do Room anteriores à 2.2.0, essa discrepância é inofensiva. No entanto, se
o app fizer upgrade para o Room 2.2.0 ou versões mais recentes e mudar a classe de entidade Song
de modo a
incluir um valor padrão para a tag
usando a
anotação @ColumnInfo
, o Room poderá
ver essa discrepância. Isso gera falhas nas validações de
esquema.
Para garantir que o esquema do banco de dados seja consistente com todos os usuários quando os valores padrão da coluna são declarados nos caminhos de migração anteriores, faça o seguinte na primeira vez que fizer upgrade do app para usar o Room 2.2.0 ou versões mais recentes:
- Declare os valores padrão da coluna nas respectivas classes de entidade usando a
anotação
@ColumnInfo
. - Aumente o número da versão do banco de dados em um.
- Defina um caminho de migração para a nova versão que implementa a estratégia de descarte e recriação (link em inglês) e adicione os valores padrão necessários às colunas atuais.
O exemplo a seguir demonstra esse processo.
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"); } };