房间数据库迁移 fallbackToDestructiveMigration() 不工作

Room db migration fallbackToDestructiveMigration() not working

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

数据库已从版本 1 自动迁移到版本 2(添加了 table)。从版本 2 到版本 3,我现在想通过在资产文件夹中提供不同的 '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.

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

已添加评论:-

I have tried an implemented migration, but the same exception as above happened again. When I try starting over with versionCode 1, I am getting "java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number." I have also changed the database name and added android:allowBackup="false" in the manifest.

有什么想法吗?

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

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

经过广泛的系统测试,我可以复制您的(需要 1-3 个)失败的唯一方法是排除 fallbackToDestructiveMigation。在这种情况下,如果迁移是从 1 到 3 或迁移是 3 到 1(即资产版本为 3 但房间版本为 1)

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

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

  • 3-1异常时AssetDB Version =3 Database Version = -1 Room Version = 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
  • 包含对象 1 的额外列 = 版本 3
  • 包含对象 1 和对象 2 的额外列 = 版本 3
    • 未考虑对象 2 的额外列,但不考虑对象 1 的额外列。

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

此外,为了满足日志记录,添加了回调函数 (.addCallback) 并且 onOpenonCreateonDestructiveMigration 都被覆盖以记录房间版本和数据库版本。

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

给运行一个测试就意味着:-

  1. 确保设备具有适当级别的应用程序。
  2. 正在删除 database.db 资产
  3. 将适当的资产文件复制并粘贴为 database.db(来自 v1dbbase.db 或 v3dbbase.db)
  4. 将 Object1 class 修改为 include/exclude 额外的列(如上所述)
  5. 将 Object2 class 修改为 include/exclude 额外的列(如上所述)
  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>
}

AppDatabase

@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 说:-

以下是这种情况下发生的情况:

  • Because the database defined in your app is on version 3 and the database instance already installed on the device is on version 2, a migration is necessary.
  • Because there is no implemented migration plan from version 2 to version 3, the migration is a fallback migration.
  • Because the fallbackToDestructiveMigration() builder method is called, the fallback migration is destructive. Room drops the database instance that's installed on the device.
  • Because there is a prepackaged database file that is on version 3, Room recreates the database and populates it using the contents of the prepackaged database file.
    • If, on the other hand, you prepackaged database file were on version 2, then Room would note that it does not match the target version and would not use it as part of the fallback migration.
  • 通过注意也许是通过异常的方式?

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

这是依赖注入。

我在 DatabaseModule class 中向 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 public 中进行了 buildDatabase 调用,并使用它为 DatabaseModule class 中的 Dagger 提供了数据库 class。

我在同时使用 fallbackToDestructiveMigration 和 createFromAsset 时遇到问题。我想分享我的经验,因为我花了好几个小时才找到它。当您提供资产数据库时,您必须更新您使用 createFromAsset 提供的默认数据库文件的用户版本编译指示。否则,您总是会丢失在应用程序运行时插入的数据。