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在编译期会:
- 读取
@Database中声明的autoMigrations列表。 - 对比
from版本和to版本的Entity定义差异。 - 生成一个
AutoMigrationSpec的实现类,里面包含了所需的SQL变更语句。 - 把这个实现类注册到
MigrationContainer中。
运行时Room打开数据库时,检测到当前版本低于期望版本,会查找对应的Migration或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时稍微注意一下可迁移性(比如新字段加默认值、不轻易改类型),大部分日常升级都能无缝搞定。当然,复杂业务还是要老老实实手动迁移,那部分我会在后面章节详细讲。