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

88

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

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

我曾经在另一个应用程序中成功实现了类似于 SharedPreferences 的解决方案。但是当我在模拟器-2.2中测试我的新实现时,从日志中可以看到备份被执行到 LocalTransport,还有恢复被执行(并调用了 onRestore())。然而,db 文件本身从未被创建。

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

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

文档中提到了数据库,建议使用自定义的 BackupAgent,但这似乎与此无关:

然而,您可能希望扩展

如果需要备份代理,请直接使用BackupAgent: * 备份数据库中的数据。如果您有一个SQLite数据库,想要在用户重新安装应用程序时还原它,那么您需要构建一个定制的BackupAgent,在备份操作期间读取相应数据,然后在还原操作期间创建表并插入数据。
一些澄清: 如果我确实需要自己完成SQL级别的工作,那么我会担心以下问题:
1. 打开数据库和事务。我不知道如何从这样一个单例类之外的应用程序工作流程中关闭它们。 2. 如何通知用户备份正在进行中,并且数据库已被锁定。这可能需要很长时间,因此我可能需要显示进度条。 3. 如何在恢复时执行相同的操作。据我所知,恢复可能发生在用户已经开始使用应用程序(并将数据输入到数据库)的情况下。因此,您不能假设只是在原地还原备份数据(删除空或旧数据)。你必须以某种方式加入它,对于任何非平凡的数据库来说,由于id的存在是不可能的。 4. 在还原完成后如何刷新应用程序,而不使用户卡在某个现在无法到达的点。 5. 我能否确信备份或还原过程中数据库已经升级?否则,期望的架构可能不匹配。

有没有简单备份整个数据库的方法? - Jin35
我需要使用FileBackupHelper备份一个文件夹。使用保存数据库的方法能否让我保存该文件夹及其下面有很多子文件夹和子文件? - coolcool1994
你最终解决了这个问题吗? - DeNitE Appz
是的,请看下面。我也回答了自己的问题。 - pjv
6个回答

34

在重新审视我的问题后,我通过查看ConnectBot如何处理来解决了这个问题。感谢Kenny和Jeffrey!

实际上,只需添加以下内容:

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

对于你的BackupAgentHelper,我所忽略的是你需要使用相对路径"../databases/"。

然而,这并不是一个完美的解决方案。例如,FileBackupHelper 的文档中提到:“FileBackupHelper 应仅用于小型配置文件,而不是大型二进制文件。”,后者是 SQLite 数据库的情况。

我希望能够得到更多建议、洞见(我们期望什么是正确的解决方案),以及关于这可能出现问题的建议。


1
我应该补充一下,尽管这有点非官方,但一直以来它都运行得非常好。 - pjv
7
硬编码路径是不好的。 - Pointer Null
@pjv,那么您是否使每个数据库交互都同步?我正在做同样的事情,并尝试弄清楚是否需要通过db.open()db.close()或仅限于insert语句来同步。 - NSouth

22

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

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提供名称。 然后所有应用程序的数据库都将保存在备份中。


将@pjv提供的解决方案与此相结合,效果完美!可能不完全符合规格要求...但它运行得非常好! - Tom
1
如果你需要备份数据库和普通文件,那么这就没有太大用处了。 - Dan Hulme
1
@Dan:在这种情况下使用多个代理。 - pjv
@指针空值 你能详细解释一下getFilesDir()的作用和必要性吗?它是如何工作的? - powder366

21
一个更加优雅的方法是创建一个自定义的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));
}

2
@logray:那段代码与问题中完全相同。不必要的编辑。 - Linuxios
@Linuxios 我同意,不过我认为最好是给作者提供适当的归属,而不是盲目地粘贴代码...考虑到原始的 OP/帖子包含了他的应用程序链接。 - logray
3
重要提示:备份文件的相关文档(http://developer.android.com/guide/topics/data/backup.html#Files)指出,该操作**不是线程安全的**,因此您应该同步处理数据库方法和备份代理。 - nicopico
@yanchenko FileBackupHelper的文档说明: “注意:仅应将其用于小型配置文件,而不是大型二进制文件。”因此,数据库通常会被归类为大型二进制文件... - IgorGanapolsky
这实际上不起作用!(除非我漏掉了什么...)。问题在于您将db的绝对路径传递给FileBackupHelper,但是FileBackupHelper会将路径前缀添加到文件目录路径中,例如data/data/<your package>/files,这导致了一个不正确的路径,例如data/data/<your package>/files/data/data/<your package>/databases/<DB Name>,而您想要的只是DB的绝对名称,由于超类将文件目录前缀添加到路径中,因此您需要使路径相对于文件目录。 - bitrock
对我很有用。这个链接用于测试:http://developer.android.com/guide/topics/data/backup.html#Testing - powder366

8
使用FileBackupHelper备份/恢复sqlite数据库会引起一些严重的问题:
1. 如果应用程序使用从ContentProvider.query()检索的游标,而备份代理尝试覆盖整个文件会发生什么情况?
2. 链接是一个完美(熵值低 ;))测试的好例子。您卸载应用程序,重新安装它,备份将被恢复。然而,生活可能是残酷的。看看链接。让我们想象一个场景,当用户购买新设备时。由于它没有自己的设置,备份代理使用其他设备的设置。应用程序被安装,您的backupHelper检索到具有低于当前版本的db版本模式的旧文件。SQLiteOpenHelper调用默认实现的onDowngrade
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can't downgrade database from version " +
            oldVersion + " to " + newVersion);
}

无论用户怎么做,他/她都无法在新设备上使用您的应用程序。
我建议使用ContentResolver来获取数据->序列化(不带_id)备份和反序列化->插入数据进行还原。
注意:通过ContentResolver进行数据获取/插入,从而避免并发问题。序列化是在您的backupAgent中完成的。如果您自己执行游标<->对象映射,则可以简单地实现将一个项序列化为具有表示实体的类上的Serializabletransient字段 _id。
我还建议使用批量插入,即ContentProviderOperation 示例CursorLoader.setUpdateThrottle,以便应用程序在备份还原过程中不会受到在数据更改时重新启动加载器的影响。
如果您遇到降级的情况,您可以选择放弃还原数据或恢复并更新与降级版本相关的ContentResolver字段。
我同意这个主题并不容易,在文档中没有很好的解释,还有一些问题仍然存在,例如批量数据大小等等。
希望这可以帮到您。

你重复了一些有效的问题。但我不明白序列化数据库如何解决问题。它并不能帮助解决并发问题。如果你无法编写一个脚本来降级数据库,那么你也无法编写一个脚本来反序列化旧数据。 - pjv
如果您使用ContentResolver,它会为您解决并发问题。 - IgorGanapolsky
@IgorGanapolsky 是的,我认为这是正确的。但是,如果序列化足够慢,并且用户已经在使用应用程序并输入数据,则可能会与您正在恢复的数据发生冲突。您能在用户访问应用程序之前以原子方式执行吗? - pjv
@pjv 你可以注册一个ContentObserver来同步你正在观察的数据,以便在通知时得到提醒。因此,你不必担心冲突。 - IgorGanapolsky
你关于降级和用户无法使用应用程序的观点是错误的。旧版本存储在文件中,新版本是你在SQLiteOpenHelper构造函数中声明的版本。将调用onUpgrade(),就像用户升级应用程序一样。请参见SQLiteOpenHelper源代码的253行左右。 - sorianiv
显示剩余2条评论

5
自 Android M 开始,应用程序现在可以使用全数据备份/还原 API。这个新的 API 包括一个基于 XML 的规范,在应用程序清单中让开发人员以直接语义的方式描述要备份的文件:'备份名为“mydata.db”的数据库'。这个新的 API 更容易供开发人员使用 -- 你不必跟踪差异或显式请求备份通行证,并且关于哪些文件需要备份的 XML 描述意味着你通常不需要编写任何代码。
(即使在完整数据备份/还原操作中,您也可以参与并在还原发生时获取回调。它非常灵活。)
请参阅 developer.android.com 上的 为应用程序配置自动备份 部分,了解如何使用新的 API。

1
这仅适用于Android 6.0(API 23),如果用户使用的是旧版本,则仍需实现键值备份。 - live-love

0

一种选择是在数据库之上构建应用程序逻辑。我觉得它实际上呼唤这样的层次。

不确定你是否已经这样做了,但大多数人(尽管使用 Android 内容管理器光标方法)都会引入一些 ORM 映射 - 自定义或一些 ORM-Lite 方法。在这种情况下,我宁愿这样做:

  1. 确保您的应用程序在后台添加新数据/移除数据时能正常工作
  2. 进行一些Java-> protobuf甚至只是简单的Java序列化映射,并编写自己的BackupHelper从流中读取数据并将其简单地添加到数据库中...

因此,在这种情况下,与其在数据库级别上执行操作,不如在应用程序级别上执行。


问题不在于如何将数据从数据库传输到BackupHelperAgent,反之亦然。请阅读我问题末尾的5个要点。 - pjv
@JarekPotiuk 你说:“大多数人(尽管 Android 内容管理器光标方法)”。但是,是什么让你认为大多数人会避免使用 ContentProvider 和 ContentResolver 来访问数据库呢? - IgorGanapolsky

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