Перенесите базу данных комнат

По мере добавления и изменения функций в вашем приложении вам необходимо модифицировать классы сущностей Room и базовые таблицы базы данных, чтобы отразить эти изменения. Важно сохранить данные пользователей, которые уже находятся в базе данных на устройстве, когда обновление приложения изменяет схему базы данных.

Room поддерживает как автоматический, так и ручной режимы инкрементальной миграции. Автоматическая миграция подходит для большинства базовых изменений схемы, но для более сложных изменений может потребоваться вручную определить пути миграции.

Автоматизированные миграции

Для объявления автоматической миграции между двумя версиями базы данных добавьте аннотацию @AutoMigration к свойству ` autoMigrations в @Database :

Котлин

// 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 {
  ...
}

Спецификации автоматической миграции

Если Room обнаруживает неоднозначные изменения схемы и не может сгенерировать план миграции без дополнительных данных, он выдает ошибку компиляции и просит вас реализовать AutoMigrationSpec . Чаще всего это происходит, когда миграция включает в себя один из следующих вариантов:

  • Удаление или переименование таблицы.
  • Удаление или переименование столбца.

Вы можете использовать AutoMigrationSpec , чтобы предоставить Room дополнительную информацию, необходимую для корректного создания путей миграции. Определите статический класс, реализующий интерфейс AutoMigrationSpec в вашем классе RoomDatabase и аннотируйте его одним или несколькими из следующих способов:

Чтобы использовать реализацию AutoMigrationSpec для автоматической миграции, установите свойство spec в соответствующей аннотации @AutoMigration :

Котлин

@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 { }
  ...
}

Если вашему приложению необходимо выполнить дополнительную работу после завершения автоматической миграции, вы можете реализовать onPostMigrate() . Если вы реализуете этот метод в своем AutoMigrationSpec , Room вызовет его после завершения автоматической миграции.

Ручная миграция

In cases where a migration involves complex schema changes, Room might not be able to generate an appropriate migration path automatically. For example, if you decide to split the data in a table into two tables, Room can't tell how to perform this split. In cases like these, you must manually define a migration path by implementing a Migration class.

Класс Migration явно определяет путь миграции между startVersion и endVersion , переопределяя метод Migration.migrate() . Добавьте свои классы Migration в конструктор базы данных, используя метод addMigrations() :

Котлин

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();

При определении путей миграции вы можете использовать автоматическую миграцию для одних версий и ручную миграцию для других. Если вы определите как автоматическую, так и ручную миграцию для одной и той же версии, Room будет использовать ручную миграцию.

Тестовые миграции

Миграции часто бывают сложными, и неправильно определенная миграция может привести к сбою приложения. Для сохранения стабильности приложения тестируйте свои миграции. Room предоставляет артефакт Maven room-testing , который помогает в процессе тестирования как автоматических, так и ручных миграций. Для работы этого артефакта необходимо сначала экспортировать схему вашей базы данных.

Экспортные схемы

Room может экспортировать информацию о схеме вашей базы данных в файл JSON во время компиляции. Экспортированные файлы JSON представляют собой историю схемы вашей базы данных. Сохраните эти файлы в вашей системе контроля версий, чтобы Room мог создавать более старые версии базы данных для целей тестирования и для автоматической генерации миграции.

Укажите расположение схемы с помощью плагина Room Gradle.

Если вы используете Room версии 2.6.0 или выше, вы можете установить плагин Room Gradle и использовать расширение room для указания каталога схемы.

Классный

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Котлин

plugins {
  id("androidx.room")
}

room {
  schemaDirectory("$projectDir/schemas")
}

Если схема вашей базы данных различается в зависимости от варианта, конфигурации или типа сборки, необходимо указать разные расположения, используя конфигурацию schemaDirectory() несколько раз, каждый раз с variantMatchName в качестве первого аргумента. Каждая конфигурация может сопоставлять один или несколько вариантов на основе простого сравнения с именем варианта.

Убедитесь, что эти параметры являются исчерпывающими и охватывают все варианты. Вы также можете включить schemaDirectory() без variantMatchName для обработки вариантов, не соответствующих ни одной из других конфигураций. Например, в приложении с двумя вариантами сборки ( demo и full и двумя типами сборки debug и release ) допустимыми конфигурациями являются следующие:

Классный

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"
}

Котлин

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")
}

Укажите расположение схемы с помощью параметра обработчика аннотаций.

Если вы используете версию Room 2.5.2 или ниже, или если вы не используете плагин Room Gradle, укажите расположение схемы с помощью параметра обработчика аннотаций room.schemaLocation .

Файлы в этом каталоге используются в качестве входных и выходных данных для некоторых задач Gradle. Для корректной работы и повышения производительности инкрементальной и кэшированной сборки необходимо использовать CommandLineArgumentProvider класса Gradle, чтобы сообщить Gradle об этом каталоге.

Сначала скопируйте показанный ниже класс RoomSchemaArgProvider в файл сборки Gradle вашего модуля. Метод asArguments() в примере класса передает room.schemaLocation=${schemaDir.path} в KSP . Если вы используете KAPT и javac , измените это значение на -Aroom.schemaLocation=${schemaDir.path} .

Классный

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()]
  }
}

Котлин

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}")
  }
}

Затем настройте параметры компиляции для использования RoomSchemaArgProvider с указанным каталогом схемы:

Классный

// 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"))
        )
      }
    }
  }
}

Котлин

// 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"))
        )
      }
    }
  }
}

Протестируйте одну миграцию.

Прежде чем тестировать миграции, добавьте артефакт Maven androidx.room:room-testing из Room в зависимости для тестирования и укажите расположение экспортированной схемы в качестве папки ресурсов:

build.gradle

Классный

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
    androidTestImplementation "androidx.room:room-testing:2.8.4"
}

Котлин

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

dependencies {
    ...
    testImplementation("androidx.room:room-testing:2.8.4")
}

Пакет тестирования предоставляет класс MigrationTestHelper , который может считывать экспортированные файлы схем. Пакет также реализует интерфейс JUnit4 TestRule , поэтому он может управлять созданными базами данных.

Следующий пример демонстрирует проверку одной миграции:

Котлин

@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.
    }
}

Протестируйте все миграции.

Хотя можно протестировать одну инкрементальную миграцию, мы рекомендуем включить тест, охватывающий все миграции, определенные для базы данных вашего приложения. Это поможет гарантировать отсутствие расхождений между недавно созданным экземпляром базы данных и более старым экземпляром, который следовал определенным путям миграции.

Следующий пример демонстрирует проверку всех определенных миграций:

Котлин

@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};
}

Корректно обрабатывайте отсутствующие пути миграции.

Если Room не может найти путь миграции для обновления существующей базы данных на устройстве до текущей версии, возникает исключение IllegalStateException . Если допустима потеря существующих данных при отсутствии пути миграции, вызовите метод построителя fallbackToDestructiveMigration() при создании базы данных:

Котлин

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .fallbackToDestructiveMigration()
        .build()

Java

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .fallbackToDestructiveMigration()
        .build();

Этот метод указывает Room на необходимость деструктивного пересоздания таблиц в базе данных вашего приложения, когда требуется выполнить инкрементальную миграцию и отсутствует определенный путь миграции.

Если вам нужно, чтобы Room возвращался к деструктивному восстановлению только в определенных ситуациях, существует несколько альтернатив функции fallbackToDestructiveMigration() :

  • Если определенные версии истории вашей схемы вызывают ошибки, которые вы не можете устранить с помощью путей миграции, используйте вместо этого fallbackToDestructiveMigrationFrom() . Этот метод указывает, что вы хотите, чтобы Room возвращался к деструктивному воссозданию только при миграции с определенных версий.
  • Если вы хотите, чтобы Room возвращался к деструктивному созданию резервной копии только при миграции с более новой версии базы данных на более старую, используйте вместо этого fallbackToDestructiveMigrationOnDowngrade() .

Обработка значений по умолчанию в столбцах при обновлении до версии Room 2.2.0

В Room 2.2.0 и выше можно задать значение по умолчанию для столбца с помощью аннотации @ColumnInfo(defaultValue = "...") . В версиях ниже 2.2.0 единственный способ задать значение по умолчанию для столбца — это определить его непосредственно в выполняемом SQL-запросе, который создает значение по умолчанию, о котором Room не знает. Это означает, что если база данных изначально создана в версии Room ниже 2.2.0, обновление вашего приложения до Room 2.2.0 может потребовать предоставления специального пути миграции для существующих значений по умолчанию, которые вы определили без использования API Room.

Например, предположим, что в версии 1 базы данных определена сущность Song :

Котлин

// 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;
}

Предположим также, что во второй версии той же базы данных добавлен новый столбец NOT NULL и определен путь миграции с версии 1 на версию 2:

Котлин

// 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 ''");
    }
};

Это приводит к несоответствию в базовой таблице между обновлениями и новыми установками приложения. Поскольку значение по умолчанию для столбца tag объявляется только в процессе миграции с версии 1 на версию 2, у пользователей, устанавливающих приложение начиная с версии 2, значение по умолчанию для tag отсутствует в схеме базы данных.

В версиях Room ниже 2.2.0 это несоответствие безвредно. Однако, если приложение позже обновится до Room 2.2.0 или выше и изменит класс сущности Song , добавив значение по умолчанию для tag с помощью аннотации @ColumnInfo , Room может обнаружить это несоответствие. Это приведет к сбоям проверки схемы.

Чтобы обеспечить согласованность схемы базы данных для всех пользователей при объявлении значений по умолчанию для столбцов в предыдущих путях миграции, выполните следующие действия при первом обновлении приложения до версии Room 2.2.0 или выше:

  1. Объявите значения по умолчанию для столбцов в соответствующих классах сущностей, используя аннотацию @ColumnInfo .
  2. Увеличьте номер версии базы данных на 1.
  3. Определите путь миграции на новую версию, реализующую стратегию удаления и повторного создания для добавления необходимых значений по умолчанию к существующим столбцам.

Следующий пример демонстрирует этот процесс:

Котлин

// 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");
    }
};