一、问题背景
在最近的项目开发中,我遇到了一个严重的数据库升级问题。项目的数据库在版本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)
解决问题的核心在于两个不同的升级路径:
- 版本9 → 版本11:需要完整的跨版本迁移。
- 版本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的处理方案
二、问题分析
- 初步尝试:全部使用自动迁移
在 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 数据库的版本升级虽然提供了自动迁移机制,但在复杂的跨版本场景中仍然可能踩坑。本次实践总结出以下关键点:
- 自动迁移适用于简单场景,跨版本更适合手动迁移。
- 针对不同版本路径分离逻辑,避免冲突。
- 充分测试所有可能升级路径,确保数据完整性。