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();
版本管理的核心流程图
最佳实践:我踩过的坑与建议
- 版本号写在常量里,不要直接在注解里写数字。比如
public static final int DB_VERSION = 5;,这样方便统一管理,也容易在测试中引用。 - 每个Migration都要有单元测试。我会专门写一个
MigrationTest类,用MigrationTestHelper模拟升级过程,并验证数据完整性。这个咱们后面章节会详细讲。 - 不要依赖fallbackToDestructiveMigration上线。有同事认为“反正用户数据可以重新登录获取”,结果发现用户本地缓存的草稿、设置偏好全没了——App五星好评直接掉到两星。
- 注意多数据库场景。如果你的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等组合 |