1. Room 数据库版本机制与迁移基础

本章是《Room 数据库迁移与版本管理实操》的基石。我们将彻底拆解 Room 的版本控制哲学:为什么需要版本号?fallbackToDestructiveMigration 究竟是“逃生舱”还是“数据毁灭者”?迁移失败时 Room 又会如何反应?掌握这些底层机制,才能安全、稳健地演进你的本地数据库架构。

核心要点:版本号是数据库 Schema 的“快照哈希”;迁移失败默认抛异常而非静默降级;fallbackToDestructiveMigration 是最后手段,慎用。

1.1 版本号管理:数据库 Schema 的身份证

Room 通过 @Database(version = ...) 声明数据库版本号。每次实体(Entity)结构、字段类型、索引、外键等 Schema 变化,必须递增版本号。Room 在编译期生成 _Impl 类,运行时对比当前已打开的数据库版本与声明版本:

  • 版本一致:直接正常打开。
  • 版本不一致:触发迁移逻辑。
元素说明
@Database(version = 1)初始版本,通常对应首个 Release
version = 2首次 Schema 变更,必须提供 Migration(1, 2)
version = N每次变更递增 1,建议使用当前 Release 编号或独立 DB_VERSION 常量
fallbackToDestructiveMigration()未匹配到 Migration 时的降级开关,默认关闭
常见错误:只递增版本号却忘记添加 Migration 实现,且未开启 fallback。后果——IllegalStateException 应用直接崩溃。

1.2 fallbackToDestructiveMigration:毁灭性回退的代价

当 Room 找不到从当前版本到目标版本的迁移路径时,默认行为是抛出 IllegalStateException 并终止数据库打开。若你显式调用 fallbackToDestructiveMigration(),Room 会将旧数据库文件清空,然后按最新 Schema 重建一个新数据库。数据全部丢失。

// 开启毁灭性回退的典型配置
@Database(entities = [User::class], version = 3)
abstract class AppDatabase : RoomDatabase() {
    companion object {
        fun getInstance(context: Context): AppDatabase {
            return Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "myapp.db"
            )
            .fallbackToDestructiveMigration()   // 不推荐用于生产!
            .build()
        }
    }
}

1.3 迁移失败时的默认行为:严格模式

Room 默认采用严格迁移验证。迁移过程包括两个阶段:

  1. 准备阶段:验证 Migration 对象是否存在,版本号是否合法。
  2. 执行阶段:运行 Migration.migrate(),提交事务。

若 migrate 中抛出 SQL 异常(如 SQLiteConstraintException 约束冲突、表已存在等),Room 会回滚整个迁移事务并向上层抛出异常,数据库保持迁移前的旧版本。绝不会部分应用迁移。

调试技巧:在 migrate() 内使用 Log.d 打印当前 SQL,或通过 .openHelperFactory(SupportFactory()) 拦截 SQL 执行日志。

1.4 版本机制与迁移关系流程图

下图清晰展示了 Room 启动时版本比对以及迁移调用的完整路径:

Room 版本比对与迁移调用流程 应用启动 读取当前数据库 version 同一版本? 正常打开数据库 寻找 Migration 对象 找到 Migration? 执行 migrate() 是否开启 fallback? 毁灭性重建 抛出 IllegalStateException 版本不一致时,Room 不会 自动覆盖或降级数据库, 必须显式提供 Migration。

1.5 常见错误与稳定性优化

  • 错误:版本号回退——从 version=3 降到 version=2 会导致 Room 抛 IllegalStateException,不允许降级。
  • 错误:并发迁移——多线程同时调用数据库构建,可能多次执行迁移。应使用单例模式 + 同步锁。
  • 优化:显式版本常量——定义 object DBVersion { const val V1 = 1; const val V2 = 2 },避免硬编码。
  • 优化:预检 Schema 差异——使用 @Database(exportSchema = true) 导出 JSON Schema,通过 diff 工具检查预期与实际变更是否一致。
生产建议:始终关闭 fallbackToDestructiveMigration()。使用 .addMigrations() 精确描述每次 Schema 变更。若开发阶段希望快速重建,可使用 .fallbackToDestructiveMigrationOnDowngrade() 控制降级行为。