【问题标题】:Backup Room database备份室数据库
【发布时间】:2018-12-01 21:11:55
【问题描述】:

我正在尝试以编程方式备份​​房间数据库。

为此,我只是复制了包含整个数据库的.sqlite 文件

但是,在复制之前,由于房间启用了预写日志记录,我们必须关闭数据库以便-shm 文件和-wal 文件合并为一个.sqlite文件。 As pointed out here

我在RoomDatabase 对象上运行.close()

备份一切正常,但是稍后,当我尝试执行 INSERT 查询时,我收到此错误:

android.database.sqlite.SQLiteException: no such table: room_table_modification_log (code 1)

如何在关闭房间数据库后正确重新打开它?

PS:.isOpen()RoomDatabase 对象在 INSERT 之前返回 true

房间版:1.1.1-rc1

【问题讨论】:

  • 尽量不要关闭DB,使dataBaseHelper类单例避免此类错误

标签: android sqlite android-room sqliteopenhelper android-architecture-components


【解决方案1】:

关闭房间数据库后如何正确重新打开房间数据库?

很抱歉,这没有回答这个问题。

但是,如果您想要将所有内容移至原始数据库文件,那么您不必一开始就关闭数据库。您可以改为使用 wal_checkpoint pragma 强制检查点。

针对数据库查询以下语句。我们在这里使用原始查询,因为 Room 尚不支持 pragma(它会触发 UNKNOWN query type 错误)。在你的 DAO 中有这个查询:

@RawQuery
int checkpoint(SupportSQLiteQuery supportSQLiteQuery);

然后当你调用检查点方法时,使用查询然后:

myDAO.checkpoint(new SimpleSQLiteQuery("pragma wal_checkpoint(full)"));

This 链接可能有助于了解wal_checkpoint 的作用。

【讨论】:

  • 虽然这不能回答我的问题“关闭房间数据库后如何正确重新打开房间数据库?”,但该解决方案解决了我的问题。首先不需要关闭 Room 数据库。这也应该是接受的答案:stackoverflow.com/questions/49394683/…
  • 先生,您刚刚拯救了世界!
  • 我应该打电话给myDAO.checkpoint(new SimpleSQLiteQuery("pragma wal_checkpoint(full)"));
  • @BertramGilfoyle 我知道,但我是新来的房间,我跟进了一个教程,该教程将从存储库中调用所有 doe,从片段/活动中调用所有存储库,所以如果我想备份,我应该打电话是在备份和恢复之前还是在创建的活动中?
  • @ZakariaDarwish - 您可以从 ViewModel 调用 myDAO.checkpoint 方法,而该方法又应该从您的活动中调用。
【解决方案2】:

为了更具体地回答您的问题,这就是我在我的一个应用中备份房间数据库的方式。

  1. 检查读取/写入外部存储的权限。如果您写入您的 App 文件目录,则可以忽略此步骤。
  2. 关闭您的RoomDatabase。在我的例子中,AppDatabase 指的是一个包含最初构建房间数据库的逻辑的单例。 AppDatabase.getInstance(this).getDatabase() 获取单例的当前实例及其当前数据库类,该类从 RoomDatabase 扩展而来。这实际上调用了RoomDatabase.close()
  3. 根据备份或恢复定义源文件和目标文件。我包含 shm 和 wal 文件,即使它们是临时文件。
  4. 使用您选择的方法复制文件。在这种情况下,FileUtils 指的是commons-io

代码

if(id == R.id.action_save_db) {
    int permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
    if(permission == PackageManager.PERMISSION_GRANTED) {
        AppDatabase.getInstance(this).getDatabase().close();

        File db = getDatabasePath("my-db");
        File dbShm = new File(db.getParent(), "my-db-shm");
        File dbWal = new File(db.getParent(), "my-db-wal");

        File db2 = new File("/sdcard/", "my-db");
        File dbShm2 = new File(db2.getParent(), "my-db-shm");
        File dbWal2 = new File(db2.getParent(), "my-db-wal");

        try {
            FileUtils.copyFile(db, db2);
            FileUtils.copyFile(dbShm, dbShm2);
            FileUtils.copyFile(dbWal, dbWal2);
        } catch (Exception e) {
            Log.e("SAVEDB", e.toString());
        }
    } else {
        Snackbar.make(mDrawer, "Please allow access to your storage", Snackbar.LENGTH_LONG)
                .setAction("Allow", view -> ActivityCompat.requestPermissions(this, new String[] {
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                }, 0)).show();
    }
} else if(id == R.id.action_load_db) {
    int permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
    if(permission == PackageManager.PERMISSION_GRANTED) {
        AppDatabase.getInstance(this).getDatabase().close();

        File db = new File("/sdcard/", "my-db");
        File dbShm = new File(db.getParent(), "my-db-shm");
        File dbWal = new File(db.getParent(), "my-db-wal");

        File db2 = getDatabasePath("my-db");
        File dbShm2 = new File(db2.getParent(), "my-db-shm");
        File dbWal2 = new File(db2.getParent(), "my-db-wal");

        try {
            FileUtils.copyFile(db, db2);
            FileUtils.copyFile(dbShm, dbShm2);
            FileUtils.copyFile(dbWal, dbWal2);
        } catch (Exception e) {
            Loge("RESTOREDB", e.toString());
        }
    } else {
        Snackbar.make(mDrawer, "Please allow access to your storage", Snackbar.LENGTH_LONG)
                .setAction("Allow", view -> ActivityCompat.requestPermissions(this, new String[] {
                        Manifest.permission.READ_EXTERNAL_STORAGE
                }, 0)).show();
    }
 }

【讨论】:

  • 我在获取实际数据库文件时没有问题。相反,我在完成备份过程后在 Room db 上运行 INSERT 时遇到错误。 (包括关闭房间 db 的程序)
  • 如何将备份文件迁移到最新的新应用数据库?因为当我尝试使用上面的代码恢复数据库时,最新的数据库被删除了
  • 你必须检查 wal 和 shm 文件是否存在,否则应该是问题
  • 如果我们发出RoomDatabase.Close(),我们将无法再次打开数据库?有什么解决方案吗?
【解决方案3】:

作为替代方案,您始终可以创建 Room 数据库,同时强制其不使用预写日志记录:

Room.databaseBuilder(context, db.class, dbName)
    .setJournalMode(JournalMode.TRUNCATE)
    .build();

【讨论】:

    【解决方案4】:

    为了更具体地回答您的问题,这就是我在我的一个应用中备份房间数据库的方式。

    1-检查读取/写入外部存储的权限。 2-关闭您的 RoomDatabase。在我的例子中,AppDatabase 指的是一个单例,其中包含最初构建房间数据库的逻辑。 AppDatabase.getInstance(this@MainActivity) 获取单例的当前实例及其当前数据库类,该类从 RoomDatabase 扩展而来。 3-然后基本上调用 dbInstance.close()。

    private fun createBackup() {
            val db = AppDatabase.getInstance(this@MainActivity)
            db.close()
            val dbFile: File = getDatabasePath(DATABASE_NAME)
            val sDir = File(Environment.getExternalStorageDirectory(), "Backup")
            val fileName = "Backup (${getDateTimeFromMillis(System.currentTimeMillis(), "dd-MM-yyyy-hh:mm")})"
            val sfPath = sDir.path + File.separator + fileName
            if (!sDir.exists()) {
                sDir.mkdirs()
            }
            val saveFile = File(sfPath)
            if (saveFile.exists()) {
                Log.d("LOGGER ", "File exists. Deleting it and then creating new file.")
                saveFile.delete()
            }
            try {
                if (saveFile.createNewFile()) {
                    val bufferSize = 8 * 1024
                    val buffer = ByteArray(bufferSize)
                    var bytesRead: Int
                    val saveDb: OutputStream = FileOutputStream(sfPath)
                    val indDb: InputStream = FileInputStream(dbFile)
                    do {
                        bytesRead = indDb.read(buffer, 0, bufferSize)
                        if (bytesRead < 0)
                            break
                        saveDb.write(buffer, 0, bytesRead)
                    } while (true)
                    saveDb.flush()
                    indDb.close()
                    saveDb.close()
                }
            } catch (e: Exception) {
                e.printStackTrace()
    
            }
        }
    

    您必须在其中包含保存文件

    try {
    //backup process
          }
            } catch (e: Exception) {
                e.printStackTrace()
    
            }
    

    按发生错误的顺序并避免应用崩溃。

    并从 currentTimeMillis 获取日期 使用这个功能

    fun getDateTimeFromMillis(millis: Long, pattern: String): String {
        val simpleDateFormat = SimpleDateFormat(pattern, Locale.getDefault()).format(Date())
        return simpleDateFormat.format(millis)
    }
    

    重新定位 Db 的代码 将文件对象传递给 Uri.fromFile

    try {
                                        val fileUri: Uri = Uri.fromFile(file)
                                        val inputStream = contentResolver.openInputStream(fileUri)
                                        println("restoring ")
                                        restoreDatabase(inputStream);
                                        inputStream?.close()
                                    } catch (e: IOException) {
                                        println( e.message)
                                        e.printStackTrace()
                                    }
    

    **或返回结果并带有开始活动的结果**

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (requestCode == 12 && resultCode == RESULT_OK && data != null) {
                Uri fileUri = data.getData();
                try {
                    assert fileUri != null;
                    InputStream inputStream = getContentResolver().openInputStream(fileUri);       
                        restoreDatabase(inputStream);
                        inputStream.close();
                    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    

    恢复数据库功能

    private fun restoreDatabase(inputStreamNewDB: InputStream?) {
            val db = AppDatabase.getInstance(this@MainActivity)
            db.close()
            val oldDB = getDatabasePath(DATABASE_NAME)
            if (inputStreamNewDB != null) {
                try {
                    copyFile(inputStreamNewDB as FileInputStream?, FileOutputStream(oldDB))
                    println("restore success")
                } catch (e: IOException) {
                    Log.d("BindingContextFactory ", "ex for is of restore: $e")
                    e.printStackTrace()
                }
            } else {
                Log.d("BindingContextFactory ", "Restore - file does not exists")
            }
        }
    

    现在您需要将文件从备份复制到真实的 db 文件中 使用复制文件

    @Throws(IOException::class)
        fun copyFile(fromFile: FileInputStream?, toFile: FileOutputStream) {
            var fromChannel: FileChannel? = null
            var toChannel: FileChannel? = null
            try {
                fromChannel = fromFile?.channel
                toChannel = toFile.channel
                fromChannel?.transferTo(0, fromChannel.size(), toChannel)
            } finally {
                try {
                    fromChannel?.close()
                } finally {
                    toChannel?.close()
                }
            }
        }
    

    【讨论】:

    • 你应该在备份和恢复过程之前关闭数据库,否则操作将不起作用。
    【解决方案5】:

    需要做的第一件事是创建具有适当日志模式的数据库。

    Room.databaseBuilder(context, AppDatabase::class.java, name)
        .setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
        .build()
    

    之后,需要执行以下检查点查询以确保应用所有待处理的事务。

    为此需要在数据库Dao接口中添加以下方法

    interface UserDao {
        @RawQuery
        fun checkpoint(supportSQLiteQuery: SupportSQLiteQuery?): Single<Int>
    }
    

    那么需要用下面的SQL查询调用方法

    userDao.checkpoint((SimpleSQLiteQuery("pragma wal_checkpoint(full)")))
    

    一旦检查点方法成功,数据库备份文件终于可以保存了。

    最后可以使用以下代码检索数据库备份文件

    File(database.openHelper.writableDatabase.path)
    

    然后需要将该文件复制到备份文件位置。

    要恢复文件,最重要的事情是用备份文件覆盖数据库文件(可以使用上面的 sn-p 检索)。

    您可以在我的博客上阅读更多详细信息

    https://androidexplained.github.io/android/room/2020/10/03/room-backup-restore.html

    【讨论】:

    • 如果有多个 Dao,是否需要从每个 Dao 调用一个检查点函数?
    • 您已切换到 TRUNCATE 模式,因此不需要 wal_checkpoint 语句。来自 SQLite 文档:“如果禁用了预写日志模式,则此编译指示是无害的无操作”
    【解决方案6】:

    首先,必须关闭数据库才能应用来自"dbName.db-wal" 文件的更改。

    然后你可以复制所有表和最后数据更改的数据库

     AppDatabase appDatabase = AppDatabase.getAppDatabase(getApplicationContext());
     appDatabase.close();
    

    【讨论】:

      【解决方案7】:

      上面已经回答过了。无需关闭/重新打开数据库。

      我在我的 android 应用程序中使用 MVVM 模式来备份 db 文件以将其上传到谷歌驱动器。只想总结一下对我有用的解决方案:

      在您的 DAO 文件中提及以下代码:

      @RawQuery
          int checkpoint(SupportSQLiteQuery supportSQLiteQuery);
      

      在您的存储库文件中提及以下代码:

          /* Android database has three files under /data/data/com.package.app/databases/
          ** test.db, test.db-shm, test.db-wal - those extra files have recent commits.
          ** To merge data from other shm and wal files to db, run following method - useful before taking backup.
          */
          void checkPoint() {
              ItemRoomDatabase.databaseWriteExecutor.execute(() -> {
                 itemDao.checkpoint(new SimpleSQLiteQuery("pragma wal_checkpoint(full)"));
              });
          }
      

      在您的 ViewModel 中提及以下内容:

      public void checkPoint() {
            itemRepository.checkPoint();
      }
      

      现在您可以在从 Activity 文件进行备份之前调用此方法

      ItemViewModel itemViewModel = new ViewModelProvider(this).get(ItemViewModel.class);
      itemViewModel.checkPoint();
      

      【讨论】:

      • 如果有多个 Dao,是否需要从每个 Dao 调用一个检查点函数?
      【解决方案8】:

      我正在使用这个库来备份和恢复房间数据库,它超级好用。

      https://github.com/salehyarahmadi/RoomDatabaseBackupAndRestore
      

      感谢 salehyarahmadi

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-02-20
        • 2021-12-08
        • 2020-09-04
        • 2015-04-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多