在最近的项目开发中,我有个产品需求,需要修改到原来的数据库,增加一个字段,对于数据库操作原本以为很简单,但是没想到踩了一个巨大的坑,今天有空对这个问题进行分析,在这过程中学习到了很多,成长的代价总是惨痛的。
一、问题背景
在项目迭代中,我们需要对数据库进行升级,为一张表新增一个字段:
@ColumnInfo(defaultValue = "1") var status: Int = 1,
起初认为这只是一次普通的字段更新,但问题却远比想象中复杂。我们在版本 10 新增了表,在版本 11 为该表新增字段。直接从线上版本(10)升级到最新版本(11)一切正常,但从更老的版本(如 9)升级到版本 11 时却爆出了以下异常:
android.database.sqlite.SQLiteException: duplicate column name: newColumn (Sqlite code 1 SQLITE_ERROR): , while compiling: ALTER TABLE target_table ADD COLUMN newColumn INTEGER NOT NULL DEFAULT 1, (OS error - 2: No such file or directory)
这个问题背后的原因正是 Room 的自动迁移与手动迁移同时启用导致冲突。
二、Room 的迁移执行顺序
Room 数据库在升级时,遵循以下迁移执行顺序:
- 优先执行自动迁移:
Room 会根据autoMigrations配置自动完成表结构变更。 - 随后执行手动迁移:
如果同一区间的版本还配置了手动迁移,Room 不会跳过,而是继续执行手动迁移逻辑。
我们的问题代码如下:
@Database(
version = 11,
autoMigrations = [
AutoMigration(from = 9, to = 10), // 自动迁移版本区间:9到10
AutoMigration(from = 10, to = 11) // 自动迁移版本区间:10到11
]
)
abstract class AppDataBase : RoomDatabase() {
companion object {
val MIGRATION_10_11 = object : Migration(10, 11) { // 手动迁移10到11
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE table_name ADD COLUMN new_column_name INTEGER NOT NULL DEFAULT 1")
}
}
}
}
在这种情况下,从版本 9 → 11 升级时,Room 会依次执行以下步骤:
- 自动迁移
9 → 10。 - 自动迁移
10 → 11。 - 手动迁移
10 → 11。
当自动迁移和手动迁移针对相同字段(new_column_name)操作时,就会导致冲突。
三、问题复现分析
1. 自动迁移的操作
自动迁移会通过表重建完成变更,具体过程如下:
-- 创建新表,包含目标字段
CREATE TABLE IF NOT EXISTS `_new_table_name` (
id INTEGER PRIMARY KEY,
column_1 TEXT,
new_column_name INTEGER NOT NULL DEFAULT 1
);
-- 将旧表数据迁移到新表
INSERT INTO `_new_table_name` (id, column_1)
SELECT id, column_1 FROM `table_name`;
-- 删除旧表
DROP TABLE `table_name`;
-- 重命名新表为旧表
ALTER TABLE `_new_table_name` RENAME TO `table_name`;
2. 手动迁移的操作
自动迁移完成后,Room 会继续执行手动迁移:
ALTER TABLE table_name ADD COLUMN new_column_name INTEGER NOT NULL DEFAULT 1;
此时,由于自动迁移已包含 new_column_name 字段,手动迁移尝试再次添加该字段,导致以下错误:
android.database.sqlite.SQLiteException: duplicate column name: new_column_name
四、问题原因
1. 重复操作
自动迁移和手动迁移在同一区间内同时操作了相同字段 new_column_name:
-- 自动迁移已包含 new_column_nameCREATE TABLE `_new_table_name` ( ... new_column_name INTEGER NOT NULL DEFAULT 1);
-- 手动迁移再次尝试添加 new_column_nameALTER TABLE table_name ADD COLUMN new_column_name INTEGER NOT NULL DEFAULT 1; -- 重复操作导致冲突
2. 状态不一致
手动迁移的逻辑是基于旧的表结构设计的,而自动迁移在执行完成后已更新表结构,导致手动迁移的操作无效或冲突。
五、Room 的设计缺陷
缺乏冲突检测机制
Room 没有提供机制来防止以下情况:
- 自动迁移与手动迁移的逻辑冲突:两个迁移同时作用于同一个版本区间。
- 自动迁移结果与手动迁移前提条件不一致:手动迁移未意识到自动迁移已更新表结构。
自动迁移生成代码示例
@Generated("androidx.room.RoomProcessor")
class AutoMigration_10_11_Impl : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
// 完整的表重建过程
}
}
手动迁移示例:
val MIGRATION_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE table_name ADD COLUMN new_column_name...")
}
}
两者在迁移过程中没有明确的优先级或冲突解决机制。
六、总结
自动迁移与手动迁移的混用容易引发操作冲突,其核心问题在于迁移逻辑的重叠与 Room 的冲突检测不足。在实际开发中,我们应根据以下建议优化迁移设计:
- 优先选择适合当前变更的迁移方式。
- 确保自动迁移与手动迁移的区间不重叠。
- 在发布前充分测试所有可能的升级路径。
通过合理设计迁移逻辑,可以避免问题复现,确保数据库升级的安全性与一致性。