Raumdatenbank migrieren

Wenn Sie Ihrer App Funktionen hinzufügen und Änderungen vornehmen, müssen Sie Ihre Room-Entitätsklassen und die zugrunde liegenden Datenbanktabellen entsprechend ändern. Es ist wichtig, dass Nutzerdaten, die sich bereits in der Datenbank auf dem Gerät befinden, beibehalten werden, wenn durch ein App-Update das Datenbankschema geändert wird.

Room unterstützt sowohl automatische als auch manuelle Optionen für die inkrementelle Migration. Automatische Migrationen funktionieren bei den meisten grundlegenden Schemaänderungen. Bei komplexeren Ä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 autoMigrations Eigenschaft in @Database die @AutoMigration Annotation hinzu:

// 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
}

Spezifikationen für die automatische Migration

Wenn Room mehrdeutige Schemaänderungen erkennt und ohne weitere Eingaben keinen Migrationsplan generieren kann, wird ein Kompilierungsfehler ausgelöst und Sie müssen eine AutoMigrationSpec-Implementierung bereitstellen. Dies tritt am häufigsten auf, wenn eine Migration Folgendes umfasst:

  • Löschen oder Umbenennen einer Tabelle
  • Löschen oder Umbenennen einer Spalte

Mit AutoMigrationSpec können Sie Room die zusätzlichen Informationen geben, die zum korrekten Generieren von Migrationspfaden erforderlich sind. Definieren Sie in Ihrer RoomDatabase-Klasse eine Klasse, die AutoMigrationSpec implementiert, und versehen Sie sie mit einer oder mehreren der folgenden Annotationen:

Wenn Sie die AutoMigrationSpec-Implementierung für eine automatische Migration verwenden möchten, legen Sie die Eigenschaft spec in der entsprechenden @AutoMigration-Annotation fest:

@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

Wenn Ihre App nach Abschluss der automatischen Migration weitere Aufgaben ausführen muss, können Sie implementieren onPostMigrate. Wenn Sie diese Funktion in Ihrer AutoMigrationSpec implementieren, ruft Room sie nach Abschluss der automatischen Migration auf.

Manuelle Migrationen

Wenn eine Migration komplexe Schemaänderungen umfasst, 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 bestimmen, wie diese Aufteilung erfolgen soll. In diesen Fällen müssen Sie einen Migrationspfad manuell definieren, indem Sie eine Migration Klasse implementieren.

Eine Migration Klasse definiert explizit einen Migrationspfad zwischen einer startVersion und einer endVersion, indem die Funktion migrate überschrieben wird. Fügen Sie Ihre Migration Klassen mit der addMigrations Funktion zum Datenbank-Builder hinzu:

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()

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 zum Absturz Ihrer App führen. Um die Stabilität Ihrer App zu gewährleisten, sollten Sie Ihre Migrationen testen. Room bietet ein room3-testing-Maven-Artefakt, das Sie beim Testen sowohl automatischer als auch manueller Migrationen unterstützt. Damit dieses Artefakt funktioniert, müssen Sie zuerst das Schema Ihrer Datenbank exportieren.

Schemas exportieren

Room exportiert die Schemainformationen Ihrer Datenbank zur Kompilierungszeit in eine JSON-Datei. Die exportierten JSON-Dateien stellen den Schemaverlauf Ihrer Datenbank dar. Speichern Sie diese Dateien in Ihrem Versionsverwaltungssystem, damit Sie niedrigere Versionen der Datenbank für Tests neu erstellen und die automatische Migrationsgenerierung unterstützen können.

Schemaort mit dem Room-Gradle-Plug-in festlegen

Wenn Sie das Schemaverzeichnis angeben möchten, wenden Sie das Room-Gradle-Plug-in an und verwenden Sie die room3 Erweiterung.

Groovy

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

room3 {
  schemaDirectory("$projectDir/schemas")
}

Wenn sich Ihr Datenbankschema je nach Variante, Flavor oder Build-Typ unterscheidet, müssen Sie verschiedene Orte angeben. Verwenden Sie dazu die Konfiguration schemaDirectory mehrmals, jeweils mit einem variantMatchName als erstes Argument. Jede Konfiguration kann mit einer oder mehreren Varianten übereinstimmen, basierend auf einem einfachen Vergleich mit dem Variantennamen.

Achten Sie darauf, dass diese umfassend sind und alle Varianten abdecken. Sie können auch ein schemaDirectory() ohne variantMatchName einfügen, um Varianten zu verarbeiten, die mit keiner der anderen Konfigurationen übereinstimmen. In einer App mit zwei Build-Flavors (demo und full) und zwei Build-Typen (debug und release) sind beispielsweise die folgenden Konfigurationen gültig:

Groovy

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")
}

Schemaort mit der Option des Annotation Processors festlegen

Wenn Sie das Room-Gradle-Plug-in nicht verwenden, legen Sie den Schemaort mit der Option room.schemaLocation des Annotation Processors fest.

Gradle verwendet Dateien in diesem Verzeichnis als Eingaben und Ausgaben für einige Gradle-Aufgaben. Für die Korrektheit und Leistung von inkrementellen und zwischengespeicherten Builds müssen Sie Gradle's CommandLineArgumentProvider verwenden, um Gradle über dieses Verzeichnis zu informieren.

Kopieren Sie zuerst die folgende RoomSchemaArgProvider-Klasse in die Gradle-Build-Datei Ihres Moduls. Die Funktion 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}.

Groovy

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}")
  }
}

Konfigurieren Sie dann die Kompilierungsoptionen so, dass RoomSchemaArgProvider mit dem angegebenen Schemaverzeichnis verwendet wird:

Groovy

ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

Kotlin

ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

Einzelne Migration testen

Bevor Sie Ihre Migrationen testen können, fügen Sie das Artefakt androidx.room3:room3-testing zu Ihren Testabhängigkeiten hinzu und fügen Sie den Ort des exportierten Schemas als Asset-Verzeichnis hinzu:

Groovy

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")
}

Das Testpaket enthält eine MigrationTestHelper-Klasse, mit der exportierte Schemadateien gelesen werden können. Das Paket implementiert auch die JUnit4 TestRule-Schnittstelle, um erstellte Datenbanken zu verwalten.

Das folgende Beispiel zeigt einen Test für eine einzelne Migration:

@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()
    }
}

Alle Migrationen testen

Sie können zwar eine einzelne inkrementelle Migration testen, sollten aber einen Test einfügen, der alle für die Datenbank Ihrer App definierten Migrationen abdeckt. So können Sie sicherstellen, dass es keine Diskrepanz zwischen einer neu erstellten Datenbankinstanz und einer früheren Instanz gibt, die den definierten Migrationspfaden gefolgt ist.

Das folgende Beispiel zeigt einen Test für alle definierten Migrationen:

@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()
    }
}

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 ein Migrationspfad fehlt, rufen Sie beim Erstellen der Datenbank die fallbackToDestructiveMigration Builder-Funktion auf:

Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name")
        .fallbackToDestructiveMigration()
        .build()

Diese Funktion konfiguriert Room so, dass die Tabellen in der Datenbank Ihrer App destruktiv neu erstellt werden, wenn eine inkrementelle Migration ausgeführt werden muss und kein Migrationspfad definiert ist.

Wenn Sie nur in bestimmten Situationen auf die destruktive Neuerstellung zurückgreifen möchten, verwenden Sie eine der folgenden Alternativen zu fallbackToDestructiveMigration:

  • Wenn bestimmte Versionen Ihres Schemaverlaufs Fehler verursachen, die Sie nicht mit Migrationspfaden beheben können , verwenden Sie fallbackToDestructiveMigrationFrom stattdessen. Diese Funktion gibt an, dass Room nur bei der Migration von bestimmten Versionen auf die destruktive Neuerstellung zurückgreifen soll.
  • Wenn Room nur bei der Migration von einer höheren zu einer niedrigeren Datenbankversion auf die destruktive Neuerstellung zurückgreifen soll, verwenden Sie stattdessen fallbackToDestructiveMigrationOnDowngrade.