https://mp.weixin.qq.com/s/N7VDL-Y8nzDHVriwpwT0RQ
在最近的项目开发中,我有个产品需求,需要修改到原来的数据库,增加一个字段,对于数据库操作原本以为很简单,但是没想到踩了一个巨大的坑,今天有空对这个问题进行分析,在这过程中学习到了很多,成长的代价总是惨痛的。
在项目迭代中,我们需要对数据库进行升级,为一张表新增一个字段:
@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 会根据
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)操作时,就会导致冲突。
自动迁移会通过表重建完成变更,具体过程如下:
-- 创建新表,包含新增字段 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`;
自动迁移完成后,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
四、问题原因
自动迁移和手动迁移在同一区间内同时操作了相同字段
new_column_name:
-- 自动迁移已包含该字段 CREATE TABLE `_new_table_name` ( ... new_column_name INTEGER NOT NULL DEFAULT 1 ); -- 手动迁移再次尝试添加同字段 ALTER TABLE table_name ADD COLUMN new_column_name INTEGER NOT NULL DEFAULT 1; -- 冲突
手动迁移的逻辑是基于旧的表结构设计的,而自动迁移在执行完成后已更新表结构,导致手动迁移的操作无效或冲突。
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 的冲突检测不足。在实际开发中,我们应根据以下建议优化迁移设计:
优先选择适合当前变更的迁移方式。
确保自动迁移与手动迁移的区间不重叠。
在发布前充分测试所有可能的升级路径。
通过合理设计迁移逻辑,可以避免问题复现,确保数据库升级的安全性与一致性。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3058812/ ,如需转载,请注明出处,否则将追究法律责任。