Wenn Sie Elemente in Ihrer Anwendung hinzufügen oder ändern, müssen Sie die Entitätsklassen des Typs „Raum“ und die zugrunde liegenden Datenbanktabellen anpassen, um diese Änderungen zu berücksichtigen. Es ist wichtig, Nutzerdaten, die sich bereits in der Datenbank auf dem Gerät befinden, beizubehalten, wenn ein App-Update das Datenbankschema ä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. Für komplexere Änderungen müssen Sie jedoch möglicherweise Migrationspfade manuell definieren.
Automatisierte Migrationen
Wenn Sie eine automatisierte Migration zwischen zwei Datenbankversionen deklarieren möchten, fügen Sie dem Attribut autoMigrations
in @Database
eine @AutoMigration
-Annotation hinzu:
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 keinen Migrationsplan ohne weitere Eingaben generieren kann, wird ein Fehler bei der Kompilierungszeit ausgegeben und Sie werden aufgefordert, eine AutoMigrationSpec
zu implementieren.
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
Sie können AutoMigrationSpec
verwenden, um Room die zusätzlichen Informationen bereitzustellen, die zum korrekten Generieren von Migrationspfaden erforderlich sind. Definieren Sie eine statische Klasse, die AutoMigrationSpec
in Ihrer RoomDatabase
-Klasse implementiert, und annotieren Sie sie mit einem oder mehreren der folgenden Elemente:
Wenn Sie die AutoMigrationSpec
-Implementierung für eine automatisierte Migration verwenden möchten, legen Sie das Attribut spec
in der entsprechenden @AutoMigration
-Annotation fest:
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 automatisierten Migration weitere Aufgaben ausführen muss, können Sie onPostMigrate()
implementieren.
Wenn Sie diese Methode in Ihrem AutoMigrationSpec
implementieren, wird sie nach Abschluss der automatisierten Migration von Room aufgerufen.
Manuelle Migrationen
Wenn eine Migration komplexe Schemaänderungen umfasst, kann Room möglicherweise nicht automatisch einen geeigneten Migrationspfad generieren. Wenn Sie beispielsweise die Daten in einer Tabelle in zwei Tabellen aufteilen, kann Room nicht erkennen, wie diese Aufteilung erfolgen soll. In solchen Fällen müssen Sie manuell einen Migrationspfad definieren. Implementieren Sie dazu eine Migration
-Klasse.
Eine Migration
-Klasse definiert explizit einen Migrationspfad zwischen startVersion
und endVersion
, indem die Methode Migration.migrate()
überschrieben wird. Fügen Sie die Migration
-Klassen mit der Methode addMigrations()
zu Ihrem Datenbank-Builder hinzu:
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 automatisierte Migrationen und für andere manuelle Migrationen verwenden. Wenn Sie sowohl eine automatisierte Migration als auch eine manuelle Migration für dieselbe Version definieren, verwendet Room die manuelle Migration.
Migrationen testen
Migrationen sind häufig komplex und eine falsch definierte Migration kann zum Absturz Ihrer Anwendung führen. Testen Sie Ihre Migrationen, um die Stabilität Ihrer Anwendung zu wahren. Room bietet ein room-testing
-Maven-Artefakt, das den Testprozess für automatisierte und manuelle Migrationen unterstützt. Damit dieses Artefakt funktioniert, müssen Sie zuerst das Schema Ihrer Datenbank exportieren.
Schemas exportieren
Room kann die Schemainformationen Ihrer Datenbank bei der Kompilierung in eine JSON-Datei exportieren. Die exportierten JSON-Dateien stellen den Schemaverlauf Ihrer Datenbank dar. Speichern Sie diese Dateien in Ihrem Versionsverwaltungssystem, damit Room zu Testzwecken niedrigere Versionen der Datenbank erstellen und die automatische Migration ermöglichen kann.
Schemaort mit Room Gradle-Plug-in festlegen
Wenn Sie die Room-Version 2.6.0 oder höher verwenden, können Sie das Room Gradle-Plug-in anwenden und mit der Erweiterung room
das Schemaverzeichnis angeben.
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-Typ unterscheidet, müssen Sie verschiedene Speicherorte angeben. Dazu verwenden Sie die schemaDirectory()
-Konfiguration mehrmals mit jeweils einem variantMatchName
als erstem Argument. Bei jeder Konfiguration können auf Basis eines einfachen Vergleichs mit dem Variantennamen eine oder mehrere Varianten zugeordnet werden.
Achten Sie darauf, dass diese Informationen vollständig sind und alle Varianten abdecken. Sie können auch eine schemaDirectory()
ohne variantMatchName
einfügen, um Varianten zu verarbeiten, die keiner der anderen Konfigurationen entsprechen. In einer Anwendung mit den beiden Build-Varianten demo
und full
sowie den Build-Typen debug
und release
sind beispielsweise folgende Konfigurationen gültig:
Cool
room {
// Applies to 'demoDebug' only
schemaLocation "demoDebug", "$projectDir/schemas/demoDebug"
// Applies to 'demoDebug' and 'demoRelease'
schemaLocation "demo", "$projectDir/schemas/demo"
// Applies to 'demoDebug' and 'fullDebug'
schemaLocation "debug", "$projectDir/schemas/debug"
// Applies to variants that aren't matched by other configurations.
schemaLocation "$projectDir/schemas"
}
Kotlin
room {
// Applies to 'demoDebug' only
schemaLocation("demoDebug", "$projectDir/schemas/demoDebug")
// Applies to 'demoDebug' and 'demoRelease'
schemaLocation("demo", "$projectDir/schemas/demo")
// Applies to 'demoDebug' and 'fullDebug'
schemaLocation("debug", "$projectDir/schemas/debug")
// Applies to variants that aren't matched by other configurations.
schemaLocation("$projectDir/schemas")
}
Schemastandort mit Option für Annotationsprozessor festlegen
Wenn Sie die Version 2.5.2 oder niedriger von Room oder das Room Gradle-Plug-in nicht nutzen, legen Sie den Speicherort des Schemas mit der Annotationsprozessoroption room.schemaLocation
fest.
Dateien in diesem Verzeichnis werden für einige Gradle-Aufgaben als Ein- und Ausgaben verwendet.
Damit inkrementelle und im Cache gespeicherte Builds korrekt und leistungsfähig sind, müssen Sie Gradle mit dem CommandLineArgumentProvider
von Gradle über dieses Verzeichnis informieren.
Kopieren Sie zuerst die unten gezeigte Klasse RoomSchemaArgProvider
in die Gradle-Build-Datei Ihres Moduls. Die Methode asArguments()
in der Beispielklasse übergibt room.schemaLocation=${schemaDir.path}
an KSP
. Wenn Sie KAPT
und javac
verwenden, ä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 angegebenen Schemaverzeichnis verwendet wird:
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 die Migrationen testen können, fügen Sie den Testabhängigkeiten das Maven-Artefakt androidx.room:room-testing
aus Room hinzu und fügen Sie den Speicherort des exportierten Schemas als Asset-Ordner hinzu:
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-Schnittstelle TestRule
, sodass es erstellte Datenbanken verwalten kann.
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, einen Test hinzuzufügen, der alle für die Datenbank Ihrer Anwendung definierten Migrationen abdeckt. Dadurch wird sichergestellt, dass es keine Diskrepanz zwischen einer kürzlich erstellten Datenbankinstanz und einer älteren Instanz gibt, die den definierten Migrationspfaden gefolgt ist.
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 Gerät auf die aktuelle Version findet, tritt ein IllegalStateException
auf. Wenn es akzeptabel ist, vorhandene Daten zu verlieren, wenn ein Migrationspfad fehlt, rufen Sie beim Erstellen der Datenbank die Builder-Methode fallbackToDestructiveMigration()
auf:
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 der Datenbank Ihrer Anwendung destruktiv neu zu erstellen, wenn eine inkrementelle Migration erforderlich ist und kein Migrationspfad definiert ist.
Wenn Sie möchten, dass in „Room“ nur in bestimmten Situationen auf die destruktive Neuerstellung zurückgegriffen werden soll, gibt es einige Alternativen zu fallbackToDestructiveMigration()
:
- Wenn bestimmte Versionen Ihres Schemaverlaufs Fehler verursachen, die sich nicht mit Migrationspfaden beheben lassen, verwenden Sie stattdessen
fallbackToDestructiveMigrationFrom()
. Diese Methode gibt an, dass Room nur bei der Migration von bestimmten Versionen auf die destruktive Neuerstellung zurückgreifen soll. - Wenn Room bei der Migration von einer höheren Datenbankversion zu einer niedrigeren Version nur auf die destruktive Neuerstellung zurückgreifen soll, verwenden Sie stattdessen
fallbackToDestructiveMigrationOnDowngrade()
.
Standardspaltenwerte beim Upgrade auf Raum 2.2.0 verarbeiten
In Raum 2.2.0 und höher können Sie mit der Annotation @ColumnInfo(defaultValue = "...")
einen Standardwert für eine Spalte definieren.
In Versionen vor 2.2.0 können Sie einen Standardwert für eine Spalte nur definieren, wenn Sie ihn direkt in einer ausgeführten SQL-Anweisung definieren. Dadurch wird ein Standardwert erstellt, den Room nicht bekannt ist. Wenn also eine Datenbank ursprünglich mit einer Version von Room erstellt wurde, die älter als Version 2.2.0 ist, müssen Sie für ein Upgrade Ihrer Anwendung auf Room 2.2.0 möglicherweise einen speziellen Migrationspfad für vorhandene Standardwerte angeben, die Sie ohne Verwendung von Room 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; }
Angenommen, Version 2 derselben Datenbank fügt eine neue Spalte NOT NULL
hinzu 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 Updates und Neuinstallationen der Anwendung. Da der Standardwert für die Spalte tag
nur im Migrationspfad von Version 1 zu Version 2 deklariert ist, haben Nutzer, die die Anwendung ab Version 2 installieren, nicht den Standardwert für tag
in ihrem Datenbankschema.
In den Versionen von Room (unter 2.2.0) sind diese Abweichungen harmlos. Wenn die Anwendung jedoch später auf Raum 2.2.0 oder höher aktualisiert und die Entitätsklasse Song
so geändert wird, dass sie mithilfe der Annotation @ColumnInfo
einen Standardwert für tag
enthält, kann der Raum diese Abweichung erkennen. Dies führt zu fehlgeschlagenen Schemavalidierungen.
Damit das Datenbankschema für alle Nutzer konsistent ist, wenn in Ihren früheren Migrationspfaden Spaltenstandardwerte deklariert werden, führen Sie beim ersten Upgrade Ihrer Anwendung für Raum 2.2.0 oder höher die folgenden Schritte aus:
- Geben Sie mithilfe der Annotation
@ColumnInfo
die Standardwerte von Spalten in ihren jeweiligen Entitätsklassen an. - Erhöhen Sie die Versionsnummer der Datenbank um 1.
- Legen Sie einen Migrationspfad zur neuen Version fest, in der die Strategie zum Löschen und Neuerstellen implementiert ist, 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"); } };