1. 数据库版本概念与迁移场景
1.1 版本号理解
Room 使用 整型版本号 标识数据库 Schema。每次在 @Database 注解中声明的 version 值改变,Room 自动触发迁移流程。版本号必须严格递增,低版本代表旧 Schema,高版本代表新 Schema。
| 概念 | 说明 |
|---|---|
| 当前版本 | 用户设备上已安装数据库的版本号 |
| 目标版本 | 新 APP 代码中 @Database(version=…) 声明的版本号 |
| 迁移流程 | Room 从当前版本依次执行到目标版本的迁移步骤 |
典型声明示例:
@Database(
entities = [User::class, Task::class],
version = 3,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() { … }
exportSchema = true 会生成 JSON Schema 文件,便于对比版本差异。
1.2 迁移触发条件
当 APP 启动时,Room 检测到 当前数据库版本 < 目标版本 且存在合法 Migration 对象时,自动执行迁移。若不存在对应 Migration,则 抛出 IllegalStateException 并强制用户卸载重装(默认 FallbackToDestructiveMigration 除外)。
- 版本不匹配:设备上数据库 version ≠ 代码中 version。
- Migration 缺失:未注册从旧版到新版的 Migration 实例。
- 降级禁止:Room 不支持降级,必须卸载重装或使用 destructive 回退。
关键避坑: 若多次修改 Schema 但未定义对应 Migration,用户升级时将直接崩溃。始终在 addMigrations() 中注册所有历史迁移路径。
1.3 常见业务场景
| 场景 | 变化内容 | 迁移策略 |
|---|---|---|
| 新增表 | 添加新实体类 | CREATE TABLE … |
| 新增列 | 字段非空且带默认值 | ALTER TABLE ADD COLUMN … DEFAULT … |
| 修改列类型 | 改变字段数据类型 | 创建临时表 + 数据复制 + 删除旧表 + 重命名 |
| 删除表/列 | 移除实体或字段 | 直接 DROP TABLE 或使用 DROP COLUMN(SQLite 3.35+) |
| 重命名表/列 | 名称变更 | ALTER TABLE RENAME TO / COLUMN RENAME TO |
| 添加索引 | 提升查询性能 | CREATE INDEX … |
1.4 迁移流程全景图
下图展示了从版本 1 到版本 3 的迁移触发与执行路径,包含 版本校验、迁移查找、逐步执行 三个核心阶段。
1.5 代码与 API 要点
在 Room 中注册迁移的典型做法如下:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE Task (id INTEGER PRIMARY KEY, name TEXT)")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "mydb")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
1.6 调试与验证
开启 Room 调试日志可实时查看迁移 SQL:
adb shell setprop log.tag.Room VERBOSE
# 或使用 RoomDatabase.Builder 的 enableMultiInstanceInvalidation() 配合日志
建议在单元测试中编写 MigrationTestHelper 验证迁移正确性:
@RunWith(AndroidJUnit4::class)
class MigrationTest {
private val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1to2() {
val db = helper.createDatabase("testdb", 1)
db.execSQL("INSERT INTO User (id, name) VALUES (1, 'test')")
val migratedDb = helper.runMigrationsAndValidate("testdb", 2, true, MIGRATION_1_2)
// 校验新列 age 存在且默认为0
}
}
1.7 常见错误与稳定性优化
| 错误 | 原因 | 解决方案 |
|---|---|---|
IllegalStateException: Migration not found | 未注册所有迁移路径 | 添加缺失的 Migration 或启用 fallbackToDestructiveMigration() |
SQLiteConstraintException 迁移中 | 新增列未设置默认值,已有数据违反非空约束 | ALTER TABLE 时指定 DEFAULT 值,或先增加 nullable 列再填充 |
| 迁移耗时过长 ANR | 大数据量复制或复杂数据转换 | 将迁移拆分为多个事务,或使用 beginTransaction / setTransactionSuccessful |
| Schema 校验失败 | exportSchema 生成的 JSON 与实际迁移结果不一致 | 删除 JSON 缓存并重新构建,确保迁移 SQL 严格匹配目标 Schema |
性能优化建议:
- 批量 DDL 语句放在同一个
migrate()方法中,减少事务开销。 - 对于大型表的重构,使用
INSERT INTO … SELECT …一次性迁移数据,避免逐行操作。 - 在
@Database中设置exportSchema = true并配合 CI 检查 Schema 差异,防止未定义迁移的版本升级。