Android Room 数据库版本升级踩坑:解决跨版本升级冲突

0 阅读5分钟

一、问题背景

在最近的项目开发中,我遇到了一个严重的数据库升级问题。项目的数据库在版本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 - 2No such file or directory)

解决问题的核心在于两个不同的升级路径:

  1. 版本9 → 版本11:需要完整的跨版本迁移。
  2. 版本10 → 版本11:只需要为表添加字段。

查阅代码后发现,问题的发生点在于我同时使用了autoMigrations和addMigrations手动升级,在addMigrations中执行了

       private val MIGRATION_10_11 = object : Migration(10, 11) {
            override fun migrate(database: SupportSQLiteDatabase) {
                // 为表新增字段
                database.execSQL(
                    "ALTER TABLE target_table " +
                    "ADD COLUMN newColumn INTEGER NOT NULL DEFAULT 1"
                )
            }
        }

但是在我去掉addMigrations,全部使用autoMigrations后发现,版本10升级版本11出现了缺少这个字段的问题,这篇文章我主要想讲如何解决出现这个问题时,如何解决线上版本9和版本10的用户升级版本11的处理方案

二、问题分析

  1. 初步尝试:全部使用自动迁移

在 Room 自动迁移机制下,自动生成了以下代码:

@Generated("androidx.room.RoomProcessor")
@SuppressWarnings({"unchecked", "deprecation"})
class AppDataBase_AutoMigration_10_11_Impl extends Migration {
  public AppDataBase_AutoMigration_10_11_Impl() {
    super(10, 11);
  }

  @Override
  public void migrate(@NonNull final SupportSQLiteDatabase database) {
    // 创建新表并增加新字段
    database.execSQL("CREATE TABLE IF NOT EXISTS `_new_target_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `column1` INTEGER NOT NULL, `column2` TEXT, `newColumn` INTEGER NOT NULL DEFAULT 1)");
    // 将旧表数据迁移到新表
    database.execSQL("INSERT INTO `_new_target_table` (`id`,`column1`,`column2`,`newColumn`) SELECT `id`,`column1`,`column2`,`newColumn` FROM `target_table`");
    // 删除旧表
    database.execSQL("DROP TABLE `target_table`");
    // 重命名新表为旧表名
    database.execSQL("ALTER TABLE `_new_target_table` RENAME TO `target_table`");
  }
}

结果

  • 如果用户直接从版本9升级到版本11,Room 会提示 newColumn 字段不存在。
  • 如果使用手动迁移插入 newColumn 字段,又会导致从版本10升级到版本11时出现 duplicate column name 错误。

根本原因:Room 的自动迁移无法兼顾跨版本升级的不同路径。

2. SQLite 限制与迁移策略选择

为什么 Room 自动迁移需要创建新表?

SQLite 对 ALTER TABLE 的操作有以下限制:

  • 添加新列时,若该列是非空字段且设置了默认值,SQLite 会尝试给所有现有行赋值。如果这个过程失败,会导致迁移中断。
  • Room 的自动迁移策略选择创建新表并迁移数据,以确保迁移的原子性和数据一致性。

为什么手动迁移也会失败?

手动迁移本质上是直接操作 SQL,但在跨版本路径中,由于不同版本可能已经存在某些字段或表结构差异,直接执行迁移逻辑会造成冲突。例如:

  • 从版本9升级到版本11时,需要新增字段 newColumn
  • 从版本10升级到版本11时,newColumn 字段已经存在,再次添加会报错

2. SQLite 限制与迁移策略选择

三、解决方案

经过实践和分析,我采取了分版本处理策略,针对不同的升级路径分别设计手动迁移逻辑。

方案:分版本手动迁移

针对 Room 的局限性,手动处理跨版本迁移逻辑,以避免字段冲突和自动迁移的问题。

@Database(
    entities = [...],
    version = 11
)
abstract class AppDataBase : RoomDatabase() {
    companion object {
        // 从版本9直接升级到版本11
        private val MIGRATION_9_11 = object : Migration(9, 11) {
            override fun migrate(database: SupportSQLiteDatabase) {
                // 创建包含所有字段的新表
                database.execSQL("""
                    CREATE TABLE IF NOT EXISTS `target_table` (
                        `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                        `column1` INTEGER NOT NULL,
                        `column2` TEXT,
                        `newColumn` INTEGER NOT NULL DEFAULT 1
                    )
                """)
            }
        }

        // 从版本10升级到版本11
        private val MIGRATION_10_11 = object : Migration(10, 11) {
            override fun migrate(database: SupportSQLiteDatabase) {
                // 为表新增字段
                database.execSQL(
                    "ALTER TABLE target_table " +
                    "ADD COLUMN newColumn INTEGER NOT NULL DEFAULT 1"
                )
            }
        }
    }
}

四、最佳实践建议

1. 迁移策略选择

  • 简单变更:使用 Room 的自动迁移功能,降低维护成本。
  • 跨版本升级:优先选择手动迁移,明确处理每个版本的升级路径。
  • 混合使用谨慎:避免在同一版本同时使用自动和手动迁移,减少冲突可能。

2. 跨版本处理技巧

  • 分版本设计迁移逻辑:针对不同的版本跨度,设计独立的迁移操作。
  • 测试升级路径:确保每个版本的所有可能升级路径(如 9→10→11 和 9→11)都经过完整测试。

3. SQLite 迁移注意事项

  • 数据验证:确保迁移后的数据符合预期。

  • 字段冲突检测:在迁移前判断目标字段是否已存在。

  • 备份数据:在正式迁移前备份用户数据,防止数据丢失。

五、总结

Room 数据库的版本升级虽然提供了自动迁移机制,但在复杂的跨版本场景中仍然可能踩坑。本次实践总结出以下关键点:

  1. 自动迁移适用于简单场景,跨版本更适合手动迁移
  2. 针对不同版本路径分离逻辑,避免冲突
  3. 充分测试所有可能升级路径,确保数据完整性

原文地址:mp.weixin.qq.com/s/WvtI0W_vF…