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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 08:10:20