单表架构变更与自动迁移:添加/删除列、修改列类型、使用AutoMigration注解

在实际迭代中,数据库模型的变更(添加字段、删除字段甚至修改列类型)是最频繁的操作。Room 从 2.4.0 开始提供 AutoMigration 注解,让开发者无需手写 Migration 类即可处理“可安全推断”的单表变更。本章深入讲解自动迁移的原理、使用流程、边界条件及常见问题。

自动迁移的原理

Room 在编译期对比当前 @Entity 声明的 schema 与数据库中实际存储的 schema(基于前一个版本号),生成差异报告。若差异仅属于“可自动推断”的变更(添加列、删除列、修改列类型且兼容),则自动生成对应的 Migration 实现;否则编译报错,要求开发者手写迁移。

适用规则:自动迁移只处理单表内字段数的增减和类型向“宽”方向的修改(如 INTEGER → TEXT 可推断,但 TEXT → INTEGER 不可;添加列必须允许 NULL 或有默认值;删除列必须确认不存在约束依赖)。
Room AutoMigration 自动迁移流程图 旧版本 @Entity (version=1) 新版本 @Entity (version=2) 编译期对比 Schema差异分析 是否可自动推断? 是 自动生成 MigrationImpl 否 → 编译报错 自动迁移支持:添加/删除列、 可兼容的类型修改 @AutoMigration (from=1, to=2)

实现流程

  1. 声明新版本号:在 @Database 注解中递增 version 字段。
  2. 修改 Entity 类:按需求添加/删除字段,或变更字段类型(仅支持“兼容”方向)。
  3. 添加 @AutoMigration 注解:在 @Database 的 autoMigrations 属性中声明从旧版本到新版本的迁移映射。
  4. 构建验证:编译项目,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 变更后迁移逻辑正确。