Room数据库版本管理基础归纳总结

嗯,说到Room的数据库版本管理,这是每个Android开发迟早要踩的坑。我自己刚入行那会儿,就因为版本号写错导致用户直接闪退,差点被产品经理拉去喝茶。今天咱们就把这块儿彻底捋清楚。

版本号的定义与增量规则

Room的版本管理,说白了就是一个整数。你每次修改数据库结构——比如加个字段、改个表名、甚至只是加个索引——都必须把版本号往上加。它不是时间戳,也不是语义化版本,就是单纯的递增整数。

核心规则:版本号只能增加,不能减少。从1开始(或0,但我建议从1开始),每次变更+1。

@Database(entities = {User.class}, version = 2)
public abstract class AppDatabase extends RoomDatabase { ... }

你可能会问,能不能跳版本号?比如直接从1跳到3?技术上可以,但强烈不建议。我在一个项目里见过有人因为忘记写中间版本的Migration,结果直接跳版本,导致旧版本用户的数据全丢了。那场面…

迁移策略:三种路子

Room提供了三种处理数据库迁移的方式,我按推荐程度排个序:

策略说明适用场景
手动Migration对象定义从v1→v2、v2→v3等显式SQL生产环境,需要保留数据
fallbackToDestructiveMigration版本不匹配时直接删表重建调试阶段、纯测试App
AutoMigration(Room 2.4+)自动生成迁移代码简单场景如加字段、删字段

我个人习惯是:开发阶段用fallbackToDestructiveMigration省事,但要写个开关;发布前必须换成显式的Migration对象,不然用户的数据就没了。你想想看,用户手机上存了一年的购物车记录,因为一个小版本更新全没了——这锅咱背不起。

手动Migration怎么写

每个Migration对象包含起始版本和结束版本,以及要执行的SQL:

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");
    }
};

然后你必须在构建Database时传入所有Migration:

Room.databaseBuilder(context, AppDatabase.class, "my-db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .build();

这里有个大坑:Migration必须覆盖所有从当前版本到目标版本的路径。比如用户手机上是v1,你直接更新到v3,那你必须提供MIGRATION_1_2和MIGRATION_2_3,Room会自动串联执行。或者你也可以提供一个MIGRATION_1_3(从1直接跳到3),但这样你就得保证你写的SQL能正确处理中间状态。我曾经就因为偷懒写了个MIGRATION_1_3,结果没考虑到v2新增的非空字段,导致升级时插入失败…

警告:如果只提供了MIGRATION_2_3,而没有MIGRATION_1_2,那么v1→v3的升级会直接抛出IllegalStateException闪退。测试的时候一定要覆盖“跳版本”的场景。

fallbackToDestructiveMigration 的陷阱

这个东西用起来很简单:

Room.databaseBuilder(context, AppDatabase.class, "my-db")
    .fallbackToDestructiveMigration()
    .build();

但你要明白,它会在版本不匹配时直接删除所有旧数据,然后创建新表。我之前在调试阶段一直开着这个,结果有一次把测试机上的演示数据全清空了,被测试小姐姐追着骂了三天。所以建议只在debug模式下启用:

Room.databaseBuilder(context, AppDatabase.class, "my-db")
    .addMigrations(...)
    .fallbackToDestructiveMigration() // 最好用buildConfig控制
    .build();

版本管理的核心流程图

Room数据库版本管理核心要素 版本号 (Version) Entity定义变更 Migration代码 fallbackToDestructive 迁移验证测试 AutoMigration 触发迁移 需要验证 降级方案 支持自动 💡 每次版本变更必须同步更新 Migration 代码或启用 fallback/AutoMigration 推荐路径:Entity变更 → 编写Migration → 编写测试用例 → 验证通过后发布

最佳实践:我踩过的坑与建议

  1. 版本号写在常量里,不要直接在注解里写数字。比如 public static final int DB_VERSION = 5;,这样方便统一管理,也容易在测试中引用。
  2. 每个Migration都要有单元测试。我会专门写一个 MigrationTest 类,用 MigrationTestHelper 模拟升级过程,并验证数据完整性。这个咱们后面章节会详细讲。
  3. 不要依赖fallbackToDestructiveMigration上线。有同事认为“反正用户数据可以重新登录获取”,结果发现用户本地缓存的草稿、设置偏好全没了——App五星好评直接掉到两星。
  4. 注意多数据库场景。如果你的App有多个Database实例,每个都有自己的版本号,别搞混了。我见过把用户数据库和缓存数据库的版本号写反的,一升级就崩。

一个典型的版本变更流程

假设你有一个User表,原来叫 name,现在要拆成 firstName 和 lastName:

// 1. 版本号改为3
@Database(entities = {User.class}, version = 3)
public abstract class AppDatabase extends RoomDatabase { ... }

// 2. 编写Migration 2→3
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase db) {
        db.execSQL("ALTER TABLE user ADD COLUMN first_name TEXT NOT NULL DEFAULT ''");
        db.execSQL("ALTER TABLE user ADD COLUMN last_name TEXT NOT NULL DEFAULT ''");
        // 把原有name拆成两列,假设用空格分割
        db.execSQL("UPDATE user SET first_name = substr(name, 1, instr(name, ' ') - 1), " +
                   "last_name = substr(name, instr(name, ' ') + 1) WHERE instr(name, ' ') > 0");
        // 删除旧列(SQLite不支持直接删列,需重建表,简单起见这里略过)
    }
};

// 3. 将Migration传入DatabaseBuilder
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)

小技巧:在SQLite中,ALTER TABLE ... ADD COLUMN 只允许在表末尾加列,并且不能加 NOT NULL 约束(除非给默认值)。如果要删除列或重命名列,就得创建新表、复制数据、删旧表、重命名。Room的AutoMigration可以自动处理这些,但只支持简单的场景。

常见陷阱速查表

陷阱表现解决方案
跳版本迁移路径缺失IllegalStateException提供完整链路Migration或一个全量迁移
SQL语法错误(如加NOT NULL无默认值)升级时SQLiteException确保默认值存在,或先用NULL再UPDATE
并发升级(用户从v1直接跳到v4)Room会依次执行M1_2, M2_3, M3_4确保每个Migration幂等且无副作用
数据库文件损坏升级崩溃或数据丢失使用fallbackToDestructiveMigration? 不如做好备份
测试时只测了单步升级遗漏跳版本场景写参数化测试覆盖1→3、2→3等组合