第3章 Room数据库迁移策略:表结构变更处理、Migration编写与fallbackToDestructiveMigration风险控制

3.1 数据库迁移的核心触发机制

Room 在应用启动时对比当前数据库版本号与预期版本号。当 @Database(version = N) 中的版本号升高,且检测到实体类对应的 Schema 发生变化时,Room 会遍历已注册的 Migration 列表。若找不到从旧版本到新版本的迁移对象,默认抛出 IllegalStateException;若调用了 fallbackToDestructiveMigration(),则降级为销毁旧表并重建。

Schema 变更检测依据:Room 在编译阶段为每个 @Entity 生成 app/build/outputs/schema/ 目录下的 JSON 文件,记录每张表的列名、类型、约束和索引。版本升级时,Room 运行时通过对比该 JSON 与实际数据库结构决定是否需要迁移。

3.2 表结构变更的常见类型与策略对照

变更类型AutoMigration 支持手写 Migration 核心 SQL风险等级
新增表✅ 是CREATE TABLE IF NOT EXISTS ...低
新增列(含默认值)✅ 是ALTER TABLE ... ADD COLUMN ... NOT NULL DEFAULT ...低
新增列(非空无默认值)❌ 否分步:先加可空列,填充数据,再设置 NOT NULL中
重命名列❌ 否CREATE 新表 → INSERT SELECT → DROP 旧表高
修改列类型❌ 否同上重命名流程高
删除列❌ 否CREATE 新表(不含该列)→ INSERT SELECT → DROP 旧表高
重命名表❌ 否ALTER TABLE ... RENAME TO ...中
添加/删除索引✅ 是CREATE INDEX / DROP INDEX低

3.3 手写 Migration 的编写与注册

Migration 是一个抽象类,构造函数接收起始版本和目标版本。在 migrate() 方法中通过 SupportSQLiteDatabase 执行原生 SQL。

// 版本 1 → 2:新增 age 列
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL(
            "ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0"
        );
    }
};

// 版本 2 → 3:新增 address 表并建立外键
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL(
            "CREATE TABLE IF NOT EXISTS `address` (" +
            "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
            "`userId` INTEGER NOT NULL, " +
            "`street` TEXT NOT NULL, " +
            "FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE)"
        );
    }
};

注册迁移链:

AppDatabase db = Room.databaseBuilder(context, AppDatabase.class, "app_db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .build();
复杂变更模板——重命名列:Room 不直接支持 ALTER COLUMN,需采用“创建新表→迁移数据→删除旧表→重命名新表”四步法。
database.execSQL("CREATE TABLE user_new (id INTEGER PRIMARY KEY, user_name TEXT)");
database.execSQL("INSERT INTO user_new (id, user_name) SELECT id, old_name FROM user");
database.execSQL("DROP TABLE user");
database.execSQL("ALTER TABLE user_new RENAME TO user");

3.4 AutoMigration 自动迁移

Room 2.4.0+ 支持 @AutoMigration,适用于新增表、新增列(含默认值)、索引变更等干净变更。无需编写 Migration 对象,只需在 @Database 中声明:

@Database(
    version = 3,
    entities = {User.class, Address.class},
    autoMigrations = {
        @AutoMigration(from = 1, to = 2),
        @AutoMigration(from = 2, to = 3)
    }
)
public abstract class AppDatabase extends RoomDatabase {
    // ...
}

对于重命名列、合并表等复杂操作,需搭配 AutoMigrationSpec 回调:

@AutoMigration(from = 2, to = 3, spec = MySpec.class)
public class MySpec implements AutoMigrationSpec {
    @Override
    public void onPostMigrate(@NonNull SupportSQLiteDatabase database) {
        // 执行额外数据清洗或索引重建
    }
}

3.5 fallbackToDestructiveMigration 风险控制

当 .fallbackToDestructiveMigration() 被调用时,Room 在找不到匹配 Migration 时直接执行 DROP ALL TABLES 并重新建表。所有用户数据永久丢失,且操作不可回滚。

使用环境风险等级推荐策略
本地开发调试低可启用,但需知晓数据丢失后果
QA 测试环境中谨慎启用,优先提供完整 Migration 链
CI/CD 自动化测试中可启用(测试数据可重建),但不得发布到生产
生产环境极高❌ 禁止启用,必须提供精确 Migration
灰度 / 内测发布高❌ 禁止启用,避免真实用户数据损毁
⚠️ 生产环境禁用 fallbackToDestructiveMigration
一旦线上应用因缺少 Migration 而触发降级,用户数据被清空将导致严重投诉与业务损失。替代方案:
• 为每个版本编写精确 Migration 对象
• 使用 Pre-packaged database 预置数据库
• 通过网络请求远端 Schema 版本并动态下发迁移脚本(需自研)

3.6 调试与验证方法

Database Inspector:Android Studio 自带工具,可在模拟器或调试设备上实时查看数据库表结构、数据行和 SQL 执行日志。

MigrationTestHelper 单元测试:

@Test
public void testMigrationFrom1To2() {
    MigrationTestHelper helper = new MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(),
        AppDatabase.class,
        "app_db"
    );
    // 创建版本 1 数据库并插入一条记录
    SupportSQLiteDatabase db = helper.createDatabase("app_db", 1);
    db.execSQL("INSERT INTO user (id, name) VALUES (1, 'alice')");
    db.close();

    // 执行迁移到版本 2
    AppDatabase appDb = MigrationTestHelper.runMigrations(
        "app_db",
        2,
        true,
        MIGRATION_1_2
    );

    // 校验数据完整性
    User user = appDb.userDao().getUser(1);
    assertEquals("alice", user.getName());
    assertEquals(0, user.getAge());  // 默认值验证
}

Schema 导出对比:在 build.gradle 中配置 room.schemaLocation,将每次编译的 Schema JSON 纳入版本控制,通过 diff 工具直观查看列变更。

// build.gradle (module)
defaultConfig {
    javaCompileOptions {
        annotationProcessorOptions {
            arguments["room.schemaLocation"] = "$projectDir/schemas"
        }
    }
}

3.7 迁移决策流程总览

下图展示了从检测 Schema 变更到最终执行的完整决策树,帮助开发者在编码前快速判断应采用哪种迁移策略:

Room 数据库迁移决策流程 检测到 Schema 版本变更 存在对应 Migration ? 是 执行 Migration 数据完整保留 ✅ 否 启用 fallbackTo Destructive ? 是 销毁旧表重建 全部数据丢失 ⚠️ 否 抛出异常 需补充 Migration ❌ 生产环境强制要求 必须为每个版本提供精确 Migration,禁止启用 fallbackToDestructive

3.8 生产环境最佳实践清单

  • 每版必迁:每次版本号递增必须配套对应的 Migration(from, to) 对象,并在 addMigrations() 中注册。
  • 避免链式跳过:用户可能从任意旧版本升级,需保留所有中间版本的 Migration 对象,或提供足够长的迁移链。
  • 数据校验:迁移后通过 DAO 查询抽样数据,验证行数、列默认值、外键约束是否完整。
  • 预置数据库兜底:对于全新安装,使用 createFromAsset() 或 createFromFile() 提供预置数据库,绕过迁移逻辑。
  • 监控与告警:在 Application 层捕获 MigrationFailedException 并上报到崩溃分析平台,及时发现未覆盖的版本路径。
版本号管理建议:将数据库版本号定义为 AppDatabase.VERSION 静态常量,并与 @Database(version = ...) 同步。所有 Migration 对象集中存放在 Migrations.java 文件中,便于评审和追溯。

3.9 常见错误与解决方案

错误现象根因解决方法
java.lang.IllegalStateException: Migration didn't properly handleMigration 中 SQL 未覆盖所有表变更,或列类型不匹配导出 Schema JSON 对比,逐列验证 SQL 语句
fallbackToDestructive 导致线上数据丢失生产包误配置 .fallbackToDestructiveMigration()通过 BuildConfig.DEBUG 控制:if (BuildConfig.DEBUG) .fallbackToDestructiveMigration()
AutoMigration 报 Spec 不匹配实体类变更超出了自动迁移的兼容范围(如重命名列)改用 手写 Migration 或提供 AutoMigrationSpec 处理逻辑
Multiple migrations 顺序错误注册的 Migration 对象版本区间重叠或跳跃保证每个 Migration 的 from→to 连续且无交叉,使用 addMigrations(MIGRATION_1_2, MIGRATION_2_3)