Android备份/恢复:如何备份内部数据库?

发布于 2024-10-21 14:23:26 字数 1925 浏览 7 评论 0 原文

我已经使用提供的 FileBackupHelper 实现了一个 BackupAgentHelper 来备份和恢复我拥有的本机数据库。这是您通常与 ContentProviders 一起使用的数据库,位于 /data/data/yourpackage/databases/ 中。

人们可能会认为这是一个常见的情况。然而,文档并不清楚该怎么做: http://developer.android .com/guide/topics/data/backup.html没有专门针对这些典型数据库的BackupHelper。因此,我使用了 FileBackupHelper,将其指向“/databases/”中的 .db 文件,在任何数据库操作周围引入了锁(例如 db.insert) 在我的 ContentProviders 中,甚至尝试在 onRestore() 之前创建“/databases/”目录,因为它安装后不存在。

我过去曾在不同的应用程序中成功地为 SharedPreferences 实现了类似的解决方案。然而,当我在 emulator-2.2 中测试我的新实现时,我看到正在从日志执行到 LocalTransport 的备份,以及正在执行的恢复(以及 onRestore() > 称为)。 但是,数据库文件本身永远不会创建。

请注意,这都是在安装之后、首次启动应用程序之前、执行恢复之后。除此之外,我的测试策略基于 http://developer.android。 com/guide/topics/data/backup.html#Testing

另请注意,我不是在谈论我自己管理的某些 sqlite 数据库,也不是在谈论备份到 SD 卡、自己的服务器或其他地方。

我确实在文档中看到有关建议使用自定义 BackupAgent 的数据库的提及,但它似乎并不相关:

但是,您可能想要扩展 如果您需要,请直接 BackupAgent: * 备份数据库中的数据。如果您有一个 SQLite 数据库 当用户想要恢复时 重新安装您的应用程序,您需要 构建一个自定义的 BackupAgent 期间读取适当的数据 备份操作,然后创建您的 表并在期间插入数据 恢复操作。

请说清楚。

如果我确实需要自己完成 SQL 级别的工作,那么我会担心以下主题:

  • 打开数据库和事务。我不知道如何从应用程序工作流程之外的此类单例类中关闭它们。

  • 如何通知用户备份正在进行中并且数据库已锁定。这可能需要很长时间,所以我可能需要显示一个进度条。

  • 如何在恢复时执行相同的操作。据我了解,恢复可能会在用户已经开始使用应用程序(并将数据输入数据库)时发生。因此,您不能假设只是将备份的数据恢复到位(删除空数据或旧数据)。您必须以某种方式加入它,这对于任何重要的数据库来说都是不可能的,因为 ID 的原因。

  • 如何在恢复完成后刷新应用程序,而不会让用户卡在某些 - 现在 - 无法访问的点。

  • 我能否确定数据库已在备份或恢复时升级?否则,预期的架构可能不匹配。

I have implemented a BackupAgentHelper using the provided FileBackupHelper to backup and restore the native database I have. This is the database you typically use along with ContentProviders and which resides in /data/data/yourpackage/databases/.

One would think this is a common case. However the docs aren't clear on what to do: http://developer.android.com/guide/topics/data/backup.html. There is no BackupHelper specifically for these typical databases. Hence I used the FileBackupHelper, pointed it to my .db file in "/databases/", introduced locks around any db operation (such as db.insert) in my ContentProviders, and even tried creating the "/databases/" directory before onRestore() because it does not exist after install.

I have implemented a similar solution for the SharedPreferences successfully in a different app in the past. However when I test my new implementation in the emulator-2.2, I see a backup being performed to LocalTransport from the logs, as well as a restore being performed (and onRestore() called). Yet, the db file itself is never created.

Note that this is all after an install, and before first launch of the app, after the restore has been performed. Apart from that my test strategy was based on http://developer.android.com/guide/topics/data/backup.html#Testing.

Please also note I'm not talking about some sqlite database I manage myself, nor about backing up to SDcard, own server or elsewhere.

I did see a mention in the docs about databases advising to use a custom BackupAgent but it does not seem related:

However, you might want to extend
BackupAgent directly if you need to:
* Back up data in a database. If you have an SQLite database that you
want to restore when the user
re-installs your application, you need
to build a custom BackupAgent that
reads the appropriate data during a
backup operation, then create your
table and insert the data during a
restore operation.

Some clarity please.

If I really need to do it myself up to the SQL level, then I'm worried about the following topics:

  • Open databases and transactions. I have no idea how to close them from such a singleton class outside of my app's workflow.

  • How to notify the user that a backup is in progress and the database is locked. It might take a long time, so I might need to show a progress bar.

  • How to do the same on restore. As I understand, the restore might happen just when the user has already started using the app (and inputting data into the database). So you can't presume to just restore the backupped data in place (deleting the empty or old data). You'll have to somehow join it in, which for any non-trivial database is impossible due to the id's.

  • How to refresh the app after the restore is done without getting the user stuck at some - now - unreachable point.

  • Can I be sure the database has already been upgraded on backup or restore? Otherwise the expected schema might not match.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

冬天旳寂寞 2024-10-28 14:23:27

一种选择是将其构建在数据库之上的应用程序逻辑中。我认为它实际上尖叫着这样的水平。
不确定你是否已经这样做了,但大多数人(尽管有 android 内容管理器光标方法)会引入一些 ORM 映射 - 自定义或一些 orm-lite 方法。在这种情况下我宁愿做的是:

  1. 确保您的应用程序正常工作
    添加应用程序/数据后就可以了
    新数据的背景
    应用程序时添加/删除
    已经开始
  2. 做一些
    Java->protobuf 甚至简单的 java
    序列化映射并编写您的
    自带BackupHelper读取数据
    从流中并将其添加到
    数据库....

所以在这种情况下,不是在数据库级别上执行,而是在应用程序级别上执行。

One option will be to build it in application logic above the database. It actually screams for such levell I think.
Not sure if you are doing it already but most people (despite android content manager cursor approach) will introduce some ORM mapping - either custom or some orm-lite approach. And what I would rather do in this case is:

  1. to make sure your application works
    fine when the app/data is added in
    the background with new data
    added/removed while the application
    already started
  2. to make some
    Java->protobuf or even simply java
    serialization mapping and write your
    own BackupHelper to read the data
    from the stream and simply add it to
    database....

So in this case rather than doing it on db level do it on application level.

花间憩 2024-10-28 14:23:27

文档建议扩展 BackupAgent 而不是 BackupAgentHelper 当您需要对正在备份的数据进行精细控制时,数据库在此处特别命名:

如果您希望在用户重新安装您的应用程序时恢复 SQLite 数据库,则需要构建一个自定义 BackupAgent,用于在备份操作期间读取适当的数据,然后创建表并在备份期间插入数据。恢复操作。

这是一个有效的示例,因为线程中的其他答案都不适合我(尽管我希望相对路径黑客!)

public class CustomBackupAgent extends BackupAgent {
   private static final String TAG = "LOGTAG";
   private static final String DATABASE_BACKUP_KEY = "database_backup";

   @Override
   public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
      File dbFile = getDatabasePath(YOUR_DB_NAME);
      backupFile(dbFile, DATABASE_BACKUP_KEY, data);
   }

   @Override
   public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
      while (data.readNextHeader()) {
         String key = data.getKey();
         int dataSize = data.getDataSize();

         switch (key) {
            // More cases here if you're backing up multiple files
            case DATABASE_BACKUP_KEY:
               File dbFile = getDatabasePath(YOUR_DB_NAME);
               restoreFile(dbFile, dataSize, data);
               break;
            default:
               Log.w(TAG, "Unknown backup key: " + key);
               break;
         }
      }
   }

   private void backupFile(File file, String key, BackupDataOutput data) throws IOException {
      if (file.exists()) {
         byte[] buffer = new byte[1024];
         FileInputStream fileInputStream = new FileInputStream(file);
         long fileSize = file.length();
         data.writeEntityHeader(key, (int) fileSize);

         int bytesRead;
         long totalBytesRead = 0;
         while ((bytesRead = fileInputStream.read(buffer)) != -1) {
            data.writeEntityData(buffer, bytesRead);
            totalBytesRead += bytesRead;
         }
         fileInputStream.close();
         Log.d(TAG, "Backed up file: " + file.getName() + ". Total bytes: " + totalBytesRead);
      } else {
         Log.w(TAG, "File does not exist: " + file.getAbsolutePath());
      }
   }

   private void restoreFile(File file, int dataSize, BackupDataInput data) throws IOException {
      byte[] buffer = new byte[dataSize];
      FileOutputStream fileOutputStream = new FileOutputStream(file);
      fileOutputStream.write(buffer);
      fileOutputStream.close();
      Log.d(TAG, "Restored file: " + file.getName() + " with size: " + dataSize);
   }
}

关于使用自定义备份代理的一些注意事项:

  • 您不需要用于数据提取规则和旧备份规则的 XML。自定义代理覆盖自动备份系统而不是扩展它。

  • 另请注意,运行 bmgr backupnow 时看不到任何进度日志是正常的,就像在没有自定义代理的情况下看到的那样。

  • 最后,在使用模拟器进行测试期间,备份不会像您所期望的那样保存在 /data/backup/com.android.localtransport.LocalTransport 中。它是模拟器的默认传输(否则可以手动设置),但此处找不到备份文件。

The docs suggest extending BackupAgent and not BackupAgentHelper when you require granular control over the data you are backing up and databases are specifically named here:

If you have an SQLite database that you want to restore when the user re-installs your app, you need to build a custom BackupAgent that reads the appropriate data during a backup operation, then create your table and insert the data during a restore operation.

This is a working example of that because none of the other answers in the thread worked for me (though I had hope for the relative path hack!)

public class CustomBackupAgent extends BackupAgent {
   private static final String TAG = "LOGTAG";
   private static final String DATABASE_BACKUP_KEY = "database_backup";

   @Override
   public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
      File dbFile = getDatabasePath(YOUR_DB_NAME);
      backupFile(dbFile, DATABASE_BACKUP_KEY, data);
   }

   @Override
   public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
      while (data.readNextHeader()) {
         String key = data.getKey();
         int dataSize = data.getDataSize();

         switch (key) {
            // More cases here if you're backing up multiple files
            case DATABASE_BACKUP_KEY:
               File dbFile = getDatabasePath(YOUR_DB_NAME);
               restoreFile(dbFile, dataSize, data);
               break;
            default:
               Log.w(TAG, "Unknown backup key: " + key);
               break;
         }
      }
   }

   private void backupFile(File file, String key, BackupDataOutput data) throws IOException {
      if (file.exists()) {
         byte[] buffer = new byte[1024];
         FileInputStream fileInputStream = new FileInputStream(file);
         long fileSize = file.length();
         data.writeEntityHeader(key, (int) fileSize);

         int bytesRead;
         long totalBytesRead = 0;
         while ((bytesRead = fileInputStream.read(buffer)) != -1) {
            data.writeEntityData(buffer, bytesRead);
            totalBytesRead += bytesRead;
         }
         fileInputStream.close();
         Log.d(TAG, "Backed up file: " + file.getName() + ". Total bytes: " + totalBytesRead);
      } else {
         Log.w(TAG, "File does not exist: " + file.getAbsolutePath());
      }
   }

   private void restoreFile(File file, int dataSize, BackupDataInput data) throws IOException {
      byte[] buffer = new byte[dataSize];
      FileOutputStream fileOutputStream = new FileOutputStream(file);
      fileOutputStream.write(buffer);
      fileOutputStream.close();
      Log.d(TAG, "Restored file: " + file.getName() + " with size: " + dataSize);
   }
}

A few notes on using a custom backup agent:

  • You don't need the XMLs for data extraction rules and the legacy backup rules. The custom agent overrides the auto backup system instead of extending it.

  • Also note that it's normal not to see any progress logs when running bmgr backupnow as you would have seen without the custom agent.

  • And finally, during testing with e.g. an emulator, the backups are not saved in /data/backup/com.android.localtransport.LocalTransport as you might expect. It is the default transport for emulators (and otherwise can be manually set), but the backup files are not found here.

森林散布 2024-10-28 14:23:26

重新审视我的问题后,我在查看 ConnectBot 如何做到这一点。谢谢肯尼和杰弗里!

实际上就像添加:到您的 BackupAgentHelper 一样简单

FileBackupHelper hosts = new FileBackupHelper(this,
    "../databases/" + HostDatabase.DB_NAME);
addHelper(HostDatabase.DB_NAME, hosts);

我忽略的一点是,您必须使用带有“../databases/”的相对路径。

尽管如此,这绝不是一个完美的解决方案。 FileBackupHelper 的文档提到,例如:“FileBackupHelper 应该仅用于小型配置文件,而不是大型二进制文件。”,后者是SQLite 数据库就是这样。

我想获得更多建议,深入了解我们的期望(正确的解决方案是什么),以及如何解决这个问题的建议。

After revisiting my question, I was able to get it to work after looking at how ConnectBot does it. Thanks Kenny and Jeffrey!

It's actually as easy as adding:

FileBackupHelper hosts = new FileBackupHelper(this,
    "../databases/" + HostDatabase.DB_NAME);
addHelper(HostDatabase.DB_NAME, hosts);

to your BackupAgentHelper.

The point I was missing was the fact that you'd have to use a relative path with "../databases/".

Still, this is by no means a perfect solution. The docs for FileBackupHelper mention for instance: "FileBackupHelper should be used only with small configuration files, not large binary files.", the latter being the case with SQLite databases.

I'd like to get more suggestions, insights into what is expected of us (what is the proper solution), and advice on how this might break.

白芷 2024-10-28 14:23:26

这是将数据库备份为文件的更简洁的方法。没有硬编码路径。

class MyBackupAgent extends BackupAgentHelper{
   private static final String DB_NAME = "my_db";

   @Override
   public void onCreate(){
      FileBackupHelper dbs = new FileBackupHelper(this, DB_NAME);
      addHelper("dbs", dbs);
   }

   @Override
   public File getFilesDir(){
      File path = getDatabasePath(DB_NAME);
      return path.getParentFile();
   }
}

注意:它覆盖 getFilesDir 以便 FileBackupHelper 在数据库目录中工作,而不是文件目录。

另一个提示:您还可以使用 databaseList 来获取所有数据库和将此列表中的名称(不带父路径)馈送到 FileBackupHelper 中。然后所有应用程序的数据库将保存在备份中。

Here's yet cleaner way to backup databases as files. No hardcoded paths.

class MyBackupAgent extends BackupAgentHelper{
   private static final String DB_NAME = "my_db";

   @Override
   public void onCreate(){
      FileBackupHelper dbs = new FileBackupHelper(this, DB_NAME);
      addHelper("dbs", dbs);
   }

   @Override
   public File getFilesDir(){
      File path = getDatabasePath(DB_NAME);
      return path.getParentFile();
   }
}

Note: it overrides getFilesDir so that FileBackupHelper works in databases dir, not files dir.

Another hint: you may also use databaseList to get all your DB's and feed names from this list (without parent path) into FileBackupHelper. Then all app's DB's would be saved in backup.

猫腻 2024-10-28 14:23:26

更简洁的方法是创建自定义 BackupHelper

public class DbBackupHelper extends FileBackupHelper {

    public DbBackupHelper(Context ctx, String dbName) {
        super(ctx, ctx.getDatabasePath(dbName).getAbsolutePath());
    }
}

然后将其添加到 BackupAgentHelper

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}

A cleaner approach would be to create a custom BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

    public DbBackupHelper(Context ctx, String dbName) {
        super(ctx, ctx.getDatabasePath(dbName).getAbsolutePath());
    }
}

and then add it to BackupAgentHelper:

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}
风筝有风,海豚有海 2024-10-28 14:23:26

使用 FileBackupHelper 备份/恢复 sqlite 数据库会引发一些严重的问题:
1. 如果应用程序使用从 ContentProvider.query() 检索的游标并且备份代理尝试覆盖整个文件,会发生什么情况?
2. 链接是完美(低熵;)测试。您卸载应用程序,重新安装它,备份就会恢复。然而生活可能是残酷的。查看

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can't downgrade database from version " +
            oldVersion + " to " + newVersion);
}

无论用户做什么,他/她都无法在新设备上使用您的应用。

我建议使用 ContentResolver 来获取数据 ->序列化(不带 _ids)以进行备份和反序列化 ->插入数据进行恢复。

注意:获取/插入数据是通过 ContentResolver 完成的,从而避免了并发问题。序列化是在您的 backupAgent 中完成的。如果您自己进行光标<->对象映射,序列化一个项目就可以像在表示您的实体的类上使用transient字段_id实现Serialized一样简单。

我还使用批量插入,即 ContentProviderOperation exampleCursorLoader.setUpdateThrottle 以便应用程序不会在备份恢复过程中因数据更改而重新启动加载程序。

如果您碰巧遇到降级情况,您可以选择中止恢复数据或使用与降级版本相关的字段恢复和更新 ContentResolver。

我同意这个主题并不容易,文档中没有很好地解释,并且一些问题仍然存在,例如批量数据大小等。

希望这会有所帮助。

Using FileBackupHelper to backup/restore sqlite db raises some serious questions:

1. What happens if the app uses cursor retrieved from ContentProvider.query() and backup agent tries to override the whole file?
2. The link is a nice example of perfect (low entrophy ;) testing. You uninstall app, install it again and the backup is restored. However life can be brutal. Take a look at link. Let's imagine scenario when a user buys a new device. Since it doesn't have its own set, the backup agent uses other device's set. The app is installed and your backupHelper retrieves old file with db version schema lower than the current. SQLiteOpenHelper calls onDowngrade with the default implementation:

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can't downgrade database from version " +
            oldVersion + " to " + newVersion);
}

No matter what the user does he/she can't use your app on the new device.

I'd suggest using ContentResolver to get data -> serialize (without _ids) for backup and deserialize -> insert data for restore.

Note: get/insert data is done through ContentResolver thus avoiding cuncurrency issues. Serializing is done in your backupAgent. If you do your own cursor<->object mapping serializing an item can be as simple as implementing Serializable with transient field _id on the class representing your entity.

I'd also use bulk insert i.e. ContentProviderOperation example and CursorLoader.setUpdateThrottle so that the app is not stuck with restarting loader on data change during backup restore process.

If you happen do be in a situation of a downgrade, you can choose either to abort restore data or restore and update ContentResolver with fields relevant to the downgraded version.

I agree that the subject is not easy, not well explained in docs and some questions still remain like bulk data size etc.

Hope this helps.

大海や 2024-10-28 14:23:26

从 Android M 开始,现在有一个可供应用程序使用的完整数据备份/恢复 API。这个新的 API 在应用程序清单中包含基于 XML 的规范,使开发人员能够以直接语义方式描述要备份的文件:“备份名为“mydata.db”的数据库”。这个新的 API 对于开发人员来说更容易使用——您不必跟踪差异或显式请求备份通道,并且要备份的文件的 XML 描述意味着您通常不需要编写任何代码根本不。

(例如,您甚至甚至可以参与完整数据备份/恢复操作,以便在恢复发生时获得回调。这样很灵活。)

请参阅为应用程序配置自动备份部分位于developer.android.com,了解如何使用新 API 的说明。

As of Android M, there is now a full-data backup/restore API available to apps. This new API includes an XML-based specification in the app manifest that lets the developer describe which files to back up in a direct semantic way: 'back up the database called "mydata.db"'. This new API is much easier for developers to use -- you don't have to keep track of diffs or request a backup pass explicitly, and the XML description of which files to back up means you often don't need to write any code at all.

(You can get involved even in a full-data backup/restore operation to get a callback when the restore happens, for example. It's flexible that way.)

See the Configuring Auto Backup for Apps section on developer.android.com for a description of how to use the new API.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文