https://mp.weixin.qq.com/s/WvtI0W_vFS-t_rHMjaU_Vw
在最近的项目开发中,我遇到了一个 严重的数据库升级问题。项目的数据库在 版本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中执行了手动升级,但是在我去掉 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 的自动迁移无法兼顾跨版本升级的不同路径。
SQLite 对
ALTER TABLE 的操作有以下限制:
添加新列时,若该列是非空字段且设置了默认值,SQLite 会尝试给所有现有行赋值。如果这个过程失败,会导致迁移中断。
Room 的自动迁移策略选择创建新表并迁移数据,以确保迁移的原子性和数据一致性。
手动迁移本质上是直接操作 SQL,但在跨版本路径中,由于不同版本可能已经存在某些字段或表结构差异,直接执行迁移逻辑会造成冲突。例如:
从版本9升级到版本11时,需要新增字段
newColumn。
从版本10升级到版本11时,
newColumn 字段已经存在,再次添加会报错
经过实践和分析,我采取了 分版本处理策略,针对不同的升级路径分别设计手动迁移逻辑。
针对 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"
)
}
}
}
}
简单变更:使用 Room 的自动迁移功能,降低维护成本。
跨版本升级:优先选择手动迁移,明确处理每个版本的升级路径。
混合使用谨慎:避免在同一版本同时使用自动和手动迁移,减少冲突可能。
分版本设计迁移逻辑:针对不同的版本跨度,设计独立的迁移操作。
测试升级路径:确保每个版本的所有可能升级路径(如 9→10→11 和 9→11)都经过完整测试。
数据验证:确保迁移后的数据符合预期。
字段冲突检测:在迁移前判断目标字段是否已存在。
备份数据:在正式迁移前备份用户数据,防止数据丢失。
Room 数据库的版本升级虽然提供了自动迁移机制,但在复杂的跨版本场景中仍然可能踩坑。本次实践总结出以下关键点:
自动迁移适用于简单场景,跨版本更适合手动迁移。
针对不同版本路径分离逻辑,避免冲突。
充分测试所有可能升级路径,确保数据完整性。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3058815/ ,如需转载,请注明出处,否则将追究法律责任。