手动迁移实现与SQL语句编写:Migration类构造、execSQL编写、ALTER TABLE与CREATE TABLE语句实战
原理:Room 手动迁移机制
Room 数据库的版本迁移核心在于将旧版本 schema 安全地演变为新版本 schema。手动迁移通过编写显式的 Migration 对象,由开发者控制 execSQL 中执行的 DDL 语句。Room 在运行时检测到数据库版本变化后,会逐一执行匹配旧版本→新版本的 Migration 实例,并维护 _journal 日志确保迁移的原子性。
实现流程
- 1. 确定新旧版本号 – 定义
startVersion与endVersion。 - 2. 构造 Migration 对象 – 重写
migrate(SupportSQLiteDatabase database)方法。 - 3. 编写迁移 SQL – 在方法体内调用
database.execSQL(...)执行 DDL。 - 4. 注册迁移 – 将 Migration 实例传入
Room.databaseBuilder().addMigrations()。 - 5. 测试验证 – 使用单元测试或 instrumented test 验证迁移结果。
Migration 类构造
Room 提供 Migration 抽象类,构造函数如下:
public class Migration {
public final int startVersion;
public final int endVersion;
public Migration(int startVersion, int endVersion) {
this.startVersion = startVersion;
this.endVersion = endVersion;
}
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
}
每个 Migration 实例表示数据库从 startVersion 到 endVersion 的单向迁移。例如从版本 1 到版本 2:
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");
}
};
execSQL 编写指南
- 只允许 DDL 语句 –
execSQL不支持 SELECT/INSERT/UPDATE/DELETE;只能执行 CREATE / ALTER / DROP / INDEX 等。 - 注意 SQLite 方言限制 –
ALTER TABLE仅支持RENAME TO和ADD COLUMN,不支持 DROP COLUMN(需重建表)。 - 事务由 Room 自动包裹 – 每个 Migration 的
migrate方法已经运行在事务中,无需手动beginTransaction。 - 使用 String 拼接时警惕 SQL 注入 – 表名、列名尽量使用硬编码常量,避免动态拼接用户输入。
最佳实践:每个 Migration 只负责一步变更,保持粒度最小。如果从版本 1→3 需要两步,应分别定义 Migration(1,2) 和 Migration(2,3),Room 会自动按序执行。
ALTER TABLE 与 CREATE TABLE 实战
场景一:添加新列
database.execSQL("ALTER TABLE user ADD COLUMN phone TEXT");
注意:添加 NOT NULL 列时必须提供 DEFAULT 值,否则旧行数据会报错。
场景二:重命名表
database.execSQL("ALTER TABLE old_table_name RENAME TO new_table_name");
场景三:重建表(如删除列或修改约束)
// 1. 创建新表
database.execSQL("CREATE TABLE user_new (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
// 2. 复制旧数据
database.execSQL("INSERT INTO user_new (id, name, email) SELECT id, name, '' FROM user");
// 3. 删除旧表
database.execSQL("DROP TABLE user");
// 4. 重命名新表
database.execSQL("ALTER TABLE user_new RENAME TO user");
场景四:创建索引
database.execSQL("CREATE INDEX IF NOT EXISTS index_user_name ON user(name)");
完整迁移示例(版本 2→3,添加 phone 列 + 创建索引):
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE user ADD COLUMN phone TEXT DEFAULT ''");
database.execSQL("CREATE INDEX idx_user_phone ON user(phone)");
}
};
常见错误与调试
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| 迁移后启动闪退,日志显示 IllegalStateException: Migration didn't properly handle | 执行 SQL 后数据库 schema 与 Room 注解预期不符 | 检查 DDL 是否完整(如遗漏默认值、列类型不匹配) |
| 升级后查询返回空值 | 新列未设置默认值,旧行该列为 NULL | 在 ALTER TABLE ADD COLUMN 中指定 DEFAULT |
| 迁移卡死或报错 database disk image is malformed | 重建表时事务中途崩溃导致数据损坏 | 使用 PRAGMA foreign_keys=OFF 并在迁移前后维护外键 |
| 迁移被跳过 | startVersion 与 endVersion 不连续或注册了重复 Migration | 确保版本号严格递进,使用 .fallbackToDestructiveMigration() 仅作为最后手段 |
性能与稳定性优化
- 批量 DDL 合并 – 将多条
execSQL调用合并到一个 Migration 中,避免频繁提交事务。 - 重建表时禁用外键检查 – 迁移开始前执行
database.execSQL("PRAGMA foreign_keys=OFF"),迁移结束后恢复 ON。 - 使用 WAL 模式 – 在数据库配置时启用
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING),减少迁移中的锁竞争。 - 大表迁移分批处理 – 如果表行数超过百万级,应在
CREATE TABLE后使用INSERT INTO ... SELECT分批迁移并使用PRAGMA synchronous=OFF(迁移后恢复)。
警告:切勿在生产环境中使用
fallbackToDestructiveMigration() 作为默认策略,因为用户数据会完全丢失。该选项仅适用于开发调试或可重新获取数据的场景。手动迁移决策流程图
掌握手动迁移的核心是理解 SQLite DDL 的限制并编写正确的 execSQL。优先使用小粒度、连续的 Migration,并通过单元测试覆盖每个迁移路径,确保数据零丢失。