11、数据库升级与迁移:onUpgrade最佳实践、ALTER TABLE局限性、数据迁移策略(创建临时表)、跨版本升级(版本号跳跃)、降级处理

数据库升级,说白了就是你的App迭代了,表结构也得跟着变。我见过太多项目,前期没规划好,一到升级就崩。今天咱们就把这块硬骨头啃下来。

11.1 onUpgrade 最佳实践

onUpgrade 是 SQLiteOpenHelper 里的回调方法。每次数据库版本号增加,它就会被触发。嗯,这里要注意:这个方法只在版本号升高时调用。

核心原则:永远不要在 onUpgrade 里直接删表重建,除非你不在乎用户数据。

我个人习惯这样写 onUpgrade:

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    // 逐版本升级,而不是直接跳到最新
    switch (oldVersion) {
        case 1:
            upgradeFromV1ToV2(db);
            // 注意:这里没有 break
        case 2:
            upgradeFromV2ToV3(db);
        case 3:
            upgradeFromV3ToV4(db);
        // 以此类推
    }
}

private void upgradeFromV1ToV2(SQLiteDatabase db) {
    db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0");
}

private void upgradeFromV2ToV3(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE IF NOT EXISTS order (" +
               "id INTEGER PRIMARY KEY, " +
               "user_id INTEGER, " +
               "amount REAL)");
}

为什么用 switch 不加 break?因为用户可能从版本1直接跳到版本4。这样写,版本1、2、3的升级逻辑都会执行,保证数据完整迁移。我在项目中遇到过用户从远古版本直接升级的情况,这种写法救了我一命。

11.2 ALTER TABLE 的局限性

SQLite 的 ALTER TABLE 功能非常有限。它只支持两件事:

  • 重命名表:ALTER TABLE old_name RENAME TO new_name
  • 添加列:ALTER TABLE table_name ADD COLUMN column_name type

你不能用它来:

  • 删除列
  • 修改列的类型
  • 添加约束(比如 NOT NULL)
  • 重排列的顺序

避坑指南:我曾经在项目里想用 ALTER TABLE 给已有列加个 NOT NULL 约束,结果直接抛异常。SQLite 的 ALTER TABLE 就是这么「弱」,你得接受它。

那怎么办?遇到这些场景,就得用「创建临时表」的策略了。

11.3 数据迁移策略:创建临时表

这是最通用的方案。说白了就是三步走:

  1. 创建新表(带新结构)
  2. 把旧数据复制过去
  3. 删旧表,重命名新表

来看个例子。假设我们要给 user 表加个 email 字段,同时把 name 字段改成 NOT NULL:

private void migrateUserTable(SQLiteDatabase db) {
    // 1. 创建临时表
    db.execSQL("CREATE TABLE user_temp (" +
               "id INTEGER PRIMARY KEY, " +
               "name TEXT NOT NULL, " +
               "age INTEGER DEFAULT 0, " +
               "email TEXT)");

    // 2. 复制数据(处理空值)
    db.execSQL("INSERT INTO user_temp (id, name, age) " +
               "SELECT id, COALESCE(name, ''), age FROM user");

    // 3. 删旧表
    db.execSQL("DROP TABLE user");

    // 4. 重命名
    db.execSQL("ALTER TABLE user_temp RENAME TO user");
}

你想想看,这个方案虽然代码多了点,但胜在安全可控。我建议把迁移逻辑封装成独立的方法,方便测试。

11.4 跨版本升级(版本号跳跃)

用户可能几个月没打开你的App。一打开,版本号从1直接跳到10。这时候怎么办?

我刚才展示的 switch 无 break 写法就是答案。它会依次执行每个版本的迁移逻辑。但这里有个坑:如果版本1到版本2改了表结构,版本2到版本3又改了一次,中间某个步骤出错了怎么办?

我的做法:每个迁移方法都用 try-catch 包起来,出错时记录日志,但不要中断整个升级流程。因为有些错误可能是数据问题,不影响后续操作。

另外,我建议在数据库版本号上做文章。比如用「主版本号 * 100 + 子版本号」的格式。这样即使版本跳跃,也能清晰知道该执行哪些迁移。

11.5 降级处理

降级比升级更少见,但一旦发生,往往很棘手。onDowngrade 方法在 Android 8.0(API 26)之后才默认支持。之前版本会直接抛异常。

降级的核心问题是:新版本的数据结构可能不兼容旧版本。比如你新增了一个字段,降级后这个字段的数据就丢了。

@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    // 降级策略:通常直接删表重建
    // 因为降级意味着回退,旧版本不认识新字段
    db.execSQL("DROP TABLE IF EXISTS user");
    db.execSQL("DROP TABLE IF EXISTS order");
    onCreate(db); // 重新创建旧版本的表结构
}

警告:降级会丢失数据。我曾经在测试环境手滑降级了一次,结果测试数据全没了。所以生产环境一定要做好数据备份,或者干脆禁止降级。

如果你确实需要支持降级,可以考虑在降级前把数据导出到文件,降级后再让用户手动导入。但说实话,这种场景极少。大部分App直接禁止降级,提示用户重新安装。

知识体系总览

下面这张图总结了数据库升级与迁移的核心流程:

数据库升级与迁移核心流程 检测版本号变化 升级还是降级? 升级 降级 逐版本执行迁移 删表重建/备份 ALTER TABLE 可行? 直接执行 创建临时表迁移 升级完成 导出数据(可选) 删表 + 调用 onCreate 降级完成

数据库升级没有银弹。我的建议是:提前规划好版本号策略,每个版本只做最小改动,迁移逻辑要可测试、可回滚。这样即使出问题,也能快速定位修复。


公众号:蓝海资料掘金营,微信deep3321