第3章 Room数据库迁移策略:表结构变更处理、Migration编写与fallbackToDestructiveMigration风险控制
3.1 数据库迁移的核心触发机制
Room 在应用启动时对比当前数据库版本号与预期版本号。当 @Database(version = N) 中的版本号升高,且检测到实体类对应的 Schema 发生变化时,Room 会遍历已注册的 Migration 列表。若找不到从旧版本到新版本的迁移对象,默认抛出 IllegalStateException;若调用了 fallbackToDestructiveMigration(),则降级为销毁旧表并重建。
@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();
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 |
| 灰度 / 内测发布 | 高 | ❌ 禁止启用,避免真实用户数据损毁 |
一旦线上应用因缺少 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 变更到最终执行的完整决策树,帮助开发者在编码前快速判断应采用哪种迁移策略:
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 handle | Migration 中 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) |