Raumdatenbank migrieren

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:

  1. Geben Sie mithilfe der Methode @ColumnInfo-Anmerkung.
  2. Erhöhen Sie die Versionsnummer der Datenbank um 1.
  3. 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");
    }
};