11、数据库迁移:@Migration注解、迁移策略、测试迁移、破坏性迁移fallbackToDestructiveMigration

数据库迁移,说白了就是你的App升级了,数据库表结构也得跟着变。我刚开始做Room开发时,觉得这玩意儿挺简单——不就是加个字段嘛。结果第一次上线就翻车了,用户升级后直接闪退。嗯,从那以后我再也不敢小看数据库迁移了。

今天咱们就把Room的迁移机制彻底聊透。你想想看,一个App迭代个三五年,数据库版本从1升到20多很正常。如果迁移没写好,用户数据说丢就丢,那可就真成事故了。

核心要点:数据库迁移的本质,就是在不丢失已有数据的前提下,安全地改变数据库的Schema结构。

11.1 @Migration注解:迁移的最小单元

Room里做迁移,核心就是@Migration注解。它需要你提供三个信息:起始版本号、目标版本号、以及迁移执行的SQL语句。

@Migration(version1 = 1, version2 = 2)
class Migration1To2 : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 添加一个"年龄"字段
        database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}

这里有个细节我特别想强调:迁移版本号必须是递增的。比如从1到2,从2到3,不能跳版本。为什么?因为Room会按顺序依次执行迁移,跳版本会导致中间状态的数据不一致。

我在项目中遇到过一个问题:有个同事直接写了从1到3的迁移,跳过了版本2。结果版本2里有个字段重命名操作,版本3的迁移里没处理,上线后用户的老数据全乱了。所以,老老实实一步一步来,别偷懒。

个人习惯:我一般会在每个迁移类上加注释,写明这次迁移改了什么、为什么改。半年后回头看,你会感谢自己的。

11.2 迁移策略:单步迁移 vs 多步迁移

迁移策略其实就两种:单步迁移和多步迁移。咱们用个表格对比一下:

策略 适用场景 优点 缺点
单步迁移 版本跨度小(如1→2) 代码简单,容易理解 版本多了维护麻烦
多步迁移 版本跨度大(如1→5) 逻辑清晰,可单独测试 类文件会增多

实际开发中,我推荐用多步迁移。每个迁移只做一件事,比如加字段、建新表、改索引。这样出了问题也好排查。

// 多步迁移示例:从版本1到3
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("CREATE TABLE IF NOT EXISTS `address` (`id` INTEGER, `userId` INTEGER, PRIMARY KEY(`id`))")
    }
}

// 在Room.databaseBuilder中注册
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .build()

你可能会问:那用户从版本1直接升到3怎么办?Room会自动把MIGRATION_1_2MIGRATION_2_3串起来执行,你完全不用操心。

11.3 测试迁移:别等上线才发现问题

迁移代码写完了,怎么保证它是对的?我见过太多人直接跑App测,结果数据丢了才慌。其实Room提供了专门的测试方案。

核心思路是:创建一个旧版本的数据库,插入测试数据,然后执行迁移,最后验证数据是否完整

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB_NAME = "migration-test"

    @Test
    fun migrate1To2_containsCorrectData() {
        // 创建版本1的数据库
        val db = Room.databaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java,
            TEST_DB_NAME
        ).build()

        // 插入测试数据
        db.userDao().insert(User(1, "张三", 25))
        db.close()

        // 执行迁移到版本2
        val migratedDb = Room.databaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java,
            TEST_DB_NAME
        ).addMigrations(MIGRATION_1_2)
        .build()

        // 验证数据
        val users = migratedDb.userDao().getAll()
        assertEquals(1, users.size)
        assertEquals("张三", users[0].name)
        assertEquals(25, users[0].age) // 新字段有默认值

        migratedDb.close()
    }
}

我曾经因为没测迁移,导致用户升级后所有时间字段都变成了1970-01-01。那叫一个惨。所以现在我的习惯是:每次数据库版本变更,必须写对应的迁移测试。CI流水线里也加上了这个步骤。

注意:测试迁移时,一定要用allowMainThreadQueries()或者用runBlockingTest,否则Room会报错。另外,测试完记得删掉测试数据库文件。

11.4 破坏性迁移:fallbackToDestructiveMigration

有些场景下,你不想写迁移代码。比如开发阶段频繁改表结构,或者旧数据已经没用了。这时候可以用fallbackToDestructiveMigration()

这个方法的意思是:如果Room找不到合适的迁移路径,就直接删掉旧表重建。说白了就是暴力重置。

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .fallbackToDestructiveMigration()
    .build()

但这里有个大坑:生产环境千万别用。我见过有人上线前忘了去掉这行代码,结果用户升级后数据全没了,直接被骂上热搜。

那什么时候可以用?我个人觉得就两个场景:

  • 开发调试阶段:频繁改表结构,懒得写迁移
  • 测试环境:数据不重要,快速验证功能

如果你非要保留一些数据,Room还提供了fallbackToDestructiveMigrationOnVersion(int... versions),可以指定哪些版本允许破坏性迁移。

// 只在从版本1升级时允许破坏性迁移
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .fallbackToDestructiveMigrationOnVersion(1)
    .build()

这样至少能控制风险范围,不至于一棍子打死所有用户。

11.5 知识体系总览

说了这么多,咱们用一张图把整个迁移体系串起来:

Room数据库迁移知识体系 数据库迁移 @Migration注解 迁移策略 破坏性迁移 version1 → version2 execSQL() 执行SQL 单步迁移 vs 多步迁移 addMigrations() 注册 fallbackToDestructiveMigration() 慎用于生产环境 迁移测试:验证数据完整性

从图上可以看得很清楚:迁移的核心是@Migration注解,策略上分单步和多步,实在不行才用破坏性迁移兜底。但不管哪种方式,最后都得靠测试来验证。

避坑指南:我曾经在生产环境用过一次fallbackToDestructiveMigration(),原因是赶工期没时间写迁移。结果用户数据全丢了,被产品经理追着骂了一个月。所以我的建议是:永远不要在Release版本里用破坏性迁移。如果实在没办法,至少加个版本限制。

好了,数据库迁移这块就聊到这儿。记住一句话:迁移写得好,用户数据保;迁移写不好,上线就挨刀。下回咱们聊聊Room里的类型转换器,那个也挺有意思的。


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