备份 Room 数据库

37

我正在尝试通过编程方式备份一个Room数据库。

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

但是,在复制之前,由于Room启用了写日志记录,我们必须关闭数据库,以便-shm文件和-wal文件合并为单个.sqlite文件。 如此处所指出的那样

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

备份一切正常,但是后来,当我尝试执行一个INSERT查询时,我会得到这个错误:

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

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

PS:RoomDatabase 对象上的 .isOpen() 方法会在 INSERT 之前返回 true

Room 版本:1.1.1-rc1


1
尝试不要关闭数据库并将dataBaseHelper类设置为单例以避免此类错误。 - Tara
7个回答

51
这并没有回答原始问题。
如果您想要将所有内容移动到原始数据库文件中,那么您就不需要首先关闭数据库。相反,您可以使用wal_checkpoint pragma强制进行检查点。
针对数据库查询以下语句。我们在这里使用原始查询,因为Room尚不支持pragma(它会触发一个UNKNOWN query type错误)。将此查询放在您的DAO内部:
@RawQuery
int checkpoint(SupportSQLiteQuery supportSQLiteQuery);

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

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

这个链接可能对wal_checkpoint的作用有所启示。


4
尽管这并没有回答我的问题“我如何在关闭Room数据库后正确地重新打开它?”,但该解决方案解决了我的问题。首先根本不需要关闭Room数据库。这也应该是https://dev59.com/iFUM5IYBdhLWcg3wEM0d的被接受的答案。 - Alex Busuioc
我应该在哪里调用 myDAO.checkpoint(new SimpleSQLiteQuery("pragma wal_checkpoint(full)")); - Zakaria Darwish
@BertramGilfoyle 我知道,但我对Room不熟悉,我正在跟随一个教程,该教程将从存储库调用所有数据,并从片段/活动中调用所有存储库,因此如果我想备份,我应该在备份和恢复之前调用它还是在活动创建时? - Zakaria Darwish
是的,这对我有用,但现在我遇到了恢复数据库的问题。就像恢复成功了,但我认为数据库没有更新或类似的问题,你能帮我解决吗?谢谢。 - Gulab Sagevadiya
1
在我的情况下,在备份数据库文件之前运行查询后仍有一些数据丢失,但关闭数据库却没有这种情况。我现在创建备份后重新启动应用程序作为解决方法。 - ElegyD
显示剩余3条评论

16

更具体地回答您的问题,这是我在我的一个应用程序中备份房间数据库的方法:

  1. 检查是否具有从/写入外部存储器的权限。如果您将文件写入应用程序文件目录,则可以忽略此步骤。
  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();
    }
 }

5
我并没有问题获取实际的数据库文件,而是在我完成备份过程后,运行Room数据库中的INSERT时出现了错误。(该备份过程包括关闭Room数据库) - Alex Busuioc

6
作为替代方案,您始终可以创建 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)
}

恢复数据库的代码 将文件对象传递给 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();
            }
        }
    }

restoreDatabase函数

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")
        }
    }

现在你需要将备份文件复制到真实的数据库文件中 使用copyFile函数

@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()
            }
        }
    }

在备份和恢复过程中,您应该关闭数据库,否则操作将无法正常工作。 - Dhia Shalabi

3

首先需要做的事情是创建具有适当日志模式的数据库。

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)

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

恢复文件的唯一操作就是用备份文件覆盖数据库文件(可以使用上面的代码片段检索数据库文件)。

您可以在我的博客上阅读更详细的内容。

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


如果你有多个Dao,那么是否需要从每个Dao中调用一个checkpoint函数? - adriennoir
2
您已切换到TRUNCATE模式,因此不需要wal_checkpoint语句。根据SQLite文档:“如果禁用了写日志模式,则此编译指示符是无害的无操作”。 - Reza Mohammadi

0

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

然后,您可以复制具有所有表和最新数据更改的数据库。

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

0

以上已经有答案了。无需关闭/重新打开数据库。

我在我的Android应用程序中使用MVVM模式备份db文件以将其上传到Google Drive。只想总结一下对我起作用的解决方案:

在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调用一个checkpoint函数? - adriennoir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接