15、Room数据库迁移:从手动到自动的进化之路

数据库迁移,说白了就是你的App升级后,数据库表结构变了,怎么让旧数据平滑过渡到新结构。我刚开始用Room时,觉得这玩意儿挺简单,不就是加个字段吗?结果第一次上线就翻车了——用户升级后直接闪退,因为旧数据库里没有新字段。

嗯,从那以后,我老老实实把迁移这块啃透了。今天咱们就聊聊Room数据库迁移的几种方式,从手动编写Migration类,到Room 2.4+推出的autoMigrations自动迁移,再到测试和兜底策略。

核心知识点一览:

  • Migration类手动编写——最传统、最可控的方式
  • autoMigrations自动迁移——Room 2.4+的新特性,省心省力
  • 迁移逻辑测试——不测试的迁移就是定时炸弹
  • fallbackToDestructiveMigration——最后的救命稻草
Room数据库迁移核心体系 数据库迁移 Migration类手动编写 最可控,但代码量大 autoMigrations自动迁移 Room 2.4+,省心省力 测试迁移逻辑 不测试就是定时炸弹 fallbackToDestructive 最后的救命稻草 选择哪种方式?取决于你的业务场景和数据重要性

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