迁移 Room 数据库

当您在应用中添加和更改功能时,需要修改 Room 实体类和底层数据库表以反映这些更改。如果应用更新更改了数据库架构,那么保留设备内置数据库中已有的用户数据就非常重要。

Room 同时支持以自动和手动方式进行的增量迁移。自动迁移适用于大多数基本架构更改,不过对于更复杂的更改,您可能需要手动定义迁移路径。

自动迁移

如需声明两个数据库版本之间的自动迁移,请将 @AutoMigration注解添加到autoMigrations属性中 @Database

// Database class before the version update.
@Database(
  version = 1,
  entities = [User::class]
)
abstract class AppDatabaseV1 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

// Database class after the version update.
@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration(from = 1, to = 2)
  ]
)
abstract class AppDatabaseV2 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

自动迁移规范

如果 Room 检测到架构更改不明确,并且无法在没有更多输入的情况下生成迁移计划 ,则会抛出编译时间错误,您必须提供 AutoMigrationSpec 实现。最常见的情况是,迁移涉及以下某项:

  • 删除或重命名表。
  • 删除或重命名列。

您可以使用 AutoMigrationSpec 为 Room 提供正确生成迁移路径所需的额外信息。在 RoomDatabase 类中定义实现 AutoMigrationSpec 的类,并使用以下一个或多个注解对其进行注解:

如需使用 AutoMigrationSpec 实现进行自动迁移,请在相应的 @AutoMigration 注解中设置 spec 属性:

@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration (
      from = 1,
      to = 2,
      spec = MigrationSpec1To2::class
    )
  ]
)
abstract class AppDatabaseWithSpec : RoomDatabase() {
  abstract fun userDao(): UserDao
}

@RenameTable(fromTableName = "User", toTableName = "AppUser")
internal class MigrationSpec1To2 : AutoMigrationSpec

如果您的应用需要在自动迁移完成后执行更多工作,您可以实现 onPostMigrate。如果您在 AutoMigrationSpec 中实现此函数,Room 会在自动迁移完成后调用它。

手动迁移

如果迁移涉及复杂的架构更改,Room 可能无法自动生成适当的迁移路径。例如,如果您决定将表中的数据拆分为两个表,Room 无法确定如何执行此拆分。在这些情况下,您必须通过实现 Migration 类来手动定义 a 迁移路径。

一个 Migration 类通过替换 migrate 函数,显式定义 startVersionendVersion 之间的迁移路径。使用 addMigrations 函数将 Migration 类添加到数据库构建器:

val MIGRATION_1_2 = object : Migration(1, 2) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
      "PRIMARY KEY(`id`))")
  }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
  }
}

Room.databaseBuilder<ManualMigrationDatabase>(applicationContext, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
  .build()

定义迁移路径后,您可以对某些版本使用自动迁移,而对另一些版本使用手动迁移。如果您为同一版本同时定义了自动迁移和手动迁移,则 Room 会使用手动迁移。

测试迁移

迁移通常十分复杂,迁移定义错误可能会导致应用崩溃。为了保持应用的稳定性,请测试迁移。Room 提供了一个 room3-testing Maven 工件,以协助完成自动和手动迁移的测试过程。如需使此工件正常工作,您必须先导出数据库的架构。

导出架构

Room 会在编译时将数据库的架构信息导出为 JSON 文件。导出的 JSON 文件代表数据库的架构历史记录。将这些文件存储在版本控制系统中,以便您可以重新创建较低版本的数据库以进行测试,并支持自动生成迁移路径。

使用 Room Gradle 插件设置架构位置

如需指定架构目录,请应用 Room Gradle 插件 并使用 room3 扩展。

Groovy

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

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

如果您的数据库架构因变体、方案或构建类型而异,您必须使用 schemaDirectory 配置多次指定不同的位置,每次都以 variantMatchName 作为第一个实参。每个配置都可以根据与变体名称的简单比较来匹配一个或多个变体。

确保这些配置详尽无遗,涵盖所有变体。您还可以添加不带 variantMatchNameschemaDirectory() 来处理任何其他配置未匹配的变体。例如,在具有两个构建方案(demofull)和两个构建类型(debugrelease)的应用中,以下是有效的配置:

Groovy

room3 {
  // 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

room3 {
  // 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 Gradle 插件,请使用 room.schemaLocation 注解处理器选项设置架构位置。

Gradle 使用此目录中的文件作为某些 Gradle 任务的输入和输出。 为了确保增量构建和缓存构建的正确性和性能,您必须使用 Gradle 的 CommandLineArgumentProvider 向 Gradle 通知 此目录。

首先,将以下 RoomSchemaArgProvider 类复制到模块的 Gradle 构建文件中。示例类中的 asArguments 函数会将 room.schemaLocation=${schemaDir.path} 传递给 KSP。如果您使用的是 KAPTjavac,请将此值更改为 -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() {
    return ["room.schemaLocation=${schemaDir.path}".toString()]
  }
}

Kotlin

class RoomSchemaArgProvider(
  @get:InputDirectory
  @get:PathSensitive(PathSensitivity.RELATIVE)
  val schemaDir: File
) : CommandLineArgumentProvider {

  override fun asArguments(): Iterable<String> {
    return listOf("room.schemaLocation=${schemaDir.path}")
  }
}

然后,配置编译选项以使用带有指定架构目录的 RoomSchemaArgProvider

Groovy

ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

Kotlin

ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

测试单次迁移

测试迁移之前,先将 androidx.room3:room3-testing 工件添加到测试依赖项中,并将所导出架构的位置添加为资源目录:

Groovy

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
    androidTestImplementation "androidx.room3:room3-testing:3.0.1"
}

Kotlin

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

dependencies {
    ...
    testImplementation("androidx.room3:room3-testing:3.0.1")
}

测试软件包提供了可读取导出的架构文件的 MigrationTestHelper 类。该软件包还实现了 JUnit4 TestRule 接口来管理创建的数据库。

以下示例演示了针对单次迁移的测试:

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    @get:Rule
    val helper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrate1To2() = runTest {
        val connection = helper.createDatabase(1)
        // Database has schema version 1. Insert some data using SQL queries.
        // You can't use DAO classes because they expect the latest schema.
        connection.execSQL("INSERT INTO User (id, name) VALUES (1, 'John Doe')")
        connection.close()

        // Re-open the database with version 2 and provide MIGRATION_1_2
        val migratedConnection = helper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2))

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
        val hasData = migratedConnection.prepare("SELECT COUNT(*) FROM User").use {
          it.step()
          it.getLong(0) > 0
        }
        assertTrue("Expected data was not migrated", hasData)
        migratedConnection.close()
    }
}

测试所有迁移

虽然可以测试单次增量迁移,但您应添加涵盖为应用数据库定义的所有迁移的测试。这有助于确保最近创建的数据库实例与遵循定义的迁移路径的旧实例之间不存在差异。

以下示例演示了针对所有定义的迁移的测试:

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    // Array of all migrations.
    private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrateAll() = runTest {
        // Create earliest version of the database.
        val connection = helper.createDatabase(1)
        connection.close()

        // Create latest version of the database.
        val db = Room.databaseBuilder<AppDatabase>(instrumentation.targetContext, TEST_DB)
          .setDriver(AndroidSQLiteDriver())
          .addMigrations(*ALL_MIGRATIONS)
          .build()
        // Open the database, Room validates the schema once all migrations
        // execute.
        db.useReaderConnection { connection ->
          // Perform additional validation
        }

        db.close()
    }
}

妥善处理缺失的迁移路径

如果 Room 找不到将设备上的现有数据库升级到当前版本的迁移路径,则会发生 IllegalStateException。在迁移路径缺失的情况下,如果丢失现有数据可以接受,请在创建数据库时调用fallbackToDestructiveMigration 构建器函数:

Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name")
        .fallbackToDestructiveMigration()
        .build()

此函数会将 Room 配置为在需要执行没有定义迁移路径的增量迁移时,破坏性地重新创建应用的数据库表。

如需仅在某些情况下回退到破坏性重新创建,请使用以下替代方案之一来代替 fallbackToDestructiveMigration

  • 如果特定版本的架构历史记录导致迁移路径出现无法解决的问题 ,请改用 fallbackToDestructiveMigrationFrom。 此函数表示您希望 Room 仅在从特定版本进行迁移时回退到破坏性重新创建。
  • 如果您希望 Room 仅在从较高数据库版本迁移到较低数据库版本时回退到破坏性重新创建,请改用 fallbackToDestructiveMigrationOnDowngrade