Wenn Sie Funktionen in Ihrer App hinzufügen oder ändern, müssen Sie das Element „Raum“ anpassen und zugrunde liegende Datenbanktabellen, um diese Änderungen widerzuspiegeln. Es ist wichtig, um Nutzerdaten zu sichern, die sich bereits in der Datenbank auf dem Gerät befinden, Mit update wird das Datenbankschema geändert.
Room unterstützt sowohl automatisierte als auch manuelle Optionen für die inkrementelle Migration. Automatische Migrationen funktionieren für die meisten grundlegenden Schemaänderungen, Migrationspfade für komplexere Änderungen manuell zu definieren.
Automatisierte Migrationen
Um eine automatisierte Migration zwischen zwei Datenbankversionen zu deklarieren, fügen Sie ein
Anmerkung @AutoMigration
zu
autoMigrations
Property in @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 { ... }
Spezifikationen für die automatische Migration
Wenn Room mehrdeutige Schemaänderungen erkennt und keine
ohne weitere Eingaben zu migrieren, wird ein Fehler bei der Kompilierung ausgegeben und Sie werden gefragt,
zum Implementieren eines
AutoMigrationSpec
Am häufigsten tritt dies auf, wenn eine Migration eines der folgenden Elemente umfasst:
- Löschen oder Umbenennen einer Tabelle
- Löschen oder Umbenennen einer Spalte
Mithilfe von AutoMigrationSpec
kannst du Room die zusätzlichen Informationen
Migrationspfade korrekt generieren müssen. Definieren Sie eine statische Klasse, die
implementiert AutoMigrationSpec
in Ihrer RoomDatabase
-Klasse und annotiert mit
mindestens eines der folgenden Elemente:
Wenn Sie die AutoMigrationSpec
-Implementierung für eine automatisierte Migration verwenden möchten, legen Sie Folgendes fest:
das Attribut spec
in der entsprechenden @AutoMigration
-Annotation:
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 { } ... }
Wenn Ihre Anwendung nach der automatischen Migration weitere Arbeitsschritte ausführen muss, können Sie
implementieren
onPostMigrate()
Wenn Sie diese Methode in Ihrem AutoMigrationSpec
implementieren, wird sie nach dem
abgeschlossen wird.
Manuelle Migrationen
Wenn eine Migration komplexe Schemaänderungen umfasst,
automatisch einen geeigneten Migrationspfad
generieren können. Wenn beispielsweise
Sie die Daten in einer Tabelle in zwei Tabellen aufteilen,
wie diese Aufteilung durchgeführt wird. In solchen Fällen müssen Sie manuell
einen Migrationspfad definieren, indem Sie
Klasse Migration
.
Eine Migration
-Klasse definiert explizit einen Migrationspfad zwischen einer
startVersion
und endVersion
durch Überschreiben des
Migration.migrate()
. Fügen Sie Ihrem Datenbank-Builder Ihre Migration
-Klassen hinzu mit
die
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();
Wenn Sie Ihre Migrationspfade definieren, können Sie für einige Versionen und manuelle Migrationen für andere. Wenn Sie sowohl eine automatisierte und eine manuelle Migration für dieselbe Version. Migration.
Migrationen testen
Migrationen sind oft komplex und eine falsch definierte Migration kann zu
Ihre App abstürzt. Um die Stabilität Ihrer App zu wahren, testen Sie Ihre
Migrationen. Der Chatroom bietet ein room-testing
-Maven-Artefakt zur Unterstützung bei der
Testprozess für automatisierte und manuelle Migrationen. Damit dieses Artefakt
müssen Sie zuerst das Schema
Ihrer Datenbank exportieren.
Schemas exportieren
Der Chatroom kann die Schemainformationen Ihrer Datenbank beim Kompilieren in eine JSON-Datei exportieren . Die exportierten JSON-Dateien stellen den Schemaverlauf Ihrer Datenbank dar. Geschäft in Ihrem Versionskontrollsystem, damit Room niedrigere Versionen der Datei erstellen kann, die Datenbank zu Testzwecken und zur automatischen Migrationsgenerierung.
Schemaort mit Room Gradle-Plug-in festlegen
Wenn Sie die Room-Version 2.6.0 oder höher verwenden, können Sie
Room-Gradle-Plug-in herunter und verwenden Sie die
Erweiterung room
, um das Schemaverzeichnis anzugeben.
Cool
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Wenn sich Ihr Datenbankschema je nach Variante, Flavor oder Build unterscheidet
-Typ handelt, müssen Sie verschiedene Standorte mithilfe der schemaDirectory()
angeben
Konfiguration mehrfach ausführen, jeweils mit einem variantMatchName
als erstem
. Bei jeder Konfiguration können basierend auf einfachen
mit dem Variantennamen vergleichen.
Achten Sie darauf, dass diese Informationen vollständig sind und alle Varianten abdecken. Sie können auch einen
schemaDirectory()
ohne variantMatchName
, um nicht übereinstimmende Varianten zu verarbeiten
einer der anderen Konfigurationen. Beispiel: In einer App mit zwei
Flavor-Varianten demo
und full
sowie die beiden Build-Typen debug
und release
, die
sind folgende gültige Konfigurationen:
Cool
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")
}
Schemastandort mit Option für Annotationsprozessor festlegen
Wenn Sie Version 2.5.2 oder niedriger von Room oder die
Room Gradle-Plug-in, lege den Speicherort des Schemas mit der room.schemaLocation
fest
Annotation Processor.
Dateien in diesem Verzeichnis werden für einige Gradle-Aufgaben als Ein- und Ausgaben verwendet.
Für die Richtigkeit und Leistung von inkrementellen und im Cache gespeicherten Builds müssen Sie Folgendes verwenden:
Gradles
CommandLineArgumentProvider
um Gradle über dieses Verzeichnis zu informieren.
Kopieren Sie zuerst die unten gezeigte RoomSchemaArgProvider
-Klasse in das
Gradle-Build-Datei. Die Methode asArguments()
in der Beispielklasse besteht aus
room.schemaLocation=${schemaDir.path}
auf KSP
. Wenn Sie KAPT
verwenden und
javac
, ändern Sie diesen Wert stattdessen in -Aroom.schemaLocation=${schemaDir.path}
.
Cool
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}")
}
}
Konfigurieren Sie dann die Kompilierungsoptionen so, dass RoomSchemaArgProvider
mit dem
angegebenes Schemaverzeichnis:
Cool
// 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"))
)
}
}
}
}
Einzelne Migration testen
Bevor Sie Ihre Migrationen testen können, fügen Sie den Parameter
androidx.room:room-testing
Maven-Artefakt aus Room in den Test einfügen
Abhängigkeiten und fügen Sie den Speicherort des exportierten Schemas als Asset-Ordner hinzu:
build.gradle-Datei
Cool
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") }
Das Testpaket bietet eine
MigrationTestHelper
Klasse, die exportierte Schemadateien lesen kann. Das Paket implementiert auch die
JUnit4
TestRule
um erstellte Datenbanken zu verwalten.
Das folgende Beispiel zeigt einen Test für eine einzelne Migration:
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. } }
Alle Migrationen testen
Es ist zwar möglich, eine einzelne inkrementelle Migration zu testen, wir empfehlen jedoch, dass Sie einen Test einbinden, der alle Migrationen abdeckt, Datenbank. So wird sichergestellt, dass es keine Diskrepanz zwischen einem kürzlich erstellten Datenbankinstanz und eine ältere Instanz, die der definierten Migration folgte Pfade.
Das folgende Beispiel zeigt einen Test für alle definierten Migrationen:
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}; }
Ordnungsgemäße Handhabung fehlender Migrationspfade
Wenn Room keinen Migrationspfad zum Upgrade einer vorhandenen Datenbank auf einem
auf die aktuelle Version,
IllegalStateException
ist aufgetreten. Wenn
können vorhandene Daten verloren gehen, wenn ein Migrationspfad fehlt, rufen Sie
die
fallbackToDestructiveMigration()
Builder-Methode beim Erstellen der Datenbank:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Mit dieser Methode wird Room angewiesen, die Tabellen in den Tabellen Ihrer App wenn eine inkrementelle Migration erforderlich ist und es keine Migrationspfad definiert.
Wenn Sie möchten, dass in einem bestimmten
Raum nur destruktive Wiederherstellungen
gibt es einige Alternativen zu fallbackToDestructiveMigration()
:
- Wenn bestimmte Versionen Ihres Schemaverlaufs Fehler verursachen, die Sie nicht beheben können
mit Migrationspfaden, verwenden Sie
fallbackToDestructiveMigrationFrom()
. Diese Methode gibt an, dass der Raum auf destruktiv die Wiederherstellung nur bei einer Migration von bestimmten Versionen. - Wenn der Raum nur bei der Migration auf die destruktive Neuerstellung zurückgesetzt werden soll
Datenbankversion von einer
höheren Datenbankversion in eine niedrigere Version,
fallbackToDestructiveMigrationOnDowngrade()
.
Standardspaltenwerte beim Upgrade auf Raum 2.2.0 verarbeiten
In Raum 2.2.0 und höher können Sie einen Standardwert für eine Spalte definieren, indem Sie
die Anmerkung
@ColumnInfo(defaultValue = "...")
In Versionen vor 2.2.0 ist die einzige Möglichkeit, einen Standardwert für einen
Spalte besteht darin, sie direkt in einer ausgeführten SQL-Anweisung zu definieren, wodurch ein
Standardwert, den Room nicht bekannt ist. Wenn also eine Datenbank
ursprünglich in einer älteren Version als 2.2.0 von Room erstellt wurde.
Wenn Sie Raum 2.2.0 verwenden, müssen Sie möglicherweise einen speziellen Migrationspfad für
vorhandene Standardwerte, die Sie ohne die Verwendung von Raum-APIs definiert haben.
Angenommen, Version 1 einer Datenbank definiert eine Song
-Entität:
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; }
Nehmen wir außerdem an, dass Version 2 derselben Datenbank eine neue NOT NULL
-Spalte hinzufügt
und definiert einen Migrationspfad von Version 1 zu Version 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 ''"); } };
Dies führt zu einer Abweichung in der zugrunde liegenden Tabelle zwischen Aktualisierungen und aktuellen
der Installation der App. Da der Standardwert für die Spalte tag
nur
im Migrationspfad von Version 1 zu Version 2 angegeben haben,
die App ab Version 2 installieren, haben nicht den Standardwert für tag
in ihrem Datenbankschema.
In niedrigeren Versionen von Room als Version 2.2.0 ist diese Diskrepanz harmlos. Wenn jedoch
Die App führt später ein Upgrade auf Raum 2.2.0 oder höher durch und ändert die Entität Song
Klasse, um einen Standardwert für tag
mithilfe der Methode
Anmerkung @ColumnInfo
, Chatroom
kann diese Diskrepanz sehen. Dies führt zu einem fehlgeschlagenen Schema
Validierungen.
Damit sichergestellt ist, dass das Datenbankschema für alle Nutzer einheitlich ist, wenn die Spalte Standardwerte sind in Ihren früheren Migrationspfaden deklariert. Gehen Sie so vor: wenn Sie Ihre App zum ersten Mal auf Raum 2.2.0 oder höher aktualisieren:
- Geben Sie mithilfe der Methode
@ColumnInfo
-Anmerkung. - Erhöhen Sie die Versionsnummer der Datenbank um 1.
- Definieren Sie einen Migrationspfad zur neuen Version, in der die Drop- und Strategie neu erstellen um den vorhandenen Spalten die erforderlichen Standardwerte hinzuzufügen.
Das folgende Beispiel veranschaulicht diesen Vorgang:
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"); } };