По мере добавления и изменения функций в вашем приложении вам необходимо модифицировать классы сущностей Room и базовые таблицы базы данных, чтобы отразить эти изменения. Важно сохранить данные пользователей, которые уже находятся в базе данных на устройстве, когда обновление приложения изменяет схему базы данных.
Room поддерживает как автоматический, так и ручной режимы инкрементальной миграции. Автоматическая миграция подходит для большинства базовых изменений схемы, но для более сложных изменений может потребоваться вручную определить пути миграции.
Автоматизированные миграции
Для объявления автоматической миграции между двумя версиями базы данных добавьте аннотацию @AutoMigration к свойству ` autoMigrations в @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 }
Спецификации автоматической миграции
Если Room обнаруживает неоднозначные изменения схемы и не может сгенерировать план миграции без дополнительных данных, он выдает ошибку компиляции, и вам необходимо предоставить реализацию AutoMigrationSpec . Чаще всего это происходит, когда миграция включает в себя одно из следующих:
- Удаление или переименование таблицы.
- Удаление или переименование столбца.
Вы можете использовать AutoMigrationSpec , чтобы предоставить Room дополнительную информацию, необходимую для корректного создания путей миграции. Определите класс, реализующий интерфейс AutoMigrationSpec в вашем классе RoomDatabase и аннотируйте его одним или несколькими из следующих способов:
Чтобы использовать реализацию AutoMigrationSpec для автоматической миграции, установите свойство spec в соответствующей аннотации @AutoMigration :
@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
Если вашему приложению необходимо выполнить дополнительную работу после завершения автоматической миграции, вы можете реализовать onPostMigrate . Если вы реализуете эту функцию в своем AutoMigrationSpec , Room вызовет ее после завершения автоматической миграции.
Ручная миграция
Если миграция включает сложные изменения схемы, Room может не суметь автоматически сгенерировать подходящий путь миграции. Например, если вы решите разделить данные в таблице на две таблицы, Room не сможет определить, как выполнить это разделение. В таких ситуациях вам необходимо вручную определить путь миграции, реализовав класс Migration .
Класс Migration явно определяет путь миграции между startVersion и endVersion , переопределяя функцию migrate . Добавьте свои классы Migration в конструктор базы данных, используя функцию addMigrations :
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()
При определении путей миграции вы можете использовать автоматическую миграцию для одних версий и ручную миграцию для других. Если вы определите как автоматическую, так и ручную миграцию для одной и той же версии, Room будет использовать ручную миграцию.
Тестовые миграции
Миграции часто бывают сложными, и неправильно определенная миграция может привести к сбою приложения. Для сохранения стабильности приложения тестируйте миграции. Room предоставляет артефакт Maven room3-testing , который помогает в процессе тестирования как автоматических, так и ручных миграций. Для работы этого артефакта необходимо сначала экспортировать схему вашей базы данных.
Экспортные схемы
Room экспортирует информацию о схеме вашей базы данных в файл JSON во время компиляции. Экспортированные файлы JSON представляют собой историю схемы вашей базы данных. Сохраните эти файлы в вашей системе контроля версий, чтобы вы могли воссоздавать более старые версии базы данных для тестирования и поддерживать автоматическую генерацию миграций.
Укажите расположение схемы с помощью плагина Room Gradle.
Чтобы указать каталог схемы, примените плагин Room Gradle и используйте расширение room3 .
Классный
plugins {
id 'androidx.room3'
}
room3 {
schemaDirectory "$projectDir/schemas"
}
Котлин
plugins {
id("androidx.room3")
}
room3 {
schemaDirectory("$projectDir/schemas")
}
Если схема вашей базы данных различается в зависимости от варианта, конфигурации или типа сборки, необходимо указать разные расположения, используя конфигурацию schemaDirectory несколько раз, каждый раз с variantMatchName в качестве первого аргумента. Каждая конфигурация может сопоставлять один или несколько вариантов на основе простого сравнения с именем варианта.
Убедитесь, что эти параметры являются исчерпывающими и охватывают все варианты. Вы также можете включить schemaDirectory() без variantMatchName для обработки вариантов, не соответствующих ни одной из других конфигураций. Например, в приложении с двумя вариантами сборки ( demo и full и двумя типами сборки debug и release ) допустимыми конфигурациями являются следующие:
Классный
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"
}
Котлин
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")
}
Укажите расположение схемы с помощью параметра обработчика аннотаций.
Если вы не используете плагин Room Gradle, укажите расположение схемы с помощью параметра обработчика аннотаций room.schemaLocation .
Gradle использует файлы в этом каталоге в качестве входных и выходных данных для некоторых задач 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() {
return ["room.schemaLocation=${schemaDir.path}".toString()]
}
}
Котлин
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File
) : CommandLineArgumentProvider {
override fun asArguments(): Iterable<String> {
return listOf("room.schemaLocation=${schemaDir.path}")
}
}
Затем настройте параметры компиляции для использования RoomSchemaArgProvider с указанным каталогом схемы:
Классный
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
Котлин
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
Протестируйте одну миграцию.
Прежде чем тестировать миграции, добавьте артефакт androidx.room3:room3-testing в зависимости для тестирования и укажите расположение экспортированной схемы в качестве каталога ресурсов:
Классный
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" }
Котлин
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") }
Пакет тестирования предоставляет класс MigrationTestHelper , который может считывать экспортированные файлы схем. Пакет также реализует интерфейс JUnit4 TestRule для управления созданными базами данных.
Следующий пример демонстрирует проверку одной миграции:
@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() } }
Протестируйте все миграции.
Хотя вы можете протестировать одну инкрементальную миграцию, следует включить тест, охватывающий все миграции, определенные для базы данных вашего приложения. Это поможет гарантировать отсутствие расхождений между недавно созданным экземпляром базы данных и более ранним экземпляром, который следовал определенным путям миграции.
Следующий пример демонстрирует проверку всех определенных миграций:
@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() } }
Корректно обрабатывайте отсутствующие пути миграции.
Если Room не может найти путь миграции для обновления существующей базы данных на устройстве до текущей версии, возникает исключение IllegalStateException . Если допустима потеря существующих данных при отсутствии пути миграции, вызовите функцию-конструктор fallbackToDestructiveMigration при создании базы данных:
Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name") .fallbackToDestructiveMigration() .build()
Эта функция настраивает Room на деструктивное пересоздание таблиц в базе данных вашего приложения, когда необходимо выполнить инкрементальную миграцию и отсутствует определенный путь миграции.
Чтобы вернуться к деструктивному воссозданию только в определенных ситуациях, используйте одну из следующих альтернатив функции fallbackToDestructiveMigration :
- Если определенные версии истории вашей схемы вызывают ошибки, которые вы не можете устранить с помощью путей миграции, используйте вместо этого
fallbackToDestructiveMigrationFrom. Эта функция указывает, что вы хотите, чтобы Room возвращался к деструктивному воссозданию только при миграции с определенных версий. - Если вы хотите, чтобы Room возвращался к деструктивному созданию резервной копии только при миграции с более новой версии базы данных на более старую, используйте вместо этого
fallbackToDestructiveMigrationOnDowngrade.