Esegui la migrazione del database delle stanze virtuali

Quando aggiungi e modifichi le funzionalità nell'app, devi modificare l'entità Room e le tabelle di database sottostanti per riflettere queste modifiche. È importante per conservare i dati utente già presenti nel database sul dispositivo quando un'app di aggiornamento modifica lo schema del database.

Room supporta opzioni sia automatiche che manuali per la migrazione incrementale. Le migrazioni automatiche funzionano per la maggior parte delle modifiche di base allo schema, ma potresti dover definire manualmente i percorsi di migrazione per modifiche più complesse.

Migrazioni automatiche

Per dichiarare una migrazione automatica tra due versioni del database, aggiungi un metodo @AutoMigration su autoMigrations proprietà 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 {
  ...
}

Specifiche della migrazione automatica

Se Room rileva modifiche ambigue allo schema e non può generare un di migrazione senza ulteriori input, genera un errore di compilazione e chiede all'utente per implementare AutoMigrationSpec Più comunemente, questo si verifica quando una migrazione riguarda uno dei seguenti elementi:

  • Eliminazione o ridenominazione di una tabella.
  • Eliminazione o ridenominazione di una colonna.

Puoi utilizzare AutoMigrationSpec per fornire alla stanza le informazioni aggiuntive che deve generare i percorsi di migrazione correttamente. Definisci una classe statica che implementa AutoMigrationSpec nella tua classe RoomDatabase e la annota con uno o più dei seguenti elementi:

Per utilizzare l'implementazione AutoMigrationSpec per una migrazione automatica, imposta la proprietà spec nell'annotazione @AutoMigration corrispondente:

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

Se la tua app deve svolgere più operazioni al termine della migrazione automatica, può implementare onPostMigrate() Se implementi questo metodo in AutoMigrationSpec, la stanza virtuale lo chiama dopo la migrazione automatizzata è completata.

Migrazioni manuali

Nei casi in cui una migrazione implica modifiche complesse allo schema, la stanza virtuale potrebbe non essere generare automaticamente un percorso di migrazione appropriato. Ad esempio, se decidi di suddividere i dati di una tabella in due tabelle, la stanza virtuale non può su come eseguire questa suddivisione. In casi come questi, devi manualmente per definire un percorso di migrazione, Migration.

Una classe Migration definisce esplicitamente un percorso di migrazione tra startVersion e endVersion mediante l'override di Migration.migrate() . Aggiungi le tue classi Migration al generatore di database utilizzando il 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();

Quando definisci i percorsi di migrazione, puoi utilizzare le migrazioni automatizzate per alcuni e migrazioni manuali per altri utenti. Se definisci sia un'automazione migrazione manuale e una migrazione manuale per la stessa versione, la stanza virtuale utilizza il migrazione.

Testa migrazioni

Le migrazioni sono spesso complesse e una migrazione definita in modo errato può causare l'arresto anomalo dell'app. Per preservare la stabilità della tua app, testa le migrazioni di Kubernetes. La stanza virtuale fornisce un artefatto Maven room-testing per aiutarti con il per le migrazioni manuali e automatiche. Affinché questo artefatto devi prima esportare lo schema del database.

Esporta schemi

La stanza può esportare le informazioni sullo schema del tuo database in un file JSON durante la compilazione nel tempo. I file JSON esportati rappresentano la cronologia degli schemi del database. Negozio questi file nel tuo sistema di controllo della versione in modo che la stanza virtuale possa creare versioni precedenti a scopo di test e per abilitare la generazione automatica della migrazione.

Imposta la posizione dello schema utilizzando il plug-in Room Gradle

Se utilizzi la versione della stanza 2.6.0 o successiva, puoi applicare con il plug-in Gradle Room e utilizzare room per specificare la directory dello schema.

Alla moda

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

Se lo schema del database è diverso in base alla variante, alla versione o alla build devi specificare località diverse utilizzando l'schemaDirectory() configurazione più volte, ciascuna con un valore variantMatchName come primo . Ogni configurazione può corrispondere a una o più varianti in base a semplici con il nome della variante.

Assicurati che siano esaustivi e coprano tutte le varianti. Puoi anche includere un schemaDirectory() senza variantMatchName per gestire le varianti non corrispondenti da una qualsiasi delle altre configurazioni. Ad esempio, in un'app con due build versioni demo e full e due tipi di build debug e release, di seguito sono riportate configurazioni valide:

Alla moda

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

Imposta la posizione dello schema utilizzando l'opzione del processore di annotazione

Se usi la versione 2.5.2 o versioni precedenti di Room, o se non usi Plug-in per Room Gradle, imposta la posizione dello schema utilizzando room.schemaLocation l'opzione del processore di annotazione.

I file in questa directory vengono utilizzati come input e output per alcune attività Gradle. Per la correttezza e le prestazioni delle build incrementali e memorizzate nella cache, devi usare Di Gradle CommandLineArgumentProvider per informare Gradle di questa directory.

Innanzitutto, copia il corso RoomSchemaArgProvider mostrato di seguito nel file di build Gradle. Il metodo asArguments() nella classe di esempio viene superato room.schemaLocation=${schemaDir.path} a KSP. Se utilizzi KAPT e javac, modifica questo valore in -Aroom.schemaLocation=${schemaDir.path}.

Alla moda

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

Quindi configura le opzioni di compilazione per utilizzare RoomSchemaArgProvider con directory schema specificata:

Alla moda

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

Testa una singola migrazione

Prima di poter testare le migrazioni, aggiungi il metodo androidx.room:room-testing artefatto Maven dalla stanza virtuale nel test delle dipendenze e aggiungi la posizione dello schema esportato come cartella di asset:

build.gradle

Alla moda

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

Il pacchetto di test fornisce MigrationTestHelper che può leggere i file di schema esportati. Il pacchetto implementa anche JUnità 4 TestRule a riga di comando, in modo da poter gestire i database creati.

L'esempio seguente mostra un test per una singola migrazione:

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

Testa tutte le migrazioni

Sebbene sia possibile testare una singola migrazione incrementale, consigliamo includere un test che copra tutte le migrazioni definite per per configurare un database. In questo modo, puoi garantire che non ci siano discrepanze tra un modello di attribuzione di database e un'istanza precedente che ha seguito la migrazione definita percorsi di addestramento.

L'esempio seguente mostra un test per tutte le migrazioni definite:

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

Gestire in modo corretto i percorsi di migrazione mancanti

Se la stanza virtuale non riesce a trovare un percorso di migrazione per eseguire l'upgrade di un database esistente su dispositivo alla versione corrente, IllegalStateException. Se è accettabile perdere i dati esistenti quando manca un percorso di migrazione, richiama il fallbackToDestructiveMigration() Builder quando crei il database:

Kotlin

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

Java

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

Questo metodo indica a Room di ricreare in modo distruttivo le tabelle nella quando deve eseguire una migrazione incrementale e non del percorso di migrazione definito.

Se vuoi che la stanza venga utilizzata solo in determinate aree situazioni, esistono alcune alternative a fallbackToDestructiveMigration():

  • Se versioni specifiche della cronologia degli schemi causano errori che non puoi risolvere con percorsi di migrazione, utilizza fallbackToDestructiveMigrationFrom() . Questo metodo indica che vuoi che la stanza ritorni su distruttiva solo quando si esegue la migrazione da versioni specifiche.
  • Se vuoi che Room torni alla modalità di svago distruttiva solo quando esegui la migrazione da una versione superiore a una precedente, utilizza fallbackToDestructiveMigrationOnDowngrade() .
di Gemini Advanced.

Gestire i valori predefiniti delle colonne quando si esegue l'upgrade alla stanza 2.2.0

Nella stanza 2.2.0 e successive, puoi definire un valore predefinito per una colonna utilizzando l'annotazione @ColumnInfo(defaultValue = "...") Nelle versioni precedenti alla 2.2.0, l'unico modo per definire un valore predefinito per un è definirla direttamente in un'istruzione SQL eseguita, che crea un valore predefinito che la stanza non è a conoscenza. Ciò significa che se un database originariamente creato da una versione di Stanza precedente alla 2.2.0, eseguendo l'upgrade dell'app Se usi Room 2.2.0, potresti dover fornire un percorso di migrazione valori predefiniti esistenti che hai definito senza utilizzare le API Room.

Ad esempio, supponiamo che la versione 1 di un database definisca un'entità 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;
}

Supponiamo anche che la versione 2 dello stesso database aggiunga una nuova colonna NOT NULL e definisce un percorso di migrazione dalla versione 1 alla versione 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 ''");
    }
};

Questo causa una discrepanza nella tabella sottostante tra gli aggiornamenti e i valori delle installazioni dell'app. Perché il valore predefinito per la colonna tag è solo dichiarate nel percorso di migrazione dalla versione 1 alla versione 2, tutti gli utenti installa l'app a partire dalla versione 2 non hanno il valore predefinito per tag nello schema del database.

Nelle versioni di Room precedenti alla 2.2.0, questa discrepanza è innocua. Tuttavia, se l'app esegue in seguito l'upgrade per utilizzare la stanza 2.2.0 o versioni successive e cambia l'entità Song per includere un valore predefinito per tag utilizzando il Annotazione @ColumnInfo, Stanza virtuale potrà quindi notare questa discrepanza. Questo determina uno schema non riuscito convalide.

Per garantire che lo schema del database sia coerente per tutti gli utenti quando la colonna i valori predefiniti sono dichiarati nei percorsi di migrazione precedenti, segui questi passaggi La prima volta che esegui l'upgrade dell'app per utilizzare la Stanza 2.2.0 o versioni successive:

  1. Dichiara i valori predefiniti delle colonne nelle rispettive classi di entità utilizzando Annotazione @ColumnInfo.
  2. Aumenta il numero di versione del database di 1.
  3. Definisci un percorso di migrazione alla nuova versione che implementi le funzionalità di rilascio e strategia di nuova creazione per aggiungere i valori predefiniti necessari alle colonne esistenti.
di Gemini Advanced.

L'esempio seguente illustra questo processo:

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