Android Room 数据库踩坑:手动升级和自动升级一起使用导致数据库报错

255 阅读4分钟

在最近的项目开发中,我有个产品需求,需要修改到原来的数据库,增加一个字段,对于数据库操作原本以为很简单,但是没想到踩了一个巨大的坑,今天有空对这个问题进行分析,在这过程中学习到了很多,成长的代价总是惨痛的。

图片

一、问题背景

在项目迭代中,我们需要对数据库进行升级,为一张表新增一个字段:

@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 数据库在升级时,遵循以下迁移执行顺序:

  1. 优先执行自动迁移
    Room 会根据 autoMigrations 配置自动完成表结构变更。
  2. 随后执行手动迁移
    如果同一区间的版本还配置了手动迁移,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 会依次执行以下步骤:

  1. 自动迁移 9 → 10
  2. 自动迁移 10 → 11
  3. 手动迁移 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 没有提供机制来防止以下情况:

  1. 自动迁移与手动迁移的逻辑冲突:两个迁移同时作用于同一个版本区间。
  2. 自动迁移结果与手动迁移前提条件不一致:手动迁移未意识到自动迁移已更新表结构。

自动迁移生成代码示例

@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 的冲突检测不足。在实际开发中,我们应根据以下建议优化迁移设计:

  1. 优先选择适合当前变更的迁移方式。
  2. 确保自动迁移与手动迁移的区间不重叠。
  3. 在发布前充分测试所有可能的升级路径。

通过合理设计迁移逻辑,可以避免问题复现,确保数据库升级的安全性与一致性。

原文地址:mp.weixin.qq.com/s/N7VDL-Y8n…