Saat menambahkan dan mengubah fitur dalam aplikasi, Anda harus mengubah class entity Room dan tabel database pokok untuk mencerminkan perubahan ini. Penting untuk menyimpan data pengguna yang sudah ada dalam database pada perangkat saat update aplikasi mengubah skema database.
Room mendukung opsi otomatis dan manual untuk migrasi inkremental. Migrasi otomatis berfungsi untuk sebagian besar perubahan skema dasar, tetapi Anda mungkin perlu menentukan jalur migrasi secara manual untuk perubahan yang lebih kompleks.
Migrasi otomatis
Untuk mendeklarasikan migrasi otomatis antara dua versi database, tambahkan
anotasi @AutoMigration
ke
properti autoMigrations
di @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 { ... }
Spesifikasi migrasi otomatis
Jika Room mendeteksi adanya perubahan skema yang ambigu dan tidak dapat menghasilkan
paket migrasi tanpa input lain, Room akan menampilkan error waktu kompilasi dan meminta Anda
untuk mengimplementasikan
AutoMigrationSpec
.
Masalah ini paling sering terjadi saat migrasi melibatkan salah satu dari hal berikut:
- Menghapus atau mengganti nama tabel.
- Menghapus atau mengganti nama kolom.
Anda dapat menggunakan AutoMigrationSpec
untuk memberi informasi tambahan yang diperlukan Room
untuk membuat jalur migrasi dengan benar. Tentukan class statis yang mengimplementasikan AutoMigrationSpec
di class RoomDatabase
Anda, lalu anotasikan dengan
satu atau beberapa hal berikut:
Agar dapat menggunakan implementasi AutoMigrationSpec
untuk migrasi otomatis, tetapkan
properti spec
dalam anotasi @AutoMigration
yang sesuai:
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 { } ... }
Jika aplikasi perlu melakukan lebih banyak hal setelah migrasi otomatis selesai, Anda
dapat mengimplementasikan
onPostMigrate()
.
Jika Anda mengimplementasikan metode ini di AutoMigrationSpec
, Room akan memanggilnya setelah
migrasi otomatis selesai.
Migrasi manual
Jika migrasi melibatkan perubahan skema yang kompleks, Room mungkin tidak
dapat menghasilkan jalur migrasi yang sesuai secara otomatis. Misalnya, jika
Anda memutuskan untuk membagi data dalam tabel menjadi dua tabel, Room tidak dapat mengetahui
cara melakukan pemisahan ini. Dalam kasus seperti ini, Anda harus menentukan
jalur migrasi secara manual dengan mengimplementasikan class
Migration
.
Class Migration
secara eksplisit menentukan jalur migrasi antara
startVersion
dan endVersion
dengan mengganti
metode
Migration.migrate()
. Tambahkan class Migration
ke builder database Anda menggunakan
metode
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();
Saat menentukan jalur migrasi, Anda dapat menggunakan migrasi otomatis untuk sebagian versi dan juga migrasi manual untuk sebagian versi lainnya. Jika Anda menentukan migrasi otomatis dan migrasi manual untuk versi yang sama, Room akan memilih menggunakan migrasi manual.
Menguji migrasi
Migrasi sering kali rumit, dan migrasi yang tidak ditetapkan dengan benar dapat menyebabkan
aplikasi Anda error. Untuk mempertahankan stabilitas aplikasi, uji
migrasi. Room menyediakan artefak Maven room-testing
untuk membantu proses
pengujian migrasi otomatis dan manual. Agar artefak ini berfungsi dengan baik, Anda harus mengekspor skema
database terlebih dahulu.
Mengekspor skema
Room dapat mengekspor informasi skema database Anda ke dalam file JSON pada waktu kompilasi. File JSON yang diekspor merepresentasikan histori skema database Anda. Menyimpan file ini di sistem kontrol versi agar Room dapat membuat versi {i>database<i} untuk tujuan pengujian dan untuk mengaktifkan pembuatan migrasi otomatis.
Menetapkan lokasi skema menggunakan Plugin Room Gradle
Jika menggunakan Room versi 2.6.0 atau yang lebih baru, Anda dapat menerapkan
Plugin Room Gradle dan gunakan
room
untuk menentukan direktori skema.
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Jika skema database Anda berbeda berdasarkan varian, ragam, atau build
Anda harus menentukan lokasi yang berbeda menggunakan schemaDirectory()
beberapa kali, masing-masing dengan variantMatchName
sebagai
argumen. Setiap konfigurasi dapat mencocokkan satu atau
beberapa varian berdasarkan
perbandingan dengan nama varian.
Pastikan informasi ini lengkap dan mencakup semua varian. Anda juga dapat menyertakan
schemaDirectory()
tanpa variantMatchName
untuk menangani varian yang tidak cocok
oleh salah satu konfigurasi lainnya. Misalnya, dalam aplikasi dengan dua build
ragam demo
dan full
, serta dua jenis build debug
dan release
,
berikut adalah konfigurasi yang valid:
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")
}
Menetapkan lokasi skema menggunakan opsi pemroses anotasi
Jika Anda menggunakan Room versi 2.5.2 atau yang lebih lama, atau jika tidak menggunakan
Plugin Room Gradle, tetapkan lokasi skema menggunakan room.schemaLocation
opsi pemroses anotasi.
File dalam direktori ini digunakan sebagai input dan output untuk beberapa tugas Gradle.
Untuk ketepatan dan performa build inkremental dan build yang di-cache, Anda harus menggunakan
Gradle
CommandLineArgumentProvider
untuk memberi tahu Gradle tentang direktori ini.
Pertama, salin class RoomSchemaArgProvider
yang ditampilkan di bawah ke bagian modul
File build Gradle. Metode asArguments()
di class contoh diteruskan
room.schemaLocation=${schemaDir.path}
untuk KSP
. Jika Anda menggunakan KAPT
dan
javac
, ubah nilai ini menjadi -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}")
}
}
Kemudian, konfigurasikan opsi kompilasi untuk menggunakan RoomSchemaArgProvider
dengan
direktori skema yang ditentukan:
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"))
)
}
}
}
}
Menguji satu migrasi
Sebelum menguji migrasi, tambahkan
artefak Maven androidx.room:room-testing
dari Room ke dependensi
pengujian Anda, lalu tambahkan lokasi skema yang diekspor sebagai folder aset:
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.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") }
Paket pengujian menyediakan class
MigrationTestHelper
,
yang dapat membaca file skema yang diekspor. Paket ini juga mengimplementasikan antarmuka TestRule
JUnit4, sehingga dapat mengelola database yang dibuat.
Contoh berikut menunjukkan pengujian untuk satu migrasi:
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. } }
Menguji semua migrasi
Meskipun mungkin untuk menguji satu migrasi inkremental, sebaiknya Anda menyertakan pengujian yang mencakup semua migrasi yang ditentukan untuk database aplikasi Anda. Hal ini membantu memastikan agar tidak ada perbedaan antara instance database yang baru dibuat dan instance lama yang mengikuti jalur migrasi yang ditentukan.
Contoh berikut menunjukkan pengujian untuk semua migrasi yang ditentukan:
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}; }
Menangani jalur migrasi yang hilang dengan tepat
Jika Room tidak dapat menemukan jalur migrasi untuk mengupgrade database yang sudah ada di
perangkat ke versi saat ini,
IllegalStateException
akan muncul. Jika
data yang sudah ada hilang ketika jalur migrasi hilang, panggil metode
builder
fallbackToDestructiveMigration()
saat Anda membuat database:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Metode ini akan memberi tahu Room untuk membuat ulang tabel secara destruktif di database aplikasi Anda ketika perlu melakukan migrasi inkremental dan tidak ada jalur migrasi yang ditentukan.
Jika Anda hanya ingin Room menggunakan mode pembuatan ulang yang destruktif dalam
situasi tertentu, ada beberapa alternatif untuk fallbackToDestructiveMigration()
:
- Jika versi tertentu histori skema menyebabkan error yang tidak dapat Anda atasi
dengan jalur migrasi, gunakan
fallbackToDestructiveMigrationFrom()
sebagai gantinya. Metode ini menunjukkan bahwa Anda ingin agar Room menggunakan mode pembuatan ulang yang destruktif hanya saat melakukan migrasi dari versi tertentu. - Jika Anda ingin Room menggunakan mode pembuatan ulang yang destruktif hanya saat melakukan migrasi
dari versi database yang lebih baru ke versi yang lebih lama, gunakan
fallbackToDestructiveMigrationOnDowngrade()
sebagai gantinya.
Menangani nilai default kolom saat upgrade ke Room 2.2.0
Di Room 2.2.0 dan versi yang lebih tinggi, Anda dapat menentukan nilai default untuk kolom menggunakan
anotasi
@ColumnInfo(defaultValue = "...")
.
Pada versi yang lebih lama dari 2.2.0, satu-satunya cara untuk menentukan nilai default untuk
kolom adalah dengan menentukannya secara langsung dalam pernyataan SQL yang dijalankan, yang menghasilkan
nilai default yang tidak diketahui oleh Room. Hal ini berarti bahwa jika database
awalnya dibuat oleh versi Room sebelum 2.2.0, melakukan upgrade aplikasi untuk
menggunakan Room 2.2.0 mungkin memerlukan jalur migrasi khusus untuk
nilai default yang sudah ada, yang Anda tetapkan tanpa menggunakan Room API.
Sebagai contoh, anggap saja versi 1 database mendefinisikan entity 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; }
Misalkan versi 2 dari database yang sama menambahkan kolom NOT NULL
baru dan menentukan jalur migrasi dari versi 1 ke versi 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 ''"); } };
Hal ini menyebabkan perbedaan dalam tabel yang mendasari antara update dan penginstalan
aplikasi yang baru. Karena nilai default untuk kolom tag
hanya
dideklarasikan di jalur migrasi dari versi 1 hingga versi 2, setiap pengguna yang
menginstal aplikasi mulai dari versi 2 tidak memiliki nilai default untuk tag
dalam skema databasenya.
Pada versi Room yang lebih lama dari 2.2.0, perbedaan ini tidak berbahaya. Namun, jika
aplikasi nanti diupgrade untuk menggunakan Room 2.2.0 atau versi yang lebih tinggi dan mengubah class entity
Song
agar menyertakan nilai default untuk tag
menggunakan anotasi
@ColumnInfo
, Room
dapat melihat perbedaan ini. Hal ini menyebabkan kegagalan validasi
skema.
Untuk membantu memastikan skema database konsisten di semua pengguna ketika nilai default kolom dideklarasikan dalam jalur migrasi sebelumnya, lakukan hal berikut saat pertama kali Anda mengupgrade aplikasi untuk menggunakan Room versi 2.2.0 atau versi yang lebih baru:
- Deklarasikan nilai default kolom di class entity masing-masing menggunakan anotasi
@ColumnInfo
. - Tingkatkan nomor versi database satu tingkat.
- Tentukan jalur migrasi ke versi baru yang mengimplementasikan strategi penurunan dan pembuatan ulang untuk menambahkan nilai default yang diperlukan ke kolom yang ada.
Contoh berikut menunjukkan proses ini:
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"); } };