Migração de banco de dados de sala se apenas uma nova tabela for adicionada

98

Não vamos supor que tenho um banco de dados simples do Room:

@Database(entities = {User.class}, version = 1)
abstract class AppDatabase extends RoomDatabase {
    public abstract Dao getDao();
}

Agora, estou adicionando uma nova entidade: Pete aumentando a versão para 2:

@Database(entities = {User.class, Pet.class}, version = 2)
abstract class AppDatabase extends RoomDatabase {
    public abstract Dao getDao();
}

Claro, o Room lança uma exceção: java.lang.IllegalStateException: A migration from 1 to 2 is necessary.

Supondo que eu não mudei de Userclasse (portanto, todos os dados estão seguros), tenho que fornecer uma migração que apenas cria uma nova tabela. Então, estou procurando por classes geradas pelo Room, procurando por consulta gerada para criar minha nova tabela, copiando e colando na migração:

final Migration MIGRATION_1_2 =
        new Migration(1, 2) {
            @Override
            public void migrate(@NonNull final SupportSQLiteDatabase database) {
                database.execSQL("CREATE TABLE IF NOT EXISTS `Pet` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))");
            }
        };

No entanto, acho inconveniente fazê-lo manualmente. Existe uma maneira de dizer ao Room: não estou tocando em nenhuma tabela existente, então os dados estão seguros. Por favor, crie a migração para mim?

Piotr Aleksander Chmielowski
fonte
Você encontrou uma solução para isso?
Mikkel Larsen
3
Tive o mesmo problema e resolvi da mesma forma que você, e também não encontrei uma solução. Ainda bem que não estou sozinho. :)
Mikkel Larsen
3
O mesmo aqui. Acho muito inconveniente que a sala seja capaz de gerar a consulta de criação dentro do database_impl, mas não pode simplesmente criar a tabela, uma vez que a migração começa ....
JacksOnF1re
1
Eu daria muito por esse recurso ... Também seria bom misturar migrações e o mecanismo de fallback ...
Appyx
3
Não tenho certeza se isso seria útil, mas a Room tem a opção de exportar o esquema do banco de dados para um arquivo JSON. developer.android.com/training/data-storage/room/… Obviamente, isso ainda significaria adicionar manualmente o script de migração, mas você não precisaria passar pelas classes geradas automaticamente para obter sua instrução SQL.
James Lendrem,

Respostas:

75

Quarto se não tiver um bom sistema de migração, pelo menos não até 2.1.0-alpha03.

Portanto, até que tenhamos um sistema de migração melhor, existem algumas soluções alternativas para facilitar as migrações na sala.

Como não existe um método como @Database(createNewTables = true) ou MigrationSystem.createTable(User::class), que deveria haver um ou outro, a única maneira possível é executando

CREATE TABLE IF NOT EXISTS `User` (`id` INTEGER, PRIMARY KEY(`id`))

dentro do seu migratemétodo.

val MIGRATION_1_2 = object : Migration(1, 2){
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `User` (`id` INTEGER, PRIMARY KEY(`id`))")
    }
}

Para obter o script SQL acima , você tem 4 maneiras

1. Escreva sozinho

Basicamente, você deve escrever o script acima que irá corresponder ao script gerado pela Room. Esse caminho é possível, não viável. (Considere que você tem 50 campos)

2. Esquema de exportação

Se você incluir exportSchema = trueem sua @Databaseanotação, a Room irá gerar o esquema do banco de dados dentro de / schemas da pasta do seu projeto. O uso é

@Database(entities = [User::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase {
   //...
}

Certifique-se de incluir as linhas abaixo no build.grademódulo do seu aplicativo

kapt {
    arguments {
        arg("room.schemaLocation", "$projectDir/schemas".toString())
    }
} 

Ao executar ou construir o projeto, você obterá um arquivo JSON 2.json, que contém todas as consultas no banco de dados do Room.

  "formatVersion": 1,
  "database": {
    "version": 2,
    "identityHash": "325bd539353db508c5248423a1c88c03",
    "entities": [
      {
        "tableName": "User",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },

Portanto, você pode incluir o acima createSqldentro de seu migratemétodo.

3. Obtenha a consulta de AppDatabase_Impl

Se você não quiser exportar o esquema, você ainda pode obter a consulta executando ou construindo o projeto que irá gerar o AppDatabase_Impl.javaarquivo. e dentro do arquivo especificado você pode ter.

@Override
public void createAllTables(SupportSQLiteDatabase _db) {
  _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`id` INTEGER, PRIMARY KEY(`id`))");

Dentro do createAllTablesmétodo, haverá os scripts de criação de todas as entidades. Você pode obtê-lo e incluí-lo em seu migratemétodo.

4. Processamento de anotações.

Como você pode imaginar, o Room gera todos os itens mencionados acima schema, e AppDatabase_Implarquivos dentro do tempo de compilação e com processamento de anotação que você adiciona com

kapt "androidx.room:room-compiler:$room_version"

Isso significa que você também pode fazer o mesmo e criar sua própria biblioteca de processamento de anotações que gera todas as consultas de criação necessárias para você.

A ideia é fazer uma biblioteca de processamento de anotações para anotações de Room @Entitye @Database. Pegue uma classe com anotações, @Entitypor exemplo. Estas são as etapas que você terá que seguir

  1. Faça um novo StringBuildere anexe "CRIAR TABELA SE NÃO EXISTIR"
  2. Obtenha o nome da tabela de class.simplenameou por tableNamecampo de @Entity. Adicione ao seuStringBuilder
  3. Então, para cada campo de sua classe, crie colunas de SQL. Pegue o nome, tipo e nulidade do campo pelo próprio campo ou por @ColumnInfoanotação. Para cada campo, você deve adicionar o id INTEGER NOT NULLestilo de uma coluna ao seu StringBuilder.
  4. Adicionar chaves primárias por @PrimaryKey
  5. Adicione ForeignKeye Indicesse existir.
  6. Após terminar, converta-o em string e salve-o em alguma nova classe que deseja usar. Por exemplo, salve como abaixo
public final class UserSqlUtils {
  public String createTable = "CREATE TABLE IF NOT EXISTS User (id INTEGER, PRIMARY KEY(id))";
}

Então, você pode usá-lo como

val MIGRATION_1_2 = object : Migration(1, 2){
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(UserSqlUtils().createTable)
    }
}

Eu fiz uma biblioteca para mim que você pode conferir e até mesmo usar em seu projeto. Observe que a biblioteca que fiz não está cheia e apenas atende aos meus requisitos para a criação de tabelas.

RoomExtension para uma melhor migração

Aplicativo que usa RoomExtension

Espero que tenha sido útil.

ATUALIZAR

No momento em que escrevi esta resposta, a versão da sala era 2.1.0-alpha03e quando enviei um e-mail aos desenvolvedores, recebi uma resposta de

Espera-se que haja um melhor sistema de migração em 2.2.0

Infelizmente, ainda não temos um sistema de migração melhor.

musooff
fonte
3
Você pode apontar onde leu que a Room 2.2.x terá uma migração melhor? Não consigo encontrar nada que faça essa afirmação e, uma vez que estamos trabalhando no beta 2.1.0, o que está em 2.2.0 parece ser desconhecido no momento.
jkane001
1
@ jkane001 Mandei um e-mail para um dos desenvolvedores da sala e recebi uma resposta. Não há tal aviso público em relação ao 2.2.x (ainda?)
musooff
4
Atualmente na versão 2.2.2 e ainda sem uma migração melhor :( No entanto, esta é uma resposta excelente e me economizou muito trabalho, então +1 para isso.
smitty1
@androiddeveloper Todos exceto # 4, que é Processamento de Anotação
musooff
1
@musooff Na verdade, acho que não há problema em adicionar a criação da tabela. É a maneira mais segura de copiar o código da função "createAllTables".
desenvolvedor Android
3

A Room não oferece suporte à criação automática de tabelas sem perda de dados.

É obrigatório escrever a migração. Caso contrário, ele apagará todos os dados e criará a nova estrutura da tabela.

Viswanath Kumar Sandu
fonte
0

Você pode fazer desta forma-

@Database(entities = {User.class, Pet.class}, version = 2)

abstract class AppDatabase extends RoomDatabase {
public abstract Dao getDao();
public abstract Dao getPetDao();
}

O restante será o mesmo que você mencionou acima-

 db = Room.databaseBuilder(this, AppDatabase::class.java, "your_db")
        .addMigrations(MIGRATION_1_2).build()

Referência - para mais

Sujeet Kumar
fonte
0

Você pode adicionar o seguinte comando gradle ao defaultConfig em seu app.gradle:

javaCompileOptions {
        annotationProcessorOptions {
            arguments = ["room.schemaLocation":
                                 "$projectDir/schemas".toString()]
        }
    }

Quando você o executa, ele compila uma lista de nomes de tabela com suas instruções CREATE TABLE relevantes, da qual você pode simplesmente copiar e colar em seus objetos de migração. Você pode ter que alterar os nomes das tabelas.

Por exemplo, isso é do meu esquema gerado:

"tableName": "assets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `base` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`asset_id`))"

E então eu copio e colo a instrução createSql e altero '$ {TABLE_NAME}' para 'assets' o nome da tabela, e voila as instruções de criação de Room gerados automaticamente.

Larry Stent
fonte
-1

Nesse caso, você não precisa fazer uma migração, você pode chamar .fallbackToDestructiveMigration () ao criar uma instância do banco de dados.

Exemplo:

    instance = Room.databaseBuilder(context, AppDatabase.class, "database name").fallbackToDestructiveMigration().build();

E não se esqueça de mudar a versão do banco de dados.

rudicjovan
fonte
Esta solução removerá todos os meus dados das tabelas existentes.
Piotr Aleksander Chmielowski
Infelizmente sim. "Você pode chamar este método para alterar esse comportamento para recriar o banco de dados em vez de travar. Observe que isso excluirá todos os dados nas tabelas do banco de dados gerenciadas pela Room."
rudicjovan
-2

Talvez neste caso (se você apenas criou uma nova tabela sem alterar as outras) você possa fazer isso sem criar nenhuma migração?

user1730694
fonte
1
Não, neste caso, a sala gera logs: java.lang.IllegalStateException: É necessária uma migração de {old_version} para {new_version}
Max Makeichik