Android中RoomDB数据库迁移时崩溃问题求助
RoomDB迁移异常:表结构一致却提示迁移未正确处理
应用启动即崩溃,Logcat报错显示Room认为迁移未正确处理downloads表,但预期表结构与实际发现的表结构完全一致。
报错日志
java.lang.RuntimeException: Exception while computing database live data. at androidx.room.RoomTrackingLiveData.refreshRunnableSlamb da-0(RoomTrackingLiveData.kt:74) at androidx.room.RoomTrackingLiveData. $r8slambdaSUkyPj-RMUOTXOMbUuy5NWSwMoOE(Unknown Source:0) at androidx.room.RoomTrackingLiveData$ șExternalSyntheticLambdaO.run(Unknown Source:2) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPo olExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadP oolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.IllegalStateException: Migration didn't properly handle: downloads(ge.mov.mobile.data.local.entity.O fflineMovieEntity). Expected; TableInfo(name='downloads' columns=(src=Column(name='src', type='TEXT', affinity='2', notNull=notNull, primarykeyPosition=0, defaultValue='null'), filePath=Column(name='filePath', type='TEXT' affinity='2', notNull=notNull, primarykeyPosition=0, defaultValue='null'), i=Column(name='i', type='INTEGER' affinity='3', notNull=notNull, primarykeyPosition=1, defaultValue='null'), episode=Column(name='episode' type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), language=Column(name='language', type='TEXT' affinity='2', notNull=notNull, primarykeyPosition=0, defaultValue='null'), adjaraId=Column(name='adjaraId' type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), titleEng=Column(name='titleEng', type='TEXT', affinity='2', notNull=notNull, primarykeyPosition=0, defaultValue='null'), quality=Column(name='quality', type='TEXT', affinity='2' notNull=notNull, primarykeyPosition=0, defaultValue='null'), titleGeo=Column(name='titleGeo', type='TEXT', affinity='2' notNull=notNull, primarykeyPosition=0, defaultValue='null'), fileSize=Column(name='fileSize', type='REAL', affinity='4' notNull=notNull, primarykeyPosition=0, defaultValue='null'), season=Column(name='season', type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), savedAt=Column(name='savedAt', type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), id=Column(name='id', type='INTEGER' affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), poster=Column(name='poster' type='BLOB', affinity='5', notNull=notNull, primaryKeyPosition=0, defaultValue='null')), foreignKeys=[], indices=[] Found: TableInfo(name='downloads' columns=(src=Column(name='src', type='TEXT', affinity='2' notNull=notNull, primarykeyPosition=0, defaultValue='null'), filePath=Column(name='filePath', type='TEXT' affinity='2', notNull=notNull, primarykeyPosition=0, defaultValue='null'), i=Column(name='i', type='INTEGER' affinity='3', notNull=notNull, primarykeyPosition=1, defaultValue='null'), episode=Column(name='episode' type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), language=Column(name='language', type='TEXT' affinity='2', notNull=notNull, primarykeyPosition=0, defaultValue='null'), adjaraId=Column(name='adjaraId' type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), titleEng=Column(name='titleEng', type='TEXT', affinity='2' notNull=notNull, primarykeyPosition=0, defaultValue='null'), quality=Column(name='quality', type='TEXT', affinity='2' notNull=notNull, primarykeyPosition=0, defaultValue='null'), titleGeo=Column(name='titleGeo', type='TEXT', affinity='2' notNull=notNull, primarykeyPosition=0, defaultValue='null'), fileSize=Column(name='fileSize', type='REAL', affinity='4' notNull=notNull, primarykeyPosition=0, defaultValue='null'), season=Column(name='season', type='INTEGER', affinity='3' notNull=notNull, primarykeyPosition=0, defaultValue='null'), savedAt=Column(name='savedAt', type='INTEGER' affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), id=Column(name='id', type='INTEGER', affinity='3', notNull=notNull, primarykeyPosition=0, defaultValue='null'), poster=Column(name='poster', type='BLOB', affinity='5', notNull=notNull, primarykeyPosition=0, defaultValue='null')), foreignKeys=[], indices=[] at androidx.room.RoomOpenHelper.onUpgrade(RoomOpenHelp er.kt:94) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$ OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.java:29) at android.database.sqlite.SQLiteOpenHelper.getDatabaseLock ed(SQLiteOpenHelper.java:515) at android.database.sqlite.SQLiteOpenHelper.getWritableDatab ase(SQLiteOpenHelper.java:413) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$ OpenHelper.getWritableOrReadableDatabase(FrameworkSQL iteOpenHelper.java:273) androidx.sqlite.db.framework.FrameworkSQLiteOpenHelpers OpenHelper.innerGetDatabase(FrameworkSQLiteOpenHelper. java:225) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$ OpenHelper.getSupportDatabase(FrameworkSQLiteOpenHel per.java:183) androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getW ritableDatabase(FrameworkSQLiteOpenHelper.java:133) at androidx.room.RoomDatabase
实体类代码
@Entity(tableName = "downloads") data class OfflineMovieEntity( @PrimaryKey(autoGenerate = true) val i: Int, var id: Long, var adjaraId: Long, var titleGeo: String, var titleEng: String, var poster: Bitmap, var season: Int, var episode: Int, var language: String, var quality: String, var src: String, var filePath: String, var fileSize: Double, var savedAt: Long )
迁移代码
object Migrations { private const val TYPE_INT = "INTEGER" private const val TYPE_STRING = "TEXT" private const val TYPE_DOUBLE = "REAL" private const val TYPE_BITMAP = "BLOB" private const val TYPE_LONG = "INTEGER" private const val TYPE_BOOLEAN = "INTEGER" private fun SupportSQLiteDatabase.createTable( tableName: String, values: HashMap<String, String> ) { var sql = "CREATE TABLE IF NOT EXISTS '$tableName' (" values.entries.forEach { sql += "'${it.key}' ${it.value.uppercase()}" sql += if (it == values.entries.last()) "" else ", " } sql += ");" execSQL(sql) } private val MIGRATION_17_18 = object : Migration(17, 18) { override fun migrate(database: SupportSQLiteDatabase) { database.createTable( "downloads", hashMapOf( OfflineMovieEntity::i.name to "$TYPE_LONG PRIMARY KEY", OfflineMovieEntity::id.name to "$TYPE_LONG NOT NULL", OfflineMovieEntity::adjaraId.name to "$TYPE_LONG NOT NULL", OfflineMovieEntity::titleEng.name to "$TYPE_STRING NOT NULL", OfflineMovieEntity::titleGeo.name to "$TYPE_STRING NOT NULL", OfflineMovieEntity::poster.name to "$TYPE_BITMAP NOT NULL", OfflineMovieEntity::season.name to "$TYPE_INT NOT NULL", OfflineMovieEntity::episode.name to "$TYPE_INT NOT NULL", OfflineMovieEntity::language.name to "$TYPE_STRING NOT NULL", OfflineMovieEntity::quality.name to "$TYPE_STRING NOT NULL", OfflineMovieEntity::src.name to "$TYPE_STRING NOT NULL", OfflineMovieEntity::filePath.name to "$TYPE_STRING NOT NULL", OfflineMovieEntity::fileSize.name to "$TYPE_DOUBLE NOT NULL", OfflineMovieEntity::savedAt.name to "$TYPE_LONG NOT NULL", ) ) } } val migrations = arrayOf( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18 ) }
问题原因与解决方法
1. 主键自增属性缺失
实体类中i字段标注了@PrimaryKey(autoGenerate = true),但迁移SQL里只写了PRIMARY KEY,没有加上AUTOINCREMENT。Room预期主键是自增的,但实际表中主键没有自增属性,导致结构校验失败。
修复方法:修改迁移代码中i字段的定义,加上AUTOINCREMENT:
OfflineMovieEntity::i.name to "$TYPE_LONG PRIMARY KEY AUTOINCREMENT",
2. HashMap的无序性导致列顺序不一致
HashMap是无序集合,生成SQL时列的顺序可能和Room根据实体类生成的预期顺序不一致。Room的表结构校验会检查列顺序,即使字段完全一致,顺序不同也会判定为结构不匹配。
修复方法:改用LinkedHashMap保证列顺序和实体类字段顺序一致:
private fun SupportSQLiteDatabase.createTable( tableName: String, values: LinkedHashMap<String, String> ) { // 原逻辑不变 } // 迁移时使用LinkedHashMap database.createTable( "downloads", LinkedHashMap<String, String>().apply { put(OfflineMovieEntity::i.name, "$TYPE_LONG PRIMARY KEY AUTOINCREMENT") put(OfflineMovieEntity::id.name, "$TYPE_LONG NOT NULL") put(OfflineMovieEntity::adjaraId.name, "$TYPE_LONG NOT NULL") put(OfflineMovieEntity::titleEng.name, "$TYPE_STRING NOT NULL") put(OfflineMovieEntity::titleGeo.name, "$TYPE_STRING NOT NULL") put(OfflineMovieEntity::poster.name, "$TYPE_BITMAP NOT NULL") put(OfflineMovieEntity::season.name, "$TYPE_INT NOT NULL") put(OfflineMovieEntity::episode.name, "$TYPE_INT NOT NULL") put(OfflineMovieEntity::language.name, "$TYPE_STRING NOT NULL") put(OfflineMovieEntity::quality.name, "$TYPE_STRING NOT NULL") put(OfflineMovieEntity::src.name, "$TYPE_STRING NOT NULL") put(OfflineMovieEntity::filePath.name, "$TYPE_STRING NOT NULL") put(OfflineMovieEntity::fileSize.name, "$TYPE_DOUBLE NOT NULL") put(OfflineMovieEntity::savedAt.name, "$TYPE_LONG NOT NULL") } )
3. 验证迁移逻辑
如果是从旧版本升级到18,确认downloads表在版本17中不存在,否则CREATE TABLE IF NOT EXISTS不会执行任何操作,导致旧表结构不符合新版本要求。如果版本17中已有downloads表,需要改用ALTER TABLE修改结构,而不是创建新表。
内容的提问来源于stack exchange,提问作者grgg7
相关产品推荐
相关产品推荐

