4. 自动迁移(Auto Migration)入门

说实话,Room 刚推出那两年,写手动迁移是真的痛苦。每次数据库版本升级,你得手写一个Migration对象,里面塞满了`ALTER TABLE`、`CREATE INDEX`这些SQL。改错一个字段名,整个App就崩了。后来Room 2.4加入了Auto Migration,我当时心里想:这玩意儿靠谱吗?

现在用了两年多了,我的结论是:对于大多数数据模型的变动,Auto Migration已经足够好用。它不是万能药,但能让你的迁移代码减少七八成。

4.1 什么是自动迁移?

简单说,Room会自动对比你定义的Entity版本和新版Entity的差异。如果差异仅限于添加/删除列、修改列默认值、添加/删除索引、添加/删除外键这几种情况,Room就能自动生成对应的SQL语句,无需你手写一行`ALTER`。

核心机制: Room在编译期通过注解处理器对比两个版本的Entity结构,生成对应的AutoMigrationSpec实现类。运行时,Room会调用这个实现类来执行迁移。

我们来看一个典型场景。比如你有这样一个Entity:

@Entity(tableName = "users")
data class UserV1(
    @PrimaryKey val id: Long,
    val name: String,
    val age: Int
)

现在要升级到V2,加一个邮箱字段:

@Entity(tableName = "users")
data class UserV2(
    @PrimaryKey val id: Long,
    val name: String,
    val age: Int,
    val email: String? = null
)

传统手动迁移要写:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN email TEXT DEFAULT NULL")
    }
}

而自动迁移模式下,你只需要在Database类上声明一下:

@Database(
    version = 2,
    entities = [UserV2::class],
    autoMigrations = [
        AutoMigration(from = 1, to = 2)
    ]
)
abstract class AppDatabase : RoomDatabase() { ... }

剩下的Room全帮你干了。是不是清爽很多?

4.2 自动迁移的声明方式

你需要用@AutoMigration注解,指定from版本号和to版本号。记住三个要点:

  • 版本号必须和@Database(version = ...)里的版本对齐。
  • 只能声明连续的版本迁移,比如1→2、2→3,不能直接1→3(除非中间版本没变化)。
  • 多个自动迁移可以写在一起:
@Database(
    version = 3,
    entities = [UserV3::class],
    autoMigrations = [
        AutoMigration(from = 1, to = 2),
        AutoMigration(from = 2, to = 3)
    ]
)

我个人习惯在项目里把所有自动迁移排成一个链条。这样Room会按顺序逐个执行,也很容易在出问题时定位到具体版本。

小技巧: 如果你的自动迁移数量超过两三个,可以考虑写一个常量类来管理所有迁移配置。比如object MigrationVersions { const val V1 = 1; const val V2 = 2 },这样不容易写错版本号。

4.3 什么情况下自动迁移失效?

嗯,这里要特别提醒你。自动迁移不是万能的,它只支持以下几种变更:

支持的操作 不支持的操作
添加列(可空或带默认值) 重命名列
删除列 重命名表
修改列默认值 合并/拆分表
添加/删除索引 修改列的非空约束
添加/删除外键 修改列的数据类型
添加/删除@ForeignKey、@Index 任何涉及数据迁移的复杂逻辑

我曾经踩过一个坑:把V1里一个Int类型的列改成了Long,Room直接编译报错,提示这种变更不在自动迁移支持范围内。当时我还想偷懒,结果只能乖乖写手动迁移。

还有一个易疏忽的点:如果Entity里新增的列没有指定默认值,且不是可空类型,自动迁移也会失败。因为新列在旧表中不存在,Room必须知道怎么填充旧数据这一列的值。所以最好的做法是:新增列一律声明为可空,或者给它一个非空的默认值。

重要: 自动迁移不处理任何数据转换。如果旧数据需要重映射(例如把旧字段拆成两个新字段),你必须自己写手动迁移。千万别指望自动迁移能帮你迁移数据业务逻辑。

4.4 自动迁移的内部原理(了解即可)

你不需要深究每个细节,但理解大致流程对排查问题有帮助。Room在编译期会:

  1. 读取@Database中声明的autoMigrations列表。
  2. 对比from版本和to版本的Entity定义差异。
  3. 生成一个AutoMigrationSpec的实现类,里面包含了所需的SQL变更语句。
  4. 把这个实现类注册到MigrationContainer中。

运行时Room打开数据库时,检测到当前版本低于期望版本,会查找对应的Migration或AutoMigrationSpec,并按顺序执行。

下面这张图帮你理清整个自动迁移的流程:

自动迁移(Auto Migration)核心流程 数据库版本变更 声明 @AutoMigration 注解 编译期解析Entity差异 变更是否支持? 是 否 生成 AutoMigrationSpec 运行时自动执行迁移 需要手动迁移 注意: 不支持重命名、 数据类型变更等操作

4.5 自动迁移的限制与避坑

我再补充几个实战中容易碰壁的地方:

  • 不能处理@Embedded嵌套对象的变更。 如果你重构了Embedded类的字段(比如把一个类移到另一个类里),自动迁移无法感知,你必须手动处理。
  • 不支持表名变更。 我刚开始以为把tableName改了就能自动迁移,结果Room直接报错说从1到2的迁移找不到对应表。后来才意识到,Room是通过表名来匹配Entity的,改表名相当于删表重建——必须手动。
  • @Ignore字段的变更不受影响。 被@Ignore标记的字段不会持久化,所以它们的变化自动迁移不会管。
  • 外键约束变更要小心。 在旧数据库里如果存在违反新外键的数据,自动迁移执行时会直接崩溃。所以强烈建议在自动迁移前先做数据清理。

我曾经遇到的坑: 有一次我删除了一个@ForeignKey,自动迁移成功执行了。但后来发现旧数据里残留了一些被外键约束保护的外键记录,导致后续查询出现奇怪的异常。从那以后,只要有外键相关的变更,我都会先手动检查一遍数据完整性。

4.6 什么时候该用自动迁移?什么时候该手动?

我的建议很简单:

  • 如果只是加字段删字段、改默认值、加索引——大胆用自动迁移,省时省力。
  • 如果需要移动数据、合并表、改列类型、重命名——必须手动迁移,别偷懒。
  • 如果自动迁移和手动迁移混在一起(比如1→2自动,2→3手动),完全没问题,Room会按照你声明的顺序依次执行。只要版本号连续就行。

举个混合使用的例子:

@Database(
    version = 3,
    entities = [UserV3::class],
    autoMigrations = [
        AutoMigration(from = 1, to = 2)
    ]
)
abstract class AppDatabase : RoomDatabase() {
    companion object {
        val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                // 手动处理重命名列等复杂操作
            }
        }
    }
}

然后在构建时把MIGRATION_2_3也传进去:

Room.databaseBuilder(...)
    .addMigrations(MIGRATION_2_3)
    .build()

Room会把1→2的自动迁移和2→3的手动迁移链式执行。

4.7 总结几个关键点

  • 自动迁移依赖编译期注解处理器,确保你的build.gradle里配置了kapt或ksp。
  • 版本号必须连续,不能跳号。
  • 新增列如果是非空的,必须给默认值,否则编译报错。
  • 自动迁移不会产生日志,但你可以开启Room的日志来观察执行过程。

我个人觉得自动迁移最大的价值是减少了手工写重复SQL的次数。只要你在设计Entity时稍微注意一下可迁移性(比如新字段加默认值、不轻易改类型),大部分日常升级都能无缝搞定。当然,复杂业务还是要老老实实手动迁移,那部分我会在后面章节详细讲。