W miarę dodawania i zmieniania funkcji w aplikacji musisz modyfikować klasy encji Room i powiązane tabele bazy danych, aby odzwierciedlały te zmiany. Ważne jest, aby zachować dane użytkownika, które są już w bazie danych na urządzeniu, gdy aktualizacja aplikacji zmienia schemat bazy danych.
Room obsługuje zarówno automatyczne, jak i ręczne opcje migracji przyrostowej. Automatyczne migracje działają w przypadku większości podstawowych zmian schematu, ale w przypadku bardziej złożonych zmian może być konieczne ręczne zdefiniowanie ścieżek migracji.
Automatyczne migracje
Aby zadeklarować automatyczną migrację między 2 wersjami bazy danych, dodaj an
@AutoMigration adnotację do właściwości autoMigrations w
@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 }
Specyfikacje automatycznej migracji
Jeśli Room wykryje niejednoznaczne zmiany schematu i nie będzie w stanie wygenerować planu migracji
bez dodatkowych danych, zgłosi błąd kompilacji i musisz podać implementację
AutoMigrationSpec. Najczęściej zdarza się to, gdy migracja obejmuje jedną z tych czynności:
- Usuwanie lub zmiana nazwy tabeli.
- Usuwanie lub zmiana nazwy kolumny.
Możesz użyć AutoMigrationSpec, aby przekazać Room dodatkowe informacje potrzebne do prawidłowego wygenerowania ścieżek migracji. Zdefiniuj klasę, która implementuje AutoMigrationSpec w klasie RoomDatabase, i dodaj do niej jedną lub więcej z tych adnotacji:
Aby użyć implementacji AutoMigrationSpec w automatycznej migracji, ustaw właściwość spec w odpowiedniej adnotacji @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
Jeśli aplikacja musi wykonać więcej czynności po zakończeniu automatycznej migracji, możesz zaimplementować onPostMigrate. Jeśli zaimplementujesz tę funkcję w AutoMigrationSpec, Room wywoła ją po zakończeniu automatycznej migracji.
Migracje ręczne
Jeśli migracja obejmuje złożone zmiany schematu, Room może nie być w stanie automatycznie wygenerować odpowiedniej ścieżki migracji. Jeśli na przykład zdecydujesz się podzielić dane w tabeli na 2 tabele, Room nie będzie w stanie określić, jak to zrobić. W takich sytuacjach musisz ręcznie zdefiniować a
ścieżkę migracji, implementując a Migration klasę.
Klasa Migration jawnie definiuje ścieżkę migracji między startVersion
a endVersion, zastępując funkcję migrate. Dodaj
swoje Migration klasy do narzędzia do tworzenia bazy danych za pomocą
addMigrations funkcji:
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()
Podczas definiowania ścieżek migracji możesz używać automatycznych migracji w przypadku niektórych wersji i ręcznych migracji w przypadku innych. Jeśli zdefiniujesz zarówno automatyczną, jak i ręczną migrację dla tej samej wersji, Room użyje migracji ręcznej.
Testowanie migracji
Migracje są często złożone, a nieprawidłowo zdefiniowana migracja może spowodować awarię aplikacji. Aby zachować stabilność aplikacji, przetestuj migracje. Room udostępnia artefakt Maven room3-testing, który pomaga w testowaniu zarówno automatycznych, jak i ręcznych migracji. Aby ten artefakt działał, musisz najpierw wyeksportować schemat bazy danych.
Eksportowanie schematów
Room eksportuje informacje o schemacie bazy danych do pliku JSON w czasie kompilacji. Wyeksportowane pliki JSON reprezentują historię schematu bazy danych. Przechowuj te pliki w systemie kontroli wersji, aby móc odtworzyć starsze wersje bazy danych na potrzeby testowania i obsługi automatycznego generowania migracji.
Ustawianie lokalizacji schematu za pomocą wtyczki Room Gradle
Aby określić katalog schematu, zastosuj wtyczkę Room Gradle i użyj rozszerzenia
room3.
Dynamiczny
plugins {
id 'androidx.room3'
}
room3 {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room3")
}
room3 {
schemaDirectory("$projectDir/schemas")
}
Jeśli schemat bazy danych różni się w zależności od wariantu, wersji lub typu kompilacji, musisz określić różne lokalizacje, używając konfiguracji schemaDirectory kilka razy, a jako pierwszy argument podając variantMatchName. Każda konfiguracja może pasować do co najmniej 1 wariantu na podstawie prostego porównania z nazwą wariantu.
Upewnij się, że są one wyczerpujące i obejmują wszystkie warianty. Możesz też dodać schemaDirectory() bez variantMatchName, aby obsługiwać warianty, które nie pasują do żadnej z pozostałych konfiguracji. Na przykład w aplikacji z 2 wersjami kompilacji (demo i full) oraz 2 typami kompilacji (debug i release) prawidłowe konfiguracje to:
Dynamiczny
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")
}
Ustawianie lokalizacji schematu za pomocą opcji procesora adnotacji
Jeśli nie używasz wtyczki Room Gradle, ustaw lokalizację schematu za pomocą opcji procesora adnotacji room.schemaLocation.
Gradle używa plików w tym katalogu jako danych wejściowych i wyjściowych w przypadku niektórych zadań Gradle.
Aby zapewnić poprawność i wydajność kompilacji przyrostowych i buforowanych, musisz użyć
Gradle's CommandLineArgumentProvider, aby poinformować Gradle o
tym katalogu.
Najpierw skopiuj tę klasę RoomSchemaArgProvider do pliku kompilacji Gradle modułu. Funkcja asArguments w przykładowej klasie przekazuje room.schemaLocation=${schemaDir.path} do KSP. Jeśli używasz KAPT i javac, zmień tę wartość na -Aroom.schemaLocation=${schemaDir.path}.
Dynamiczny
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}")
}
}
Następnie skonfiguruj opcje kompilacji, aby używać RoomSchemaArgProvider z określonym katalogiem schematu:
Dynamiczny
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
Kotlin
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
Testowanie pojedynczej migracji
Zanim przetestujesz migracje, dodaj artefakt androidx.room3:room3-testing do zależności testowych i dodaj lokalizację wyeksportowanego schematu jako katalog zasobów:
Dynamiczny
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") }
Pakiet testowy zawiera klasę MigrationTestHelper, która może odczytywać
wyeksportowane pliki schematu. Pakiet implementuje też interfejs JUnit4
TestRule do zarządzania utworzonymi bazami danych.
Ten przykład pokazuje test pojedynczej migracji:
@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() } }
Testowanie wszystkich migracji
Chociaż możesz przetestować pojedynczą migrację przyrostową, warto uwzględnić test, który obejmuje wszystkie migracje zdefiniowane dla bazy danych aplikacji. Pomaga to zapewnić, że nie ma rozbieżności między nowo utworzoną instancją bazy danych a wcześniejszą instancją, która korzystała ze zdefiniowanych ścieżek migracji.
Ten przykład pokazuje test wszystkich zdefiniowanych migracji:
@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() } }
Eleganckie obsługiwanie brakujących ścieżek migracji
Jeśli Room nie może znaleźć ścieżki migracji, aby uaktualnić istniejącą bazę danych na
urządzeniu do bieżącej wersji, wystąpi IllegalStateException. Jeśli
utrata istniejących danych w przypadku braku ścieżki migracji jest dopuszczalna, podczas tworzenia bazy danych wywołaj
funkcję narzędzia do tworzenia fallbackToDestructiveMigration:
Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name") .fallbackToDestructiveMigration() .build()
Ta funkcja konfiguruje Room tak, aby destrukcyjnie odtwarzała tabele w bazie danych aplikacji, gdy musi przeprowadzić migrację przyrostową, a nie ma zdefiniowanej ścieżki migracji.
Aby przywrócić destrukcyjne odtwarzanie tylko w określonych sytuacjach, użyj jednej z tych alternatyw dla fallbackToDestructiveMigration:
- Jeśli określone wersje historii schematu powodują błędy, których nie można rozwiązać
za pomocą ścieżek migracji, użyj
fallbackToDestructiveMigrationFromzamiast tego. Ta funkcja wskazuje, że chcesz, aby Room przywracał destrukcyjne odtwarzanie tylko podczas migracji z określonych wersji. - Jeśli chcesz, aby Room przywracał destrukcyjne odtwarzanie tylko podczas migracji
z wyższej wersji bazy danych do niższej, użyj
fallbackToDestructiveMigrationOnDowngradezamiast tego.