随着您在应用中添加和更改功能,您需要修改 Room 实体类和底层数据库表以反映这些更改。在应用更新更改数据库架构时,保留设备数据库中已有的用户数据非常重要。
Room 支持自动和手动增量迁移选项。自动迁移适用于大多数基本的架构更改,但对于更复杂的更改,您可能需要手动定义迁移路径。
自动迁移
要在两个数据库版本之间声明自动迁移,请将 @AutoMigration
注解添加到 @Database
中的 autoMigrations
属性
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 { ... }
自动迁移规范
如果 Room 检测到模糊的架构更改,并且无法在没有更多输入的情况下生成迁移计划,它会抛出编译时错误并要求您实现 AutoMigrationSpec
。最常见的情况是当迁移涉及以下任一情况时发生
- 删除或重命名表。
- 删除或重命名列。
您可以使用 AutoMigrationSpec
向 Room 提供正确生成迁移路径所需的额外信息。在您的 RoomDatabase
类中定义一个实现 AutoMigrationSpec
的静态类,并使用以下一个或多个注解对其进行注解
要将 AutoMigrationSpec
实现用于自动迁移,请在相应的 @AutoMigration
注解中设置 spec
属性
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 { } ... }
如果您的应用在自动迁移完成后需要执行更多工作,您可以实现 onPostMigrate()
。如果您在 AutoMigrationSpec
中实现此方法,Room 会在自动迁移完成后调用它。
手动迁移
在迁移涉及复杂架构更改的情况下,Room 可能无法自动生成适当的迁移路径。例如,如果您决定将表中的数据拆分到两个表中,Room 无法知道如何执行此拆分。在这种情况下,您必须通过实现 Migration
类来手动定义迁移路径。
一个 Migration
类通过覆盖 Migration.migrate()
方法,明确定义了 startVersion
和 endVersion
之间的迁移路径。使用 addMigrations()
方法将您的 Migration
类添加到您的数据库构建器中
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();
定义迁移路径时,您可以对某些版本使用自动迁移,对其他版本使用手动迁移。如果您为同一版本定义了自动迁移和手动迁移,Room 将使用手动迁移。
测试迁移
迁移通常很复杂,定义不正确的迁移可能导致应用崩溃。为了保持应用的稳定性,请测试您的迁移。Room 提供了一个 room-testing
Maven 工件,以协助自动和手动迁移的测试过程。要使此工件工作,您必须首先导出数据库的架构。
导出架构
Room 可以在编译时将数据库的架构信息导出到 JSON 文件中。导出的 JSON 文件代表您的数据库架构历史记录。将这些文件存储在版本控制系统中,以便 Room 可以创建较低版本的数据库用于测试,并启用自动迁移生成。
使用 Room Gradle 插件设置架构位置
如果您使用的是 Room 2.6.0 或更高版本,可以应用 Room Gradle 插件并使用 room
扩展程序来指定架构目录。
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
如果您的数据库架构因变体、风味或构建类型而异,您必须多次使用 schemaDirectory()
配置指定不同的位置,每次都将 variantMatchName
作为第一个参数。每个配置可以根据与变体名称的简单比较来匹配一个或多个变体。
确保这些配置是详尽的并覆盖所有变体。您还可以包含一个不带 variantMatchName
的 schemaDirectory()
来处理与任何其他配置都不匹配的变体。例如,在一个有两个构建风味 demo
和 full
以及两个构建类型 debug
和 release
的应用中,以下是有效的配置
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")
}
使用注解处理器选项设置架构位置
如果您使用的是 Room 2.5.2 或更低版本,或者您未使用 Room Gradle 插件,则使用 room.schemaLocation
注解处理器选项设置架构位置。
此目录中的文件用作某些 Gradle 任务的输入和输出。为了确保增量构建和缓存构建的正确性和性能,您必须使用 Gradle 的 CommandLineArgumentProvider
来告知 Gradle 此目录。
首先,将下面所示的 RoomSchemaArgProvider
类复制到模块的 Gradle 构建文件中。示例类中的 asArguments()
方法将 room.schemaLocation=${schemaDir.path}
传递给 KSP
。如果您使用的是 KAPT
和 javac
,请将此值更改为 -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}")
}
}
然后配置编译选项,以便使用带有指定架构目录的 RoomSchemaArgProvider
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"))
)
}
}
}
}
测试单个迁移
在测试迁移之前,将 Room 中的 androidx.room:room-testing
Maven 工件添加到您的测试依赖项中,并将导出架构的位置添加为资源文件夹
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.7.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.7.1") }
测试包提供了 MigrationTestHelper
类,它可以读取导出的架构文件。该包还实现了 JUnit4 TestRule
接口,因此它可以管理创建的数据库。
以下示例演示了单个迁移的测试
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. } }
测试所有迁移
虽然可以测试单个增量迁移,但我们建议您包含一个覆盖应用数据库所有已定义迁移的测试。这有助于确保最近创建的数据库实例与遵循已定义迁移路径的较旧实例之间没有差异。
以下示例演示了所有已定义迁移的测试
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}; }
优雅地处理缺失的迁移路径
如果 Room 找不到将设备上的现有数据库升级到当前版本的迁移路径,则会发生 IllegalStateException
。如果当迁移路径缺失时可以接受丢失现有数据,请在创建数据库时调用 fallbackToDestructiveMigration()
构建器方法
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
此方法告知 Room,当它需要执行增量迁移且没有定义迁移路径时,将应用数据库中的表进行破坏性重建。
如果您只想让 Room 在特定情况下回退到破坏性重建,fallbackToDestructiveMigration()
还有几种替代方案
- 如果架构历史中的特定版本导致无法通过迁移路径解决的错误,请改用
fallbackToDestructiveMigrationFrom()
。此方法表示您希望 Room 仅在从特定版本迁移时回退到破坏性重建。 - 如果您希望 Room 仅在从较高数据库版本迁移到较低版本时回退到破坏性重建,请改用
fallbackToDestructiveMigrationOnDowngrade()
。
升级到 Room 2.2.0 时处理列默认值
在 Room 2.2.0 及更高版本中,您可以使用注解 @ColumnInfo(defaultValue = \"...\")
为列定义默认值。在低于 2.2.0 的版本中,为列定义默认值的唯一方法是在执行的 SQL 语句中直接定义它,这会创建一个 Room 不知道的默认值。这意味着,如果数据库最初是由低于 2.2.0 的 Room 版本创建的,那么将您的应用升级到使用 Room 2.2.0 可能需要您为未通过 Room API 定义的现有默认值提供特殊的迁移路径。
例如,假设数据库的第 1 版定义了一个 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; }
再假设同一个数据库的第 2 版添加了一个新的 NOT NULL
列,并定义了从第 1 版到第 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 ''"); } };
这会导致应用更新和全新安装之间底层表存在差异。由于 tag
列的默认值仅在从第 1 版到第 2 版的迁移路径中声明,因此从第 2 版开始安装应用的任何用户在其数据库架构中都没有 tag
的默认值。
在 Room 低于 2.2.0 的版本中,此差异是无害的。但是,如果应用稍后升级到使用 Room 2.2.0 或更高版本,并更改 Song
实体类以使用 @ColumnInfo
注解包含 tag
的默认值,Room 就能看到此差异。这会导致架构验证失败。
为帮助确保当列默认值在早期迁移路径中声明时,所有用户的数据库架构保持一致,首次将应用升级到使用 Room 2.2.0 或更高版本时,请执行以下操作
- 使用
@ColumnInfo
注解在其各自的实体类中声明列默认值。 - 将数据库版本号加 1。
- 定义到新版本的迁移路径,该路径实现删除并重新创建策略,以便将必要的默认值添加到现有列中。
以下示例演示了此过程
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"); } };