Room数据库迁移异常:project表字段不匹配问题求助
升级应用后打开新版本时触发Room数据库迁移异常,报错显示project表的预期结构包含nfcCollectorId、nfcMerchantId、nfcPrivateKey、nfcProfileId字段,但实际未检测到这些字段。
异常详情
Caused by: java.lang.IllegalStateException: Migration didn't properly handle: Expected
TableInfo{name='project', columns={venueName=Column{name='venueName', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, nfcMerchantId=Column{name='nfcMerchantId', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, nfcPrivateKey=Column{name='nfcPrivateKey', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, endDate=Column{name='endDate', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, scheduledAnonymisationDate=Column{name='scheduledAnonymisationDate', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, nfcProfileId=Column{name='nfcProfileId', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, timeZone=Column{name='timeZone', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=1, defaultValue='null'}, uuid=Column{name='uuid', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, nfcCollectorId=Column{name='nfcCollectorId', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, startDate=Column{name='startDate', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[Index{name='index_project_uuid', unique=false, columns=[uuid], orders=[ASC]}]}
Found
TableInfo{name='project', columns={venueName=Column{name='venueName', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, endDate=Column{name='endDate', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, scheduledAnonymisationDate=Column{name='scheduledAnonymisationDate', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, nfcProfileId=Column{name='nfcProfileId', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, timeZone=Column{name='timeZone', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=1, defaultValue='null'}, uuid=Column{name='uuid', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, startDate=Column{name='startDate', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[Index{name='index_project_uuid', unique=false, columns=[uuid], orders=[ASC]}]}
已执行操作
- 添加
Migration18To19类,通过ALTER TABLE语句新增上述4个TEXT类型字段 - 将数据库版本升至23,同时为其他表的修改添加了对应迁移类
- 调试日志显示
project表已包含目标字段,但异常仍触发 - 4个字段最初设为非空,后续改为可空,不确定是否因此导致问题
相关代码
ProjectEntity实体类
@Entity( tableName = "project", indices = [Index("uuid")] ) data class ProjectEntity( @PrimaryKey val id: String, val uuid: String, val name: String, val startDate: String?, val endDate: String?, val venueName: String?, val timeZone: String?, val scheduledAnonymisationDate: String?, val nfcCollectorId: String?, val nfcMerchantId: String?, val nfcPrivateKey: String?, val nfcProfileId: String? )
Room数据库类
@Database(entities = {ProjectEntity.class}, version = 23) public abstract class RoomDatabaseImpl extends RoomDatabase { abstract fun projectDao(): ProjectDao }
18→19迁移类
internal object Migration18To19 : Migration(18, 19) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `project` ADD COLUMN `nfcCollectorId` TEXT") database.execSQL("ALTER TABLE `project` ADD COLUMN `nfcMerchantId` TEXT") database.execSQL("ALTER TABLE `project` ADD COLUMN `nfcPrivateKey` TEXT") database.execSQL("ALTER TABLE `project` ADD COLUMN `nfcProfileId` TEXT") } }
调试获取的表结构
45 { type=table name=project tbl_name=project rootpage=49 sql=CREATE TABLE `project` (`id` TEXT NOT NULL, `uuid` TEXT NOT NULL, `name` TEXT NOT NULL, `startDate` TEXT, `endDate` TEXT, `venueName` TEXT, `timeZone` TEXT, `scheduledAnonymisationDate` TEXT, `nfcCollectorId` TEXT, `nfcMerchantId` TEXT, `nfcPrivateKey` TEXT, `nfcProfileId` TEXT, PRIMARY KEY(`id`)) } 46 { type=index name=sqlite_autoindex_project_1 tbl_name=project rootpage=50 sql=null } 47 { type=index name=index_project_uuid tbl_name=project rootpage=52 sql=CREATE INDEX `index_project_uuid` ON `project` (`uuid`) }
排查原因
- 迁移类未完全关联:数据库版本从18升至23,中间所有版本区间的迁移类(如19→20、20→21等)都需添加到Room数据库构建中,若遗漏某段迁移,Room会跳过部分操作导致表结构不匹配。
- 编译缓存问题:Room的注解处理器可能缓存了旧的表结构元数据,导致生成的预期结构与实际数据库结构不符。
- 迁移逻辑冲突:后续版本的迁移类可能对
project表执行了意外操作(如删除字段),覆盖了18→19迁移的结果。 - 可空性历史遗留:虽然当前实体类字段为可空,但如果早期迁移按非空创建字段后未做对应修改,可能导致Room校验时的预期与实际存在隐性差异。
解决方案
- 确保迁移类全量关联:在构建Room数据库时,添加所有版本区间的迁移类,示例代码:
Room.databaseBuilder(context, RoomDatabaseImpl::class.java, "your-db-name") .addMigrations(Migration18To19, Migration19To20, ..., Migration22To23) .build() - 清理编译缓存并重建项目:执行Android Studio的
Clean Project和Rebuild Project操作,清除Room注解处理器的旧缓存,重新生成数据库元数据。 - 验证所有迁移逻辑:检查19到23版本之间的所有迁移类,确认没有对
project表的目标字段执行删除、修改等操作。 - 临时测试方案(生产环境禁用):若为开发阶段,可临时添加
fallbackToDestructiveMigration()方法跳过迁移直接重建数据库,但会丢失所有本地数据。
内容的提问来源于stack exchange,提问作者sridhar venkatesan

