Migracja bazy danych sal

Podczas dodawania i modyfikowania funkcji aplikacji musisz zmodyfikować element pokoju i bazowych tabel baz danych, aby odzwierciedlić te zmiany. To ważne w celu zachowania danych użytkownika, które są już w bazie danych na urządzeniu, gdy aplikacja update zmienia schemat bazy danych.

W przypadku pokoi obsługiwane są zarówno automatyczne, jak i ręczne opcje migracji przyrostowej. Automatyczne migracje działają w przypadku większości podstawowych zmian schematu, ale może być konieczne możesz ręcznie zdefiniować ścieżki migracji w przypadku bardziej złożonych zmian.

Migracje automatyczne

Aby zadeklarować automatyczną migrację między 2 wersjami bazy danych, dodaj atrybut @AutoMigration adnotacja do autoMigrations właściwość w @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 {
  ...
}

Specyfikacje automatycznej migracji

Jeśli Room wykryje niejednoznaczne zmiany schematu i nie będzie w stanie wygenerować bez wprowadzania dodatkowych danych, generuje błąd podczas kompilowania i pyta aby zaimplementować AutoMigrationSpec Dzieje się tak najczęściej wtedy, gdy migracja obejmuje jedną z tych sytuacji:

  • usunięcie tabeli lub zmianę jej nazwy,
  • usunięcie kolumny lub zmianę jej nazwy,

Za pomocą AutoMigrationSpec możesz przekazać do pokoju dodatkowe informacje, musi prawidłowo wygenerować ścieżki migracji. Zdefiniuj klasę statyczną, która implementuje AutoMigrationSpec w klasie RoomDatabase i dodaje do niej adnotację któreś z tych problemów:

Aby użyć implementacji AutoMigrationSpec do automatycznej migracji, ustaw właściwość spec w odpowiedniej adnotacji @AutoMigration:

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 { }
  ...
}

Jeśli po zakończeniu automatycznej migracji aplikacja musi wykonać jeszcze więcej zadań, może zastosować onPostMigrate() Jeśli wdrożysz tę metodę w swoim obiekcie AutoMigrationSpec, sala wywoła ją po po zakończeniu automatycznej migracji.

Migracje ręczne

Jeśli migracja obejmuje złożone zmiany schematu, aby automatycznie wygenerować odpowiednią ścieżkę migracji. Na przykład, jeśli Jeśli postanowisz podzielić dane w tabeli na dwie tabele, pokój nie wie, jak przeprowadzić ten podział. W takich przypadkach musisz ręcznie zdefiniować ścieżkę migracji, implementując Migration.

Klasa Migration wyraźnie określa ścieżkę migracji między startVersion i endVersion, zastępując wartości Migration.migrate() . Dodaj klasy Migration do kreatora baz danych za pomocą 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();

Podczas definiowania ścieżek migracji możesz użyć automatycznych migracji oraz przeprowadzić migrację ręczną dla innych. Jeśli zdefiniujesz zarówno tag automatyczny, migracji i migracji ręcznej dla tej samej wersji, to pokój korzysta z migracji danych.

Migracje testowe

Migracje są często złożone, a ich nieprawidłowe zdefiniowanie może do awarii. Aby zachować stabilność aplikacji, przetestuj ją migracji. W sali dostępny jest artefakt Maven room-testing, który pomaga w automatycznego i ręcznego testowania migracji. Dla tego artefaktu do musisz najpierw wyeksportować schemat bazy danych.

Eksportuj schematy

Sala może wyeksportować informacje o schemacie bazy danych do pliku JSON na stronie build obecnie się znajdujesz. Wyeksportowane pliki JSON reprezentują historię schematu bazy danych. Sklep tych plików w systemie kontroli wersji, aby pokój mógł tworzyć niższe wersje bazy danych do celów testowych oraz do automatycznego generowania migracji.

Ustawianie lokalizacji schematu za pomocą wtyczki Room Gradle

Jeśli korzystasz z sali w wersji 2.6.0 lub nowszej, możesz zastosować wtyczki do obsługi sali Gradle i użyj wtyczki room określające katalog schematu.

Odlotowe

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

Jeśli schemat bazy danych różni się w zależności od wersji, smaku lub kompilacji musisz określić różne lokalizacje za pomocą atrybutu schemaDirectory() konfiguracji kilka razy, przy czym w każdej z nich jako pierwszym ustawiono variantMatchName . Każda konfiguracja może pasować do co najmniej jednego wariantu na podstawie prostych w porównaniu z nazwą wariantu.

Upewnij się, że są one wyczerpujące i obejmują wszystkie wersje. Możesz też dodać atrybut schemaDirectory() bez variantMatchName, aby obsługiwać niedopasowane warianty przez dowolną z pozostałych konfiguracji. Na przykład w aplikacji z 2 kompilacjami smaki demo i full oraz 2 typy kompilacji debug i release, to są prawidłowe konfiguracje:

Odlotowe

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

Ustaw lokalizację schematu za pomocą opcji procesora adnotacji

Jeśli korzystasz z wersji 2.5.2 lub starszej pokoju lub nie używasz Wtyczka Room Gradle, ustaw lokalizację schematu za pomocą interfejsu room.schemaLocation procesora adnotacji.

Pliki w tym katalogu są używane jako dane wejściowe i wyjściowe w niektórych zadaniach Gradle. Aby uzyskać prawidłowość i wydajność kompilacji przyrostowych i zapisanych w pamięci podręcznej, musisz użyć Gradle CommandLineArgumentProvider aby poinformować Gradle o tym katalogu.

Najpierw skopiuj widoczne poniżej zajęcia RoomSchemaArgProvider do modułu Plik kompilacji Gradle. Metoda asArguments() w przykładowej klasie przekazuje room.schemaLocation=${schemaDir.path} do KSP. Jeśli korzystasz z KAPT i javac, zmień tę wartość na -Aroom.schemaLocation=${schemaDir.path}.

Odlotowe

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

Następnie skonfiguruj opcje kompilacji, aby używać polecenia RoomSchemaArgProvider z parametrem określony katalog schematu:

Odlotowe

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

Testowanie pojedynczej migracji

Aby przetestować migracje, dodaj do androidx.room:room-testing artefakt Maven z pokoju do testu zależności i dodaj lokalizację wyeksportowanego schematu jako folder zasobów:

build.gradle

Odlotowe

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

Pakiet testowy zawiera: MigrationTestHelper klasy, która może odczytywać wyeksportowane pliki schematu. Pakiet wykorzystuje również JUnit4 TestRule do zarządzania utworzonymi bazami danych.

Ten przykład przedstawia test pojedynczej migracji:

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

Testowanie wszystkich migracji

Mimo że można przetestować pojedynczą migrację przyrostową, zalecamy uwzględnić test obejmujący wszystkie migracje zdefiniowane dla w bazie danych. Pomoże to uniknąć rozbieżności między niedawno utworzonymi instancja bazy danych oraz starsza instancja, która nastąpiła po zdefiniowanej migracji ścieżek konwersji.

Poniższy przykład przedstawia test dla wszystkich zdefiniowanych migracji:

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

Bezpłatna obsługa brakujących ścieżek migracji

Jeśli Sala nie może znaleźć ścieżki migracji do uaktualnienia istniejącej bazy danych do aktualnej wersji, IllegalStateException ma miejsce. Jeśli można utracić istniejące dane, jeśli brakuje ścieżki migracji, wywołanie fallbackToDestructiveMigration() konstruktora podczas tworzenia bazy danych:

Kotlin

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .fallbackToDestructiveMigration()
        .build()

Java

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .fallbackToDestructiveMigration()
        .build();

Dzięki tej metodzie Room może niszczycielsko odtworzyć tabele w pliku w bazie danych, gdy musi przeprowadzić migrację przyrostową, i nie jest zdefiniowaną ścieżkę migracji.

Jeśli chcesz, aby pokój powrócił do destrukcyjnej rekreacji w określonych sytuacji, jednak zamiast fallbackToDestructiveMigration() możesz zastosować kilka rozwiązań:

  • Jeśli określone wersje historii schematu powodują błędy, których nie możesz naprawić ze ścieżkami migracji, użyj funkcji fallbackToDestructiveMigrationFrom() . Ta metoda wskazuje, że chcesz, aby pokój zmienił się w niszczycielski odtwarzania tylko w przypadku migracji z określonych wersji.
  • Jeśli chcesz, aby pokój powrócił do stanu destrukcyjnego tylko podczas migracji z wyższej wersji bazy danych na niższą, użyj fallbackToDestructiveMigrationOnDowngrade() .

Przejście na pokój 2.2.0 obsługuje wartości domyślne kolumn

W pokoju 2.2.0 i nowszych możesz zdefiniować domyślną wartość kolumny za pomocą funkcji adnotacja @ColumnInfo(defaultValue = "...") W wersjach starszych niż 2.2.0 jedynym sposobem zdefiniowania wartości domyślnej dla jest zdefiniowanie jej bezpośrednio w wykonanej instrukcji SQL, która tworzy wartość domyślna, o której Sala nie wie. Oznacza to, że jeśli baza danych to została pierwotnie utworzona w Pokoju w wersji starszej niż 2.2.0, po przejściu na jeśli używasz wersji Room 2.2.0, może być konieczne podanie specjalnej ścieżki migracji zdefiniowane przez Ciebie wartości domyślne bez korzystania z interfejsów API sal.

Załóżmy na przykład, że wersja 1 bazy danych definiuje encję Song:

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

Załóżmy też, że wersja 2 tej samej bazy danych dodaje nową kolumnę NOT NULL i określa ścieżkę migracji z wersji 1 do wersji 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 ''");
    }
};

Powoduje to rozbieżność między aktualizacjami a aktualnymi danymi w tabeli. instalacji aplikacji. Ponieważ domyślna wartość w kolumnie tag to tylko zadeklarowane w ścieżce migracji z wersji 1 do wersji 2, wszyscy użytkownicy, którzy instalowanie aplikacji od wersji 2 nie ma domyślnej wartości parametru tag w schemacie bazy danych.

W wersjach pokoju starszych niż 2.2.0 ta rozbieżność jest nieszkodliwa. Jeśli jednak aplikacja zostanie później uaktualniona do pomieszczenia 2.2.0 lub nowszego i zmieni się element Song class, aby uwzględnić domyślną wartość dla tag za pomocą funkcji Adnotacja @ColumnInfo, sala zobaczy tę rozbieżność. Powoduje to nieprawidłowy schemat weryfikacji danych.

Aby mieć pewność, że schemat bazy danych jest spójny u wszystkich użytkowników, gdy kolumna wartości domyślne są zadeklarowane we wcześniejszych ścieżkach migracji, wykonaj te czynności przy pierwszym uaktualnieniu aplikacji do pokoju 2.2.0 lub nowszego:

  1. Zadeklaruj domyślne wartości kolumn w odpowiednich klasach encji za pomocą @ColumnInfo adnotacja.
  2. Zwiększ numer wersji bazy danych o 1.
  3. Zdefiniuj ścieżkę migracji do nowej wersji, która implementuje mechanizm usuwania odtworzyć strategię aby dodać niezbędne wartości domyślne do istniejących kolumn.

Następujący przykład ilustruje ten proces:

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