You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Room数据库迁移异常:project表字段不匹配问题求助

Room数据库迁移异常排查与解决方案

升级应用后打开新版本时触发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`)
 }

排查原因

  1. 迁移类未完全关联:数据库版本从18升至23,中间所有版本区间的迁移类(如19→20、20→21等)都需添加到Room数据库构建中,若遗漏某段迁移,Room会跳过部分操作导致表结构不匹配。
  2. 编译缓存问题:Room的注解处理器可能缓存了旧的表结构元数据,导致生成的预期结构与实际数据库结构不符。
  3. 迁移逻辑冲突:后续版本的迁移类可能对project表执行了意外操作(如删除字段),覆盖了18→19迁移的结果。
  4. 可空性历史遗留:虽然当前实体类字段为可空,但如果早期迁移按非空创建字段后未做对应修改,可能导致Room校验时的预期与实际存在隐性差异。

解决方案

  1. 确保迁移类全量关联:在构建Room数据库时,添加所有版本区间的迁移类,示例代码:
    Room.databaseBuilder(context, RoomDatabaseImpl::class.java, "your-db-name")
        .addMigrations(Migration18To19, Migration19To20, ..., Migration22To23)
        .build()
    
  2. 清理编译缓存并重建项目:执行Android Studio的Clean Project和Rebuild Project操作,清除Room注解处理器的旧缓存,重新生成数据库元数据。
  3. 验证所有迁移逻辑:检查19到23版本之间的所有迁移类,确认没有对project表的目标字段执行删除、修改等操作。
  4. 临时测试方案(生产环境禁用):若为开发阶段,可临时添加fallbackToDestructiveMigration()方法跳过迁移直接重建数据库,但会丢失所有本地数据。

内容的提问来源于stack exchange,提问作者sridhar venkatesan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 02:45:55