单表架构变更与自动迁移:添加/删除列、修改列类型、使用AutoMigration注解
在实际迭代中,数据库模型的变更(添加字段、删除字段甚至修改列类型)是最频繁的操作。Room 从 2.4.0 开始提供 AutoMigration 注解,让开发者无需手写 Migration 类即可处理“可安全推断”的单表变更。本章深入讲解自动迁移的原理、使用流程、边界条件及常见问题。
自动迁移的原理
Room 在编译期对比当前 @Entity 声明的 schema 与数据库中实际存储的 schema(基于前一个版本号),生成差异报告。若差异仅属于“可自动推断”的变更(添加列、删除列、修改列类型且兼容),则自动生成对应的 Migration 实现;否则编译报错,要求开发者手写迁移。
适用规则:自动迁移只处理单表内字段数的增减和类型向“宽”方向的修改(如 INTEGER → TEXT 可推断,但 TEXT → INTEGER 不可;添加列必须允许 NULL 或有默认值;删除列必须确认不存在约束依赖)。
实现流程
- 声明新版本号:在
@Database注解中递增version字段。 - 修改 Entity 类:按需求添加/删除字段,或变更字段类型(仅支持“兼容”方向)。
- 添加 @AutoMigration 注解:在
@Database的autoMigrations属性中声明从旧版本到新版本的迁移映射。 - 构建验证:编译项目,Room 会自动生成
AutoMigration_1_2_Impl类并注册到RoomDatabase。
代码示例:添加列 + 删除列 + 类型修改
1. 原 Entity (version=1)
@Entity(tableName = "users")
data class UserV1(
@PrimaryKey val id: Long,
val name: String,
val age: Int
)
2. 修改后 Entity (version=2)
@Entity(tableName = "users")
data class UserV2(
@PrimaryKey val id: Long,
val name: String,
val nickname: String?, // 新增列,可为 null
@ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val email: String // 原来可能为 INTEGER?此处改为 TEXT(兼容方向)
)
注意:age 字段被删除;nickname 为新增可空列;email 从 INTEGER 改为 TEXT(Room 认为 INTEGER→TEXT 是兼容的,但 TEXT→INTEGER 会报错)。
3. 配置 AutoMigration
@Database(
entities = [UserV2::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
4. 编译生成的迁移日志(构建后查看 build 目录)
AutoMigration_1_2_Impl.java // 自动生成
// 核心逻辑:
// ALTER TABLE users ADD COLUMN nickname TEXT NULL
// ALTER TABLE users ADD COLUMN email TEXT
// 删除列通过重建表实现,但 Room 自动处理了 DROP 和重命名
调试与验证
- 开启 Room 日志:设置
allowMainThreadQueries()和 SQLite 日志:Room.databaseBuilder(context, AppDatabase::class.java, "db").fallbackToDestructiveMigration(false).openHelperFactory(...)。 - 查看生成的 Migration 文件:在
build/generated/source/room/目录下找到AutoMigration_1_2_Impl.java,检查其生成的 SQL 是否符合预期。 - 单元测试:使用
MigrationTestHelper验证迁移前后数据是否完整。
@RunWith(AndroidJUnit4::class)
class AutoMigrationTest {
private val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun autoMigrateAddColumn() {
val db = testHelper.createDatabase("test.db", 1).apply {
execSQL("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 25)")
}
db.close()
val db2 = testHelper.runMigrationsAndValidate("test.db", 2, true)
val cursor = db2.query("SELECT * FROM users")
cursor.moveToFirst()
assertEquals("Alice", cursor.getString(cursor.getColumnIndex("name")))
assertEquals(null, cursor.getString(cursor.getColumnIndex("nickname"))) // 新列为 null
}
}
常见错误与避坑
| 错误场景 | 具体表现 | 解决方案 |
|---|---|---|
| 新增列不允许 NULL 且无默认值 | 编译失败:AutoMigration failed: column X does not have a default value | 将新列设为可空或指定 @ColumnInfo(defaultValue = "...") |
| 修改列类型为不兼容方向(如 TEXT→INTEGER) | 编译失败:Cannot resolve auto migration | 手写 Migration 类,使用 SQL 重建表 |
| 删除的列被其他表的外键引用 | 运行时迁移失败,抛出 SQLiteConstraintException | 先删除外键约束,或手写迁移处理级联 |
| 同时修改多个表的结构 | 编译期可能合并生成多个 AutoMigration,但若一个失败则全部中断 | 拆分成多个版本逐步迁移,或统一手写迁移 |
性能与稳定性优化
- 避免大表频繁增减列:自动迁移在删除列时会重建整张表(实际上执行
CREATE TABLE IF NOT EXISTS ...+INSERT ... SELECT+DROP TABLE),对于百万行级表需预留足够的 WAL 空间和超时时间。 - 使用 WAL 模式:在迁移前确保数据库为 WAL 模式,可减少锁竞争。
- 批量迁移:若一次版本跨越多个版本,Room 会自动按序执行所有 AutoMigration;避免每次都触发重建,可考虑将多次变更合并到一个版本中以减少重建次数。
- 监控迁移耗时:在
roomDatabaseBuilder中注册MigrationCallback记录 start/end 时间,帮助定位慢迁移。
⚠ 慎用破坏性回退:
fallbackToDestructiveMigration() 会直接删除旧数据,生产环境绝不能开启。自动迁移失败时优先手写 Migration 而不是启用破坏性回退。
部署要点
- 上线前务必对已有数据的真实设备进行迁移测试,覆盖从任意旧版本升入新版本的场景。
- 若使用混淆,需确保
@AutoMigration注解及生成的类不被混淆。 - 在 CI 中集成
MigrationTestHelper自动化测试,保证每次 schema 变更后迁移逻辑正确。