【问题标题】:Room db migration fallbackToDestructiveMigration() not working房间数据库迁移 fallbackToDestructiveMigration() 不工作
【发布时间】:2021-11-18 16:25:32
【问题描述】:

我在资产文件夹中使用带有预填充数据库的 Room。对于应用更新,我想通过添加新列并使用新数据预填充此列来更改此数据库。

数据库从版本 1 自动迁移到版本 2(添加了一个表)。从版本 2 到 3,我现在想通过在 assets 文件夹中提供不同的“database.db”文件并允许破坏性迁移来应用上述更改。

@Database(entities = [Object1::class, Object2::class], version = 3, autoMigrations = [
    AutoMigration (from = 1, to = 2)], exportSchema = true)
abstract class AppDatabase : RoomDatabase() {

    abstract fun dao(): Dao

    companion object {

        private const val DB_NAME = "database.db"

        @Volatile
        private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }

        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(
                context,
                AppDatabase::class.java, "AppDB.db")
                .fallbackToDestructiveMigration()
                .createFromAsset(DB_NAME)
                .build()
        }
    }
}

问题是我仍然得到以下异常:

   java.lang.IllegalStateException: A migration from 1 to 3 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

我不确定为什么还会发生这种情况。我认为它要么提供迁移脚本,要么允许破坏性迁移使迁移工作。

添加评论:-

我已尝试实施迁移,但与上述相同的异常再次发生。当我尝试使用 versionCode 1 重新开始时,我收到“java.lang.IllegalStateException:Room 无法验证数据完整性。看起来您已更改架构但忘记更新版本号。您可以通过增加版本来解决此问题数字。”我还更改了数据库名称并在清单中添加了 android:allowBackup="false"。

有什么想法吗?

【问题讨论】:

标签: android kotlin android-room database-migration


【解决方案1】:

翻阅房间文档并没有太多发现,我的直觉是这与您使用自动迁移而不是实施迁移的事实有关。您是否尝试过将 Automigration 从 1->2 更改为已实施的迁移?

此外,由于您要手动将其替换为具有预填充数据的新数据库,因此我的解决方案是摆脱旧的迁移,稍微更改数据库的名称并从版本 1 重新开始。没有理由如果从旧版本升级到当前版本的任何人都删除了他们的数据库,请维护旧迁移。

【讨论】:

  • 我已经尝试过实施迁移,但再次发生与上述相同的异常。当我尝试使用 versionCode 1 重新开始时,我收到“java.lang.IllegalStateException:Room 无法验证数据完整性。看起来您已更改架构但忘记更新版本号。您可以通过增加版本来解决此问题数字。”我还更改了数据库名称并在清单中添加了 android:allowBackup="false"。
【解决方案2】:

经过广泛的有条不紊的测试,我可以复制您的(需要 1-3 次)失败的唯一方法是排除 fallbackToDestructiveMigation。在这种情况下,如果迁移是从 1 到 3 或迁移是 3 到 1(即 Asset Version 为 3 但 Room 版本为 1),则会发生异常

  • 根据下面的电子表格屏幕截图

  • AssetDB Version =3 Database Version = 1 Room Version = 3时的1-3异常

  • 还有AssetDB Version =3 Database Version = -1 Room Version = 1时的3-1异常

    • -1 版本表示文件不存在(即初始安装)

我怀疑您不知何故无意中引入了上述两个扫描仪之一。我没有测试的是替代房间库版本。上述内容已按照 2.4.0-alpha04 进行了测试:-

implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.room:room-ktx:2.4.0-alpha04'
implementation 'androidx.room:room-runtime:2.4.0-alpha04'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
kapt 'androidx.room:room-compiler:2.4.0-alpha0

为了测试,我有两个资产文件副本,一个在版本 1,另一个在版本 2(v1dbbase.dbv3dbbase.db),公共列中的数据指示数据是否适用于版本 3。实际使用的资产文件在测试前被删除,并将相应的版本复制粘贴到database.db

我有两个实体 Object1 和 Object2,并且可以在其中一个额外的列中添加或注释掉。例如:-

/*TESTING INCLUDE FOR V2+ >>>>>*///, @ColumnInfo(name = COL_EXTRAV2, defaultValue = "x") val object1_extra: String
- as above it is excluded
/*TESTING INCLUDE FOR V2+ >>>>>*/, @ColumnInfo(name = COL_EXTRAV2, defaultValue = "x") val object1_extra: String
- with the two //'s before the comma now included
  • 两个额外的列都被注释掉 = 版本 1
  • 包含 Object1 的额外列 = 版本 3
  • 包含 Object1 和 Object2 的额外列 = 版本 3
    • 未考虑包含 Object2 的额外列,但未考虑 Object1 的额外列。

添加了一些常量来满足日志记录。

另外,为了记录一个回调函数(.addCallback),onOpenonCreateonDestructiveMigration 都被覆盖以记录房间版本和数据库版本。

为了进一步增强日志记录,添加了两个函数,从 sqlite 数据库头中获取版本。一个用于资产文件,另一个用于数据库。在数据库构建之前调用/调用的函数。

运行测试意味着:-

  1. 确保设备具有适当级别的应用程序。
  2. 删除 database.db 资产
  3. 将适当的资产文件复制并粘贴为 database.db(来自 v1dbbase.db 或 v3dbbase.db)
  4. 修改 Object1 类以包含/排除额外的列(如上所述)
  5. 修改 Object2 类以包含/排除额外的列(如上所述)
  6. 将房间版本修改为适当的级别。

用于测试的代码:-

对象1

@Entity(tableName = TABLE_NAME)
data class Object1(
    @PrimaryKey
    @ColumnInfo(name = COL_ID)
    val object1_id: Long,
    @ColumnInfo(name = COL_NAME)
    val object1_name: String
    /*TESTING INCLUDE FOR V2+ >>>>>*///, @ColumnInfo(name = COL_EXTRAV2, defaultValue = "x") val object1_extra: String
) {
    companion object {
        const val TABLE_NAME = "object1"
        const val COL_ID = TABLE_NAME + "_object1_id"
        const val COL_NAME = TABLE_NAME + "_object1_name"
        const val COL_EXTRAV2 = TABLE_NAME + "_object1_extrav2"
    }
}

对象2

@Entity(tableName = TABLE_NAME)
data class Object2(
    @PrimaryKey
    @ColumnInfo(name = COL_ID)
    val object2_id: Long,
    @ColumnInfo(name = COL_NAME)
    val object2_name: String
    /*TESTING INCLUDE FOR V3>>>>>*///, @ColumnInfo(name = COL_EXTRAV3, defaultValue = "x") val object3_extrav3: String
) {
    companion object {
        const val TABLE_NAME = "object2"
        const val COL_ID = TABLE_NAME + "_object2_id"
        const val COL_NAME = TABLE_NAME + "_object2_name"
        const val COL_EXTRAV3 = TABLE_NAME + "_object2_extrav3"
    }
}

@Dao
abstract class Dao {

    @Insert
    abstract fun insert(object1: Object1): Long
    @Insert
    abstract fun insert(object2: Object2): Long
    @Query("SELECT * FROM ${Object1.TABLE_NAME}")
    abstract fun getAllFromObject1(): List<Object1>
    @Query("SELECT * FROM ${Object2.TABLE_NAME}")
    abstract fun getAllFromObject2(): List<Object2>
}

应用数据库

@Database(
    entities = [Object1::class, Object2::class],
    version = AppDatabase.DBVERSION,
    autoMigrations = [AutoMigration (from = 1, to = 2)],
    exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun dao(): Dao

    companion object {

        private const val DB_NAME = "database.db"
        private const val DB_FILENAME = "AppDB.db" //<<<<< ADDED for getting header
        const val TAG = "DBINFO" //<<<< ADDED for logging
        const val DBVERSION = 1 //<<<<<ADDED for logging

        @Volatile
        private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                //ADDED>>>>> to get database version from dbfile and assets before building the database
                Log.d(TAG,
                    "AssetDB Version =${getAssetDBVersion(context, DB_NAME)} " +
                            "Database Version = ${getDBVersion(context, DB_FILENAME)} " +
                            "Room Version = ${DBVERSION}")
                instance ?: buildDatabase(context).also { instance = it }
            }
        }

        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(
                context,
                AppDatabase::class.java, DB_FILENAME)
                .fallbackToDestructiveMigration()
                .createFromAsset(DB_NAME)
                .allowMainThreadQueries()
                .addCallback(rdc)
                .build()
        }

        /* Call Backs for discovery */
        object rdc: RoomDatabase.Callback(){
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                Log.d(TAG,"onCreate called. DB Version = ${db.version}, Room Version is ${DBVERSION}")
            }

            override fun onOpen(db: SupportSQLiteDatabase) {
                super.onOpen(db)
                Log.d(TAG,"onOpen called. DB Version = ${db.version}, Room Version is ${DBVERSION}")
            }

            override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
                super.onDestructiveMigration(db)
                Log.d(TAG,"onDestructiveMigration called. DB Version = ${db.version}, Room Version is ${DBVERSION}")
            }
        }

        fun getAssetDBVersion(context: Context, assetFilePath: String): Int {
            var assetFileHeader = ByteArray(100)
            try {
                var assetFileStream = context.assets.open(assetFilePath)
                assetFileStream.read(assetFileHeader,0,100)
                assetFileStream.close()
            } catch (e: IOException) {
                return -2 // Indicates file not found (no asset)
            }
            return ByteBuffer.wrap(assetFileHeader,60,4).getInt()
        }

        fun getDBVersion(context: Context, dbFileName: String): Int {
            var SQLiteHeader = ByteArray(100)
            val dbFile = context.getDatabasePath(dbFileName)
            if(dbFile.exists()) {
                var inputStream = dbFile.inputStream()
                inputStream.read(SQLiteHeader, 0, 100)
                inputStream.close()
                return ByteBuffer.wrap(SQLiteHeader, 60, 4).getInt()
            } else {
                return -1 // Indicates no database file (e.g. new install)
            }
        }
    }
}
  • 您可能希望考虑包括上面的日志记录,它可以很容易地检测到正在使用的版本的问题。

MainActivity

class MainActivity : AppCompatActivity() {

    lateinit var db: AppDatabase
    lateinit var dao: Dao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = AppDatabase.getInstance(this)
        dao = db.dao()
        for(o1: Object1 in dao.getAllFromObject1()) {
            logObject1(o1)
        }
        for(o2: Object2 in dao.getAllFromObject2()) {
            logObject2(o2)
        }

    }

    fun logObject1(object1: Object1) {
        Log.d(TAG,"ID is ${object1.object1_id}, Name is ${object1.object1_name}")
    }

    fun logObject2(object2: Object2) {
        Log.d(TAG,"ID is ${object2.object2_id}, Name is ${object2.object2_name}")
    }
    companion object {
        const val TAG = AppDatabase.TAG
    }
}

除了利用上述代码并确保完成 6 项任务外,我还保留了版本和结果的电子表格,例如:-

上一个答案(测试后不是这样)

我认为您的问题可能与预填充的数据库有关,因为它的版本号 (user_version) 尚未更改为 3。

  • 您可以使用 SQL 更改版本(来自 SQlite 工具)PRAGMA user_version = 3;

documentation 说:-

在这种情况下会发生以下情况:

  • 由于您的应用中定义的数据库是版本 3,而设备上已安装的数据库实例是版本 2,因此需要进行迁移。
  • 由于没有实施从版本 2 到版本 3 的迁移计划,因此迁移是回退迁移。
  • 因为调用了fallbackToDestructiveMigration() builder方法,所以回退迁移是破坏性的。 Room 删除设备上安装的数据库实例。
  • 由于版本 3 中有一个预打包的数据库文件,Room 会重新创建数据库并使用预打包的数据库文件的内容填充它。
    • 另一方面,如果您预打包的数据库文件使用的是版本 2,那么 Room 会注意到它与目标版本不匹配,并且不会将其用作后备迁移的一部分。
  • 注意也许是例外?

【讨论】:

  • 不幸的是,这不是解决方案,因为预填充的数据库已经在版本 3 上(通过打开 DB Browser for SQLite -> Edit Pragmas -> User Version = 3 进行测试)。
  • @user5102612 是的,实际测试中,Room 跳过了太低且具有破坏性的资产文件版本。我已经更新了答案,但只能通过排除 fallbackToDestructive 来获得所需的 1-3(尽管也设法获得了所需的 3-1)。
【解决方案3】:

我终于弄清楚了问题所在,它与版本控制或与房间或资产数据库文件相关的任何其他内容无关。

这是依赖注入。

我在 DatabaseModule 类中将我的数据库提供给 Dagger,如下所示:

private const val DB_NAME = "database.db"

@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {

@Provides
fun provideDao(appDatabase: AppDatabase): Dao {
    return appDatabase.dao()
}

@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase {
    return Room.databaseBuilder(
        appContext,
        AppDatabase::class.java, "AppDB.db")
        .createFromAsset(DB_NAME)
        .build()
}

}

它缺少 fallBackToDestructiveMigration() 调用,所以这弄乱了 RoomOpenHelper.java 中 Room 的内部 onUpgrade 调用。

为了解决这个问题,我公开了 AppDatabase 中的 buildDatabase 调用,并使用它在 DatabaseModule 类中将数据库提供给 Dagger。

【讨论】:

    猜你喜欢
    • 2019-02-13
    • 2021-08-13
    • 2021-09-02
    • 2020-12-14
    • 2018-11-11
    • 1970-01-01
    • 2019-10-07
    • 2018-12-03
    • 2021-12-11
    相关资源
    最近更新 更多