手动迁移实现与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() 作为默认策略,因为用户数据会完全丢失。该选项仅适用于开发调试或可重新获取数据的场景。

手动迁移决策流程图

手动迁移实现流程 检测数据库版本变化 匹配 Migration? 是 执行 migrate() execSQL 内部操作 ALTER TABLE / CREATE TABLE CREATE INDEX / 重建表 迁移完成 否 fallbackToDestructive 每个 Migration 必须精确匹配 start→end

掌握手动迁移的核心是理解 SQLite DDL 的限制并编写正确的 execSQL。优先使用小粒度、连续的 Migration,并通过单元测试覆盖每个迁移路径,确保数据零丢失。