15、Room数据库迁移:从手动到自动的进化之路
数据库迁移,说白了就是你的App升级后,数据库表结构变了,怎么让旧数据平滑过渡到新结构。我刚开始用Room时,觉得这玩意儿挺简单,不就是加个字段吗?结果第一次上线就翻车了——用户升级后直接闪退,因为旧数据库里没有新字段。
嗯,从那以后,我老老实实把迁移这块啃透了。今天咱们就聊聊Room数据库迁移的几种方式,从手动编写Migration类,到Room 2.4+推出的autoMigrations自动迁移,再到测试和兜底策略。
核心知识点一览:
- Migration类手动编写——最传统、最可控的方式
- autoMigrations自动迁移——Room 2.4+的新特性,省心省力
- 迁移逻辑测试——不测试的迁移就是定时炸弹
- fallbackToDestructiveMigration——最后的救命稻草
15.1 Migration类:手动编写,步步为营
先说说最传统的方式。当你修改了Entity类,比如给User表加了一个age字段,你需要写一个Migration类,告诉Room怎么从旧版本迁移到新版本。
// 从版本1迁移到版本2
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 添加新字段,并设置默认值
database.execSQL(
"ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0"
)
}
}
// 在构建数据库时添加迁移
Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_database.db"
)
.addMigrations(MIGRATION_1_2)
.build()
我个人习惯把所有的Migration类放在一个单独的文件里,按版本号命名清晰。比如MIGRATION_1_2、MIGRATION_2_3这样。你想想看,如果以后版本迭代多了,找起来也方便。
小技巧: 迁移时尽量用SQL标准语句,避免用Room特有的注解。因为migrate方法里你直接操作的是SQLite数据库,不是Room的抽象层。
我在项目中遇到过一个问题:有个同事在迁移时用了INSERT INTO ... SELECT来合并两张表,结果忘了处理主键冲突,线上用户数据直接丢了。嗯,从那以后我要求所有迁移代码必须经过单元测试。
15.2 autoMigrations:Room 2.4+的自动挡
从Room 2.4.0开始,Google推出了autoMigrations。说白了,你只需要在Entity的变更上加上注解,Room会自动帮你生成迁移代码。这玩意儿怎么用?
// 在Entity上标记版本号
@Entity(tableName = "User")
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int // 新增字段
)
// 在Database类中声明自动迁移
@Database(
entities = [User::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() {
// ...
}
是不是很爽?但注意,autoMigrations只支持三种操作:
- 添加新表——直接新增Entity类即可
- 添加新字段——字段必须有默认值,或者用@NonNull标注
- 修改字段默认值——通过@ColumnInfo(defaultValue = "...")指定
⚠️ 重要提醒: autoMigrations不支持重命名表、重命名列、删除字段、修改字段类型等操作。这些场景你还是得老老实实写Migration类。我曾经天真地以为autoMigrations能搞定一切,结果编译时直接报错,提示我"AutoMigration failed"。
15.3 测试迁移逻辑:不测试就是给自己挖坑
数据库迁移最怕什么?怕用户升级后数据丢了,或者App直接崩溃。所以测试迁移逻辑是必须的。Room官方提供了测试辅助库,咱们来看看怎么写。
// 在build.gradle中添加测试依赖
androidTestImplementation "androidx.room:room-testing:2.6.1"
// 编写迁移测试
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@Test
fun testMigrationFrom1To2() {
// 创建版本1的数据库
val db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
TestDatabase::class.java
).build()
// 插入旧数据
db.userDao().insert(User(1, "张三"))
// 关闭旧数据库
db.close()
// 执行迁移到版本2
val migratedDb = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
TestDatabase::class.java
).addMigrations(MIGRATION_1_2)
.build()
// 验证数据是否完整
val users = migratedDb.userDao().getAll()
assertEquals(1, users.size)
assertEquals("张三", users[0].name)
assertEquals(0, users[0].age) // 默认值
migratedDb.close()
}
}
我个人建议每个迁移版本都写一个对应的测试用例。你想想看,如果迁移逻辑有bug,测试阶段就能发现,总比用户反馈强吧?
避坑指南: 测试时一定要用inMemoryDatabaseBuilder,不要用文件数据库。因为每次测试都是独立的,内存数据库用完就销毁,不会互相影响。我曾经用文件数据库跑测试,结果多个测试用例共用同一个数据库文件,数据互相污染,排查了半天才发现问题。
15.4 fallbackToDestructiveMigration:最后的救命稻草
有时候,迁移逻辑太复杂,或者你根本不在乎旧数据(比如缓存数据),这时候可以用破坏性迁移。说白了就是:如果找不到合适的迁移路径,直接删掉旧数据库,重新创建。
Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_database.db"
)
.fallbackToDestructiveMigration()
.build()
这行代码的意思是:如果Room发现数据库版本从1升到3,但你只提供了1到2的迁移,没有2到3的迁移,那Room就会直接删掉旧数据库,重新创建新数据库。嗯,数据就全没了。
⚠️ 慎用! 这个选项只适合以下场景:
- 数据库存储的是缓存数据,丢了也无所谓
- 开发阶段频繁改表结构,懒得写迁移
- 用户数据可以从服务器重新拉取
如果是用户的核心数据(比如笔记、联系人、订单),千万别用这个。我曾经在一个内部工具App里用了fallbackToDestructiveMigration,结果测试同事的本地配置全丢了,被骂了一下午。
另外,你还可以指定特定版本使用破坏性迁移:
Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_database.db"
)
.fallbackToDestructiveMigration(1, 2, 3) // 只有从1、2、3版本迁移时才使用破坏性
.build()
这样更精细一些。比如你从版本4升级到5,有正常的迁移逻辑,那就走正常流程;如果是从版本1直接跳到5,没有中间版本的迁移,那就直接重建。
总结一下
数据库迁移这件事,说白了就是平衡「数据安全」和「开发效率」。我个人建议:
- 生产环境用Migration类手动编写,每个版本都测试
- 开发阶段或者简单场景用autoMigrations
- 缓存数据可以用fallbackToDestructiveMigration
- 但无论如何,测试不能省
嗯,今天就聊到这儿。记住一句话:数据库迁移不是写代码,是写信任——用户信任你不会丢他的数据。
公众号:蓝海资料掘金营,微信deep3321