4. 增量迁移与链式迁移:连续版本间的迁移组合

4.1 原理与概念

增量迁移(Incremental Migration)是指每次仅处理一个版本差的迁移策略,例如从版本 1 到 2、从 2 到 3 分别定义独立的迁移逻辑。链式迁移(Chained Migration)则通过 Room 的 addMigrations() 方法将多个增量迁移对象注册到构建器中,运行时由 Room 根据当前数据库版本与目标版本自动解析并顺序执行迁移链。

核心原理可概括为:

  • 版本差最小化:每个 Migration 对象只负责相邻版本间的 schema 变更,降低单次迁移的复杂度与风险。
  • 自动路由:Room 在 build() 时比较数据库当前版本与目标版本,按版本号递增顺序依次调用匹配的 Migration。
  • 幂等组合:链中的每个迁移独立且顺序固定,同一条链多次执行结果一致,便于测试与回滚。
对比维度 增量迁移 完整迁移(一次性)
版本跨度 仅相邻版本(如 1→2) 可跨越多个版本(如 1→4)
维护成本 低,每个迁移职责单一 高,随版本增加需同步修改
测试难度 易覆盖每个版本变更 需构建完整历史数据
运行时性能 按需执行部分链段,更快 可能执行冗余操作
推荐场景 长期维护、多版本迭代 原型验证、一次性升级

4.2 实现流程与 API 链式调用

实现链式迁移需遵循以下流程:

  1. 为每一对相邻版本定义 Migration 对象,实现 migrate() 方法。
  2. 在 Room.databaseBuilder() 中通过 .addMigrations() 注册所有增量迁移。
  3. 调用 .build() 触发 Room 解析并执行迁移链。
  4. 通过 MigrationTestHelper 编写测试覆盖每条链的组合路径。
关键 API 签名:
Room.databaseBuilder(context, AppDatabase.class, "app.db")
  .addMigrations(Migration... migrations)
  .build()

addMigrations() 接受可变参数或多个 Migration 对象,Room 按版本号升序自动编排。

4.3 代码示例:定义增量迁移并链式注册

// 迁移 1→2:新增 age 列
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0"
        )
    }
}

// 迁移 2→3:创建 posts 表
val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "CREATE TABLE posts (" +
            "id INTEGER PRIMARY KEY, " +
            "title TEXT NOT NULL, " +
            "user_id INTEGER NOT NULL, " +
            "FOREIGN KEY(user_id) REFERENCES users(id))"
        )
    }
}

// 迁移 3→4:增加索引
val MIGRATION_3_4 = object : Migration(3, 4) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "CREATE INDEX idx_posts_user_id ON posts(user_id)"
        )
    }
}

// 链式注册到 Room 构建器
val database = Room.databaseBuilder(
        context,
        AppDatabase::class.java,
        "app.db"
    )
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
    .build()

当用户设备上的数据库版本为 1 且目标版本为 4 时,Room 自动顺序执行 MIGRATION_1_2 → MIGRATION_2_3 → MIGRATION_3_4,无需开发者手动编排。

提示:若只升级到版本 3,Room 会智能跳过 MIGRATION_3_4,仅执行前两个迁移。链式迁移天然支持“按需部分执行”。
增量迁移与链式迁移流程 数据库 v1 MIGRATION_1_2 数据库 v2 MIGRATION_2_3 数据库 v3 链式调用入口 .addMigrations(m1, m2, …) 测试覆盖 MigrationTestHelper 链式迁移执行策略 • Room 根据当前版本与目标版本自动选择迁移子链 • 迁移按版本号严格升序执行,任一失败则整体回滚 • 已注册但版本跨度外的迁移不会被执行,保证幂等性

4.4 测试覆盖:验证链式迁移的正确性

使用 AndroidX 提供的 MigrationTestHelper 可针对每条迁移链编写单元测试,核心步骤包括:

  1. 创建指定版本的数据库并插入模拟数据。
  2. 调用 runMigrationsAndValidate() 执行目标迁移链。
  3. 断言 schema 变更生效、数据完整性未丢失。
@RunWith(AndroidJUnit4::class)
class ChainMigrationTest {

    companion object {
        private const val TEST_DB_NAME = "chain-migration-test.db"
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
            }
        }
        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, user_id INTEGER)")
            }
        }
    }

    @Test
    fun migrateFrom1To3_chainSuccess() {
        val helper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
        )

        // 创建版本 1 的数据库并插入一条用户
        var db = helper.createDatabase(TEST_DB_NAME, 1)
        db.execSQL("INSERT INTO users (id, name) VALUES (1, 'Alice')")
        db.close()

        // 执行链式迁移 1→2→3
        db = helper.runMigrationsAndValidate(
            TEST_DB_NAME,
            3,
            true,   // validateDroppedTables
            MIGRATION_1_2,
            MIGRATION_2_3
        )

        // 验证 age 列默认值
        val cursor = db.query("SELECT age FROM users WHERE id = 1")
        assertTrue(cursor.moveToFirst())
        assertEquals(0, cursor.getInt(0))
        cursor.close()

        // 验证 posts 表存在
        val postsCursor = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='posts'")
        assertTrue(postsCursor.count == 1)
        postsCursor.close()
        db.close()
    }

    @Test
    fun migrateFrom2To3_onlyChainSegment() {
        val helper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
        )

        // 直接创建版本 2 的数据库
        var db = helper.createDatabase(TEST_DB_NAME, 2)
        db.execSQL("INSERT INTO users (id, name, age) VALUES (1, 'Bob', 25)")
        db.close()

        // 仅执行 2→3 迁移
        db = helper.runMigrationsAndValidate(TEST_DB_NAME, 3, true, MIGRATION_2_3)
        val cursor = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='posts'")
        assertTrue(cursor.count == 1)
        cursor.close()
        db.close()
    }
}
注意:runMigrationsAndValidate() 的第三个参数 validateDroppedTables 设为 true 时,Room 会检测是否有表被删除但未在 @Database 实体中声明,防止意外丢失数据。

4.5 调试与验证策略

在生产环境中可采用以下方式确认链式迁移是否按预期执行:

  • 启用 Room 日志:通过 QueryCallback 或 setQueryLogger 输出迁移过程中执行的 SQL。
  • 版本号埋点:迁移完成后读取 PRAGMA user_version 确认当前版本。
  • 中间版本快照:在测试中通过 createDatabase(version) 分别验证每个中间版本的数据结构。
// 开启 Room 日志(在 Application 中配置)
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .setQueryCallback { sql, bindArgs ->
        Log.d("RoomSQL", "SQL: $sql  Args: ${bindArgs.joinToString()}")
    }
    .build()

// 运行时验证版本
val pragmaCursor = database.openHelper.readableDatabase
    .query("PRAGMA user_version")
pragmaCursor.moveToFirst()
val currentVersion = pragmaCursor.getInt(0)
Log.i("RoomVersion", "Current DB version: $currentVersion")

4.6 常见错误与规避

错误类型 现象 解决方案
迁移未注册 运行时抛出 IllegalStateException: Migration not found 确认所有增量迁移均已传入 addMigrations()
迁移顺序混乱 SQL 执行失败,外键约束或列不存在 确保每个 Migration 的起始/目标版本号严格连续且递增
Schema 不匹配 runMigrationsAndValidate 抛出验证异常 检查 @Entity 定义与迁移 SQL 是否一致,尤其是列类型与默认值
忽略降级路径 用户回退 App 版本时数据库不兼容 使用 .fallbackToDestructiveMigration() 或单独定义降级迁移
测试数据不足 生产环境迁移后数据丢失 为每个迁移链编写包含边界数据的测试用例

4.7 性能与稳定性优化

  • 事务包裹:Room 默认将每个 Migration.migrate() 放在独立事务中,若迁移包含大量 DML 操作,可手动合并事务减少提交开销。
  • 批量 DDL:在 migrate() 中尽量合并多条 execSQL 调用,减少跨事务切换。
  • 异步迁移:在 Room.databaseBuilder() 中不指定 .allowMainThreadQueries(),确保迁移在后台线程执行,避免 ANR。
  • 版本号跳跃检测:在 CI 流程中添加 lint 规则,防止定义重复或跳跃的 Migration 版本号。
  • 预加载 Schema:将每个版本的 schema.json 导出到 assets,加速 MigrationTestHelper 的验证过程。
经验值:当迁移链超过 3 个版本时,建议将