Podczas dodawania i modyfikowania funkcji aplikacji musisz zmodyfikować element pokoju i bazowych tabel baz danych, aby odzwierciedlić te zmiany. To ważne w celu zachowania danych użytkownika, które są już w bazie danych na urządzeniu, gdy aplikacja update zmienia schemat bazy danych.
W przypadku pokoi obsługiwane są zarówno automatyczne, jak i ręczne opcje migracji przyrostowej. Automatyczne migracje działają w przypadku większości podstawowych zmian schematu, ale może być konieczne i ręcznie zdefiniować ścieżki migracji w przypadku bardziej złożonych zmian.
Migracje automatyczne
Aby zadeklarować automatyczną migrację między 2 wersjami bazy danych, dodaj atrybut
@AutoMigration
adnotacja do
autoMigrations
właściwość w @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 { ... }
Specyfikacje automatycznej migracji
Jeśli Room wykryje niejednoznaczne zmiany schematu i nie będzie w stanie wygenerować
bez wprowadzania dodatkowych danych, generuje błąd podczas kompilowania i pyta
aby zaimplementować
AutoMigrationSpec
Dzieje się tak najczęściej wtedy, gdy migracja obejmuje jedną z tych sytuacji:
- usunięcie tabeli lub zmianę jej nazwy,
- usunięcie kolumny lub zmianę jej nazwy,
Za pomocą AutoMigrationSpec
możesz przekazać do pokoju dodatkowe informacje,
musi prawidłowo wygenerować ścieżki migracji. Zdefiniuj klasę statyczną, która
implementuje AutoMigrationSpec
w klasie RoomDatabase
i dodaje do niej adnotację
któreś z tych problemów:
Aby użyć implementacji AutoMigrationSpec
do automatycznej migracji, ustaw
właściwość spec
w odpowiedniej adnotacji @AutoMigration
:
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 { } ... }
Jeśli po zakończeniu automatycznej migracji aplikacja musi wykonać jeszcze więcej zadań,
może zastosować
onPostMigrate()
Jeśli wdrożysz tę metodę w swoim obiekcie AutoMigrationSpec
, sala wywoła ją po
po zakończeniu automatycznej migracji.
Migracje ręczne
Jeśli migracja obejmuje złożone zmiany schematu,
aby automatycznie wygenerować odpowiednią ścieżkę migracji. Na przykład, jeśli
Jeśli postanowisz podzielić dane w tabeli na dwie tabele, pokój nie wie,
jak przeprowadzić ten podział. W takich przypadkach musisz ręcznie
zdefiniować ścieżkę migracji, implementując
Migration
.
Klasa Migration
wyraźnie określa ścieżkę migracji między
startVersion
i endVersion
, zastępując wartości
Migration.migrate()
. Dodaj klasy Migration
do kreatora baz danych za pomocą
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();
Podczas definiowania ścieżek migracji możesz użyć automatycznych migracji oraz przeprowadzić migrację ręczną dla innych. Jeśli zdefiniujesz zarówno tag automatyczny, migracji i migracji ręcznej dla tej samej wersji, to pokój użyje migracji danych.
Migracje testowe
Migracje są często złożone, a ich nieprawidłowe zdefiniowanie może
do awarii. Aby zachować stabilność aplikacji, przetestuj ją
migracji. W sali dostępny jest artefakt Maven room-testing
, który pomaga w
automatycznego i ręcznego testowania migracji. Dla tego artefaktu do
musisz najpierw wyeksportować schemat bazy danych.
Eksportuj schematy
Sala może wyeksportować informacje o schemacie bazy danych do pliku JSON na stronie kompilacji obecnie się znajdujesz. Wyeksportowane pliki JSON reprezentują historię schematu bazy danych. Sklep tych plików w systemie kontroli wersji, aby pokój mógł tworzyć niższe wersje bazy danych do celów testowych oraz do automatycznego generowania migracji.
Ustawianie lokalizacji schematu za pomocą wtyczki Room Gradle
Jeśli korzystasz z sali w wersji 2.6.0 lub nowszej, możesz zastosować
wtyczki do obsługi sali Gradle i użyj wtyczki
room
określające katalog schematu.
Odlotowe
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Jeśli schemat bazy danych różni się w zależności od wersji, smaku lub kompilacji
musisz określić różne lokalizacje za pomocą atrybutu schemaDirectory()
konfiguracji kilka razy, przy czym w każdej z nich jako pierwszym ustawiono variantMatchName
. Każda konfiguracja może pasować do co najmniej jednego wariantu na podstawie prostych
w porównaniu z nazwą wariantu.
Upewnij się, że są one wyczerpujące i obejmują wszystkie wersje. Możesz też dodać atrybut
schemaDirectory()
bez variantMatchName
, aby obsługiwać niedopasowane warianty
przez dowolną z pozostałych konfiguracji. Na przykład w aplikacji z 2 kompilacjami
smaki demo
i full
oraz 2 typy kompilacji debug
i release
,
to są prawidłowe konfiguracje:
Odlotowe
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")
}
Ustaw lokalizację schematu za pomocą opcji procesora adnotacji
Jeśli korzystasz z wersji 2.5.2 lub starszej pokoju lub nie używasz
Wtyczka Room Gradle, ustaw lokalizację schematu za pomocą interfejsu room.schemaLocation
procesora adnotacji.
Pliki w tym katalogu są używane jako dane wejściowe i wyjściowe w niektórych zadaniach Gradle.
Aby zapewnić prawidłowość i wydajność kompilacji przyrostowych i zapisanych w pamięci podręcznej, musisz użyć
Gradle
CommandLineArgumentProvider
aby poinformować Gradle o tym katalogu.
Najpierw skopiuj widoczne poniżej zajęcia RoomSchemaArgProvider
do modułu
Plik kompilacji Gradle. Metoda asArguments()
w przykładowej klasie przekazuje
room.schemaLocation=${schemaDir.path}
do KSP
. Jeśli korzystasz z KAPT
i
javac
, zmień tę wartość na -Aroom.schemaLocation=${schemaDir.path}
.
Odlotowe
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}")
}
}
Następnie skonfiguruj opcje kompilacji, aby używać polecenia RoomSchemaArgProvider
z parametrem
określony katalog schematu:
Odlotowe
// 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"))
)
}
}
}
}
Testowanie pojedynczej migracji
Aby przetestować migracje, dodaj do
androidx.room:room-testing
artefakt Maven z pokoju do testu
zależności i dodaj lokalizację wyeksportowanego schematu jako folder zasobów:
Odlotowe
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") }
Pakiet testowy zawiera:
MigrationTestHelper
klasy, która może odczytywać wyeksportowane pliki schematu. Pakiet implementuje również
JUnit4
TestRule
do zarządzania utworzonymi bazami danych.
Ten przykład przedstawia test pojedynczej migracji:
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. } }
Testowanie wszystkich migracji
Mimo że można przetestować pojedynczą migrację przyrostową, zalecamy uwzględnić test obejmujący wszystkie migracje zdefiniowane dla w bazie danych. Pomoże to uniknąć rozbieżności między niedawno utworzonymi instancja bazy danych oraz starsza instancja, która nastąpiła po zdefiniowanej migracji ścieżek konwersji.
Poniższy przykład przedstawia test dla wszystkich zdefiniowanych migracji:
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}; }
Bezpłatna obsługa brakujących ścieżek migracji
Jeśli sala nie może znaleźć ścieżki migracji do uaktualnienia istniejącej bazy danych
do aktualnej wersji,
IllegalStateException
ma miejsce. Jeśli
to można stracić istniejące dane, jeśli brakuje ścieżki migracji, wywołanie
fallbackToDestructiveMigration()
konstruktora podczas tworzenia bazy danych:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Dzięki tej metodzie Room może niszczycielsko odtworzyć tabele w pliku w bazie danych, gdy musi przeprowadzić migrację przyrostową i nie jest zdefiniowaną ścieżkę migracji.
Jeśli chcesz, aby pokój powrócił do destrukcyjnej rekreacji w określonych
fallbackToDestructiveMigration()
, istnieje kilka innych możliwości:
- Jeśli określone wersje historii schematu powodują błędy, których nie możesz naprawić
ze ścieżkami migracji, użyj funkcji
fallbackToDestructiveMigrationFrom()
. Ta metoda wskazuje, że chcesz, aby pokój zmienił się w niszczycielski odtwarzania tylko w przypadku migracji z określonych wersji. - Jeśli chcesz, aby pokój powrócił do stanu destrukcyjnego tylko podczas migracji
z wyższej wersji bazy danych na niższą, użyj
fallbackToDestructiveMigrationOnDowngrade()
.
Przejście na pokój 2.2.0 obsługuje wartości domyślne kolumn
W pokoju 2.2.0 i nowszych możesz zdefiniować domyślną wartość kolumny za pomocą funkcji
adnotacja
@ColumnInfo(defaultValue = "...")
W wersjach starszych niż 2.2.0 jedynym sposobem zdefiniowania wartości domyślnej dla
jest zdefiniowanie jej bezpośrednio w wykonanej instrukcji SQL, która tworzy
wartość domyślna, o której Sala nie wie. Oznacza to, że jeśli baza danych to
zostało utworzone w Pokoju w wersji starszej niż 2.2.0, po przejściu na
jeśli używasz aplikacji Room 2.2.0, może być konieczne podanie specjalnej ścieżki migracji
zdefiniowanych bez użycia interfejsów API sal.
Załóżmy na przykład, że wersja 1 bazy danych definiuje encję 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; }
Załóżmy też, że wersja 2 tej samej bazy danych dodaje nową kolumnę NOT NULL
i określa ścieżkę migracji z wersji 1 do wersji 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 ''"); } };
Powoduje to rozbieżność między aktualizacjami a aktualnymi danymi w tabeli.
instalacji aplikacji. Ponieważ domyślna wartość w kolumnie tag
to tylko
zadeklarowane w ścieżce migracji z wersji 1 do wersji 2, wszyscy użytkownicy, którzy
instalowanie aplikacji od wersji 2 nie ma domyślnej wartości parametru tag
w schemacie bazy danych.
W wersjach pokoju starszych niż 2.2.0 ta rozbieżność jest nieszkodliwa. Jeśli jednak
aplikacja zostanie później uaktualniona do pomieszczenia 2.2.0 lub nowszego i zmieni się element Song
class, aby uwzględnić domyślną wartość dla tag
za pomocą funkcji
Adnotacja @ColumnInfo
, sala
zobaczy tę rozbieżność. Powoduje to nieprawidłowy schemat
weryfikacji danych.
Aby mieć pewność, że schemat bazy danych jest spójny u wszystkich użytkowników, gdy kolumna wartości domyślne są zadeklarowane we wcześniejszych ścieżkach migracji, wykonaj te czynności przy pierwszym uaktualnieniu aplikacji do pokoju 2.2.0 lub nowszego:
- Zadeklaruj domyślne wartości kolumn w odpowiednich klasach encji za pomocą
@ColumnInfo
adnotacja. - Zwiększ numer wersji bazy danych o 1.
- Zdefiniuj ścieżkę migracji do nowej wersji, która implementuje mechanizm usuwania odtworzyć strategię aby dodać niezbędne wartości domyślne do istniejących kolumn.
Następujący przykład ilustruje ten proces:
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"); } };