2. 手动迁移核心类 Migration 创建
当数据库结构发生破坏性变化(如新增表、修改字段类型、删除列)时,Room 无法自动推断升级路径,必须通过手动迁移确保数据完整。手动迁移的核心是继承 Migration 抽象类,并准确声明版本区间与执行 SQL。
2.1 原理:版本区间标识与迁移调度
Room 的迁移引擎根据 startVersion 和 endVersion 匹配升级路径。当数据库版本号(version)升高时,Room 查找所有满足 startVersion == 当前数据库版本号 且 endVersion == 目标版本号 的 Migration 实例,按顺序执行其 migrate() 方法。多个连续升级可串联成链。
| 属性 | 说明 | 约束 |
|---|---|---|
startVersion |
迁移起始版本号(当前数据库版本) | 必须大于等于 1,且 startVersion < endVersion |
endVersion |
迁移目标版本号 | 必须大于 startVersion |
migrate() |
包含 SQL 语句的执行方法 | 在事务内自动执行,需保证幂等性 |
提示:版本号必须是递增整数,且
startVersion 不能等于 endVersion。Room 不支持降级迁移(反向迁移需自行处理)。
2.2 实现流程:创建 Migration 实例
- 确定新旧版本号(如 1 → 2)。
- 继承
Migration抽象类,实现构造方法中指定版本区间。 - 在
migrate()方法内编写ALTER TABLE、CREATE TABLE等 SQL 语句。 - 将创建好的
Migration对象添加到Room.databaseBuilder()的addMigrations()中。 - 适时更新数据库实体定义和 DAO,确保新旧版本兼容。
关键案例:从版本 1 升级到版本 2,新增“用户年龄”字段(INTEGER 类型,默认值 0):
public class Migration1To2 extends 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 代码与配置:完整 Migration 类
下面展示符合 Room 标准的 Migration 类实现,包含常见表结构变更操作:
public class Migration2To3 extends Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 1. 新建地址表
database.execSQL(
"CREATE TABLE IF NOT EXISTS address (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"user_id INTEGER NOT NULL, " +
"city TEXT NOT NULL, " +
"street TEXT, " +
"FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE" +
")"
);
// 2. 删除旧的不再使用的列
database.execSQL("ALTER TABLE user DROP COLUMN old_legacy_flag");
// 3. 添加新索引
database.execSQL("CREATE INDEX idx_address_user ON address(user_id)");
}
}
- 必须使用
SupportSQLiteDatabase执行 SQL,而非 Room 的 DAO 对象。 - 建议将多次
execSQL封装在同一个migrate()内,Room 会自动开启事务。 - 若需要条件判断,可在
migrate()内查询PRAGMA table_info等系统表做兼容。
2.4 调试与验证
| 验证方法 | 操作 | 预期结果 |
|---|---|---|
| 单元测试 | 使用 MigrationTestHelper 创建指定版本的数据库并执行迁移 |
迁移后表结构与数据正确 |
| 真实设备测试 | 安装旧版 App,再安装新版,检查数据是否保留 | 无数据丢失,新字段有默认值 |
| 日志分析 | 开启 Room 日志 QueryCallback 查看执行的 SQL |
确认迁移 SQL 按预期顺序执行 |
常见错误:
- 版本号不连续(如从 1 直接跳到 3 但缺少 2 的 Migration),Room 会抛出
IllegalStateException。 - SQL 语法错误(如 SQLite 不支持
ALTER TABLE DROP COLUMN旧版本),迁移会失败并导致 App 崩溃。 - 未正确处理
NOT NULL默认值与旧数据冲突。
2.5 性能与稳定性优化
- 批量 DDL 放在同一个迁移中:Room 的迁移在单个事务内执行,多次
execSQL不会额外提交,避免磁盘 I/O 碎片。 - 避免大事务阻塞:若迁移涉及大量数据转换(如复制整张表),考虑分批处理或使用
yieldIfContended()让出线程(Room 3.0+ 支持autoMigrations中的渐进式迁移)。 - 测试迁移的幂等性:多次执行同一迁移应不产生副作用,使用
CREATE TABLE IF NOT EXISTS和ALTER TABLE ADD COLUMN前先检查列是否存在。 - 预编译 SQL 语句:对于复杂迁移,使用
database.compileStatement()替代execSQL可提升少量性能,但通常execSQL足够。
2.6 流程总览:Migration 创建与生效
上述流程清晰展示了:从旧版本 N 出发,通过继承 Migration 并指定版本区间,在 migrate() 中编写 SQL,最终将数据库升级到新版本 M。每个 Migration 只负责连续版本间的单步迁移,多版本升级可通过串联多个 Migration 对象实现。