Wenn Sie Funktionen in Ihrer App hinzufügen und ändern, müssen Sie Ihre Room-Entitätsklassen und die zugrunde liegenden Datenbanktabellen entsprechend anpassen. Es ist wichtig, dass Nutzerdaten, die sich bereits in der On-Device-Datenbank befinden, erhalten bleiben, wenn durch ein App-Update das Datenbankschema geändert wird.
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 automatische Migration zwischen zwei Datenbankversionen deklarieren möchten, fügen Sie der Eigenschaft autoMigrations
in @Database
die Annotation @AutoMigration
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 Kompilierungszeitfehler ausgegeben und Sie werden aufgefordert, ein AutoMigrationSpec
zu implementieren.
Am häufigsten tritt dies auf, wenn bei einer Migration einer der folgenden Punkte zutrifft:
- Eine Tabelle wird gelöscht oder umbenannt.
- Spalte löschen oder umbenennen
Mit AutoMigrationSpec
können Sie Room die zusätzlichen Informationen geben, die für die korrekte Generierung von Migrationspfaden erforderlich sind. Definieren Sie in Ihrer RoomDatabase
-Klasse eine statische Klasse, die AutoMigrationSpec
implementiert, und versehen Sie sie mit einem oder mehreren der folgenden Elemente:
Wenn Sie die AutoMigrationSpec
-Implementierung für eine automatische Migration verwenden möchten, legen Sie die spec
-Eigenschaft 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 App nach Abschluss der automatischen Migration weitere Aufgaben ausführen muss, können Sie onPostMigrate()
implementieren.
Wenn Sie diese Methode in Ihrem AutoMigrationSpec
implementieren, ruft Room sie nach Abschluss der automatischen Migration auf.
Manuelle Migrationen
Wenn bei einer Migration komplexe Schemaänderungen erforderlich sind, kann Room möglicherweise keinen geeigneten Migrationspfad automatisch generieren. Wenn Sie beispielsweise die Daten in einer Tabelle in zwei Tabellen aufteilen möchten, kann Room nicht erkennen, wie diese Aufteilung erfolgen soll. In solchen Fällen müssen Sie einen Migrationspfad manuell definieren, indem Sie eine Migration
-Klasse implementieren.
Eine Migration
-Klasse definiert explizit einen Migrationspfad zwischen einem startVersion
und einem endVersion
, indem sie die Methode Migration.migrate()
überschreibt. Fügen Sie Ihrem Datenbank-Builder Ihre Migration
-Klassen mit der Methode addMigrations()
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 automatische und für andere manuelle Migrationen verwenden. Wenn Sie sowohl eine automatische als auch eine manuelle Migration für dieselbe Version definieren, verwendet Room die manuelle Migration.
Migrationen testen
Migrationen sind oft komplex und eine falsch definierte Migration kann dazu führen, dass Ihre App abstürzt. Um die Stabilität Ihrer App zu gewährleisten, sollten Sie Ihre Migrationen testen. 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.
Export schemas
Room kann die Schemainformationen Ihrer Datenbank zur Kompilierzeit in eine JSON-Datei exportieren. Die exportierten JSON-Dateien stellen den Schemaverlauf Ihrer Datenbank dar. Speichern Sie diese Dateien in Ihrem Versionsverwaltungssystem, damit Room niedrigere Versionen der Datenbank für Testzwecke erstellen und die automatische Migration generieren kann.
Schema-Speicherort mit dem Room-Gradle-Plug-in festlegen
Wenn Sie Room-Version 2.6.0 oder höher verwenden, können Sie das Room-Gradle-Plug-in anwenden und die Erweiterung room
verwenden, um das Schemabeziehnis anzugeben.
Groovy
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. Verwenden Sie dazu die schemaDirectory()
-Konfiguration mehrmals, jeweils mit einem variantMatchName
als erstem Argument. Jede Konfiguration kann anhand eines einfachen Vergleichs mit dem Variantennamen einer oder mehreren Varianten entsprechen.
Achten Sie darauf, dass sie vollständig sind und alle Varianten abdecken. Sie können auch ein schemaDirectory()
ohne variantMatchName
einfügen, um Varianten zu verarbeiten, die keiner der anderen Konfigurationen entsprechen. In einer App mit zwei Build-Varianten demo
und full
und zwei Build-Typen debug
und release
sind beispielsweise die folgenden Konfigurationen gültig:
Groovy
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")
}
Schemaspeicherort mit der Option des Annotationsprozessors festlegen
Wenn Sie Version 2.5.2 oder niedriger von Room verwenden oder das Room-Gradle-Plug-in nicht verwenden, legen Sie den Schemaspeicherort mit der Annotationsprozessoroption room.schemaLocation
fest.
Dateien in diesem Verzeichnis werden als Ein- und Ausgaben für einige Gradle-Aufgaben verwendet.
Damit inkrementelle und im Cache gespeicherte Builds korrekt ausgeführt werden und eine gute Leistung erzielen, müssen Sie Gradle mit dem CommandLineArgumentProvider
von Gradle über dieses Verzeichnis informieren.
Kopieren Sie zuerst die unten gezeigte RoomSchemaArgProvider
-Klasse in die Gradle-Build-Datei Ihres Moduls. Die asArguments()
-Methode 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}
.
Groovy
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, um RoomSchemaArgProvider
mit dem angegebenen Schemaverzeichnis zu verwenden:
Groovy
// 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 das Maven-Artefakt androidx.room:room-testing
aus Room zu Ihren Testabhängigkeiten hinzu und fügen Sie den Speicherort des exportierten Schemas als Asset-Ordner hinzu:
build.gradle
Groovy
android { ... sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... androidTestImplementation "androidx.room:room-testing:2.7.2" }
Kotlin
android { ... sourceSets { // Adds exported schema location as test app assets. getByName("androidTest").assets.srcDir("$projectDir/schemas") } } dependencies { ... testImplementation("androidx.room:room-testing:2.7.2") }
Das Testpaket enthält die Klasse MigrationTestHelper
, mit der exportierte Schemadateien gelesen werden können. Das Paket implementiert auch die JUnit4-Schnittstelle TestRule
, sodass es erstellte Datenbanken verwalten kann.
Im folgenden Beispiel wird ein Test für eine einzelne Migration gezeigt:
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 einzuschließen, der alle für die Datenbank Ihrer App definierten Migrationen abdeckt. So wird sichergestellt, dass es keine Diskrepanz zwischen einer neu erstellten Datenbankinstanz und einer älteren Instanz gibt, die den definierten Migrationspfaden gefolgt ist.
Im folgenden Beispiel wird ein Test für alle definierten Migrationen gezeigt:
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}; }
Fehlende Migrationspfade ordnungsgemäß verarbeiten
Wenn Room keinen Migrationspfad findet, um eine vorhandene Datenbank auf einem Gerät auf die aktuelle Version zu aktualisieren, tritt ein IllegalStateException
auf. Wenn es akzeptabel ist, dass vorhandene Daten verloren gehen, wenn kein Migrationspfad vorhanden ist, rufen Sie die Builder-Methode fallbackToDestructiveMigration()
beim Erstellen der Datenbank 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 App neu zu erstellen, wenn eine inkrementelle Migration erforderlich ist und kein Migrationspfad definiert ist.
Wenn Sie nur in bestimmten Situationen auf die destruktive Neuerstellung zurückgreifen möchten, gibt es einige Alternativen zu fallbackToDestructiveMigration()
:
- Wenn bestimmte Versionen Ihres Schemas Fehler verursachen, die Sie nicht mit Migrationspfaden beheben können, verwenden Sie stattdessen
fallbackToDestructiveMigrationFrom()
. Diese Methode gibt an, dass Room nur dann auf die destruktive Neuerstellung zurückgreifen soll, wenn die Migration von bestimmten Versionen erfolgt. - Wenn Room nur dann auf das destruktive Neuerstellen zurückgreifen soll, wenn von einer höheren Datenbankversion zu einer niedrigeren migriert wird, verwenden Sie stattdessen
fallbackToDestructiveMigrationOnDowngrade()
.
Standardwerte für Spalten beim Upgrade auf Room 2.2.0 verarbeiten
In Room 2.2.0 und höher können Sie einen Standardwert für eine Spalte mit der Annotation @ColumnInfo(defaultValue = "...")
definieren.
In Versionen vor 2.2.0 kann ein Standardwert für eine Spalte nur direkt in einer ausgeführten SQL-Anweisung definiert werden. Dadurch wird ein Standardwert erstellt, der Room nicht bekannt ist. Wenn eine Datenbank also ursprünglich mit einer Room-Version unter 2.2.0 erstellt wurde, müssen Sie möglicherweise einen speziellen Migrationspfad für vorhandene Standardwerte angeben, die Sie ohne Verwendung von Room-APIs definiert haben, wenn Sie Ihre App auf Room 2.2.0 aktualisieren.
Angenommen, in Version 1 einer Datenbank wird die Entität Song
definiert:
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, in Version 2 derselben Datenbank wird eine neue Spalte NOT NULL
hinzugefügt und ein Migrationspfad von Version 1 zu Version 2 definiert:
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 Diskrepanz in der zugrunde liegenden Tabelle zwischen Updates und Neuinstallationen der App. Da der Standardwert für die Spalte tag
nur im Migrationspfad von Version 1 zu Version 2 deklariert wird, haben Nutzer, die die App ab Version 2 installieren, den Standardwert für tag
nicht in ihrem Datenbankschema.
In Room-Versionen unter 2.2.0 ist diese Abweichung harmlos. Wenn die App jedoch später auf Room 2.2.0 oder höher aktualisiert wird und die Song
-Entitätsklasse so geändert wird, dass sie einen Standardwert für tag
mit der Annotation @ColumnInfo
enthält, kann Room diese Diskrepanz erkennen. Das führt dazu, dass die Schemavalidierung fehlschlägt.
Damit das Datenbankschema für alle Nutzer einheitlich ist, wenn in Ihren früheren Migrationspfaden Spaltenstandardwerte deklariert werden, gehen Sie beim ersten Upgrade Ihrer App auf Room 2.2.0 oder höher so vor:
- Deklarieren Sie Standardwerte für Spalten in den entsprechenden Entitätsklassen mit der Annotation
@ColumnInfo
. - Erhöhen Sie die Datenbankversionsnummer um 1.
- Definieren Sie einen Migrationspfad zur neuen Version, in dem die Strategie zum Löschen und Neuerstellen implementiert wird, 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"); } };