当 room android db 损坏时会发生什么?

What happens when room android db gets corrupted?

在大型应用程序中,我的数据库文件(通过 android 房间库创建)有可能损坏。对于这样的用户,后备方案是什么?

room 会删除 db 文件并在生产模式下从头开始重新创建,还是我必须自己处理?

For such a user whats the fallback ?

备份和恢复,注意当有任何未完成的事务时不应进行备份。最好确保如果在 WAL 模式下(这是有空间的默认模式)数据库已完全提交(即 WAL 文件为空或不存在)。

  • 备份可以是一个简单的文件副本,或者您可以使用 VACUUM INTO 后者具有潜在释放 space 的优点,但缺点是它可能会占用更多资源。

您可以维护其他数据库,在主数据库更改时将更改应用于其他数据库。显然会有开销。这样做的好处是,如果发生损坏,则不太可能在其他数据库中发生。

您可以维护允许回滚和前滚更改的日志。后者用于从备份前滚到损坏点或接近损坏点。

Will room delete the db file and re-create from scratch in production mode or will I have to handle that myself ?

如果检测到(打开时)则是:-

onCorruption 检测到数据库损坏时调用的方法。 默认实现将删除数据库文件。

https://developer.android.com/reference/androidx/sqlite/db/SupportSQLiteOpenHelper.Callback#onCorruption(androidx.sqlite.db.SupportSQLiteDatabase)

What happens when room android db gets corrupted?

这是文件损坏的示例

2021-10-27 10:36:19.281 7930-7930/a.a.so69722729kotlinroomcorrupt E/SQLiteLog: (26) file is not a database
2021-10-27 10:36:19.285 7930-7930/a.a.so69722729kotlinroomcorrupt E/SupportSQLite: Corruption reported by sqlite on database: /data/user/0/a.a.so69722729kotlinroomcorrupt/databases/thedatabase
2021-10-27 10:36:19.286 7930-7930/a.a.so69722729kotlinroomcorrupt W/SupportSQLite: deleting the database file: /data/user/0/a.a.so69722729kotlinroomcorrupt/databases/thedatabase
2021-10-27 10:36:19.306 7930-7930/a.a.so69722729kotlinroomcorrupt D/TAG: onCreate Invoked.
2021-10-27 10:36:19.312 7930-7930/a.a.so69722729kotlinroomcorrupt D/TAG: onOpen Invoked.

从回调中可以看出,删除文件后会调用 onCreate,从而创建一个新的空数据库。

上面使用的代码是:-

一个简单的@Entity Something :-

@Entity
data class Something(
    @PrimaryKey
    val id: Long?=null,
    val something: String
)

一个简单的@Dao AllDao

@Dao
abstract class AllDao {
    @Insert
    abstract fun insert(something: Something)
    @Query("SELECT * FROM something")
    abstract fun getAllFromSomething(): List<Something>
}

@Database TheDatabase

@Database(entities = [Something::class],version = 1)
abstract class TheDatabase: RoomDatabase() {
        abstract fun getAllDao(): AllDao

    companion object {
        const val DATABASENAME = "thedatabase"
        const val TAG = "DBINFO"
        private var instance: TheDatabase? = null
        var existed: Boolean = false
        fun getInstance(context: Context): TheDatabase {
            existed = exists(context)
            if (exists(context)) {
                Log.d(TAG,"Database exists so corrupting it before room opens the database.")
                corruptDatabase(context)
            }
            if (instance == null) {
                instance = Room.databaseBuilder(context,TheDatabase::class.java, DATABASENAME)
                    .allowMainThreadQueries()
                    .addCallback(cb)
                    .build()
            }
            instance!!.openHelper.writableDatabase // Force open
            return instance as TheDatabase
        }

        object cb: Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                Log.d("TAG","onCreate Invoked.")
            }

            override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
                super.onDestructiveMigration(db)
                Log.d("TAG","onDestructiveMigration Invoked.")
            }

            override fun onOpen(db: SupportSQLiteDatabase) {
                super.onOpen(db)
                Log.d("TAG","onOpen Invoked.")
            }

        }

        fun exists(context: Context): Boolean {
            val db = File(context.getDatabasePath(DATABASENAME).path)
            return db.exists()
        }

        /**
         * Corrupt the database by
         * copying the file via buffered reads and write
         * BUT only write part of a  buffer
         * AND skip the 3rd block
         * Furthermore, delete the -wal file (possible that this )
         * Note that often it is the -wal file deletion that corrupts
         */
        fun corruptDatabase(context: Context) {
            Log.d("TAG","corruptDatabase Invoked.")
            var db: File = File(context.getDatabasePath(DATABASENAME).path)
            logFileInfo(db,"Initial")
            var tempdb = File(context.getDatabasePath("temp" + DATABASENAME).path)
            logFileInfo(tempdb,"Initial")
            val blksize = 128
            var buffer = ByteArray(blksize)
            var i: InputStream
            var o: FileOutputStream
            try {
                i = FileInputStream(db)
                o = FileOutputStream(tempdb)
                var blocks = 0;
                var writes = 0;
                while (i.read(buffer) > 0) {
                    if(blocks++ % 2 == 1) {
                        writes++
                        o.write(buffer,buffer.size / 4,buffer.size / 4)
                    }
                }
                Log.d(TAG,"${blocks} Read ${writes} Written")
                o.flush()
                o.close()
                i.close()
                db = File(context.getDatabasePath(DATABASENAME).path)
                logFileInfo(db,"After copy")
                tempdb = File(context.getDatabasePath("temp${DATABASENAME}").path)
                logFileInfo(tempdb,"After copy")
            } catch (e: IOException) {
                e.printStackTrace()
            }
            db.delete()
            //(context.getDatabasePath(DATABASENAME+"-wal")).delete()
            logFileInfo(db,"After delete")
            tempdb.renameTo(context.getDatabasePath(DATABASENAME))
            logFileInfo(tempdb,"After rename")
            logFileInfo(context.getDatabasePath(DATABASENAME),"After rename/new file")
        }

        fun logFileInfo(file: File, prefix: String) {
            Log.d(TAG,"${prefix} FileName is ${file.name}\n\tpath is ${file.path}\n\tsize is ${file.totalSpace} frespace is ${file.freeSpace}")
        }
    }
}

最后调用 Activity MainActivity

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: AllDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        db = TheDatabase.getInstance(this);
        dao = db.getAllDao()
        dao.insert(Something(something = "Something1 " + TheDatabase.existed))
        dao.insert(Something(something = "Something2 " + TheDatabase.existed))
        for(s: Something in dao.getAllFromSomething()) {
            Log.d(TheDatabase.TAG,"Something with ID " + s.id +  " is " + s.something)
        }
    }
}
  • 请注意,上面的数据库损坏的是 -wal 删除。对于被删除的块来说,它是非常有弹性的,至少对于较小的数据库来说是这样,在这些数据库中,WAL 文件的大小足以通过应用存储在其中的更改来恢复。然而,基于上述但插入第二个 table 和 100000 行的测试,部分块写入和丢失的块写入确实会破坏数据库。