Android Backup / Restore: Wie sichern Sie eine interne Datenbank?

87

Ich habe implementiert eine BackupAgentHelperder vorgesehenen Verwendung FileBackupHelperzu sichern und wiederherzustellen die native Datenbank ich habe. Dies ist die Datenbank, die Sie normalerweise zusammen mit der Datenbank verwenden ContentProvidersund in der Sie sich befinden /data/data/yourpackage/databases/.

Man würde denken, dass dies ein häufiger Fall ist. In den Dokumenten ist jedoch nicht klar, was zu tun ist: http://developer.android.com/guide/topics/data/backup.html . Es gibt keine BackupHelperspeziell für diese typischen Datenbanken. Daher habe ich das verwendetFileBackupHelper , auf meine .db-Datei in " /databases/" verwiesen , Sperren für alle db-Operationen (wie z. B. db.insert) in meinem eingeführt ContentProvidersund sogar versucht, das /databases/Verzeichnis " " vorher zu erstellen, onRestore()da es nach der Installation nicht vorhanden ist.

Ich habe SharedPreferencesin der Vergangenheit eine ähnliche Lösung für die erfolgreich in einer anderen App implementiert . Wenn ich jedoch meine neue Implementierung im Emulator-2.2 teste, wird eine Sicherung LocalTransportaus den Protokollen sowie eine Wiederherstellung durchgeführt (und onRestore()aufgerufen). Die Datenbankdatei selbst wird jedoch nie erstellt.

Beachten Sie, dass dies alles nach einer Installation und vor dem ersten Start der App, nachdem die Wiederherstellung durchgeführt wurde, erfolgt. Abgesehen davon basierte meine Teststrategie auf http://developer.android.com/guide/topics/data/backup.html#Testing .

Bitte beachten Sie auch, dass ich weder über eine von mir selbst verwaltete SQLite-Datenbank noch über das Sichern auf einer SD-Karte, einem eigenen Server oder anderswo spreche.

Ich habe in den Dokumenten eine Erwähnung von Datenbanken gesehen, die die Verwendung eines benutzerdefinierten Benutzers empfehlen, BackupAgentaber es scheint nicht verwandt zu sein:

Möglicherweise möchten Sie BackupAgent jedoch direkt erweitern, wenn Sie: * Daten in einer Datenbank sichern müssen. Wenn Sie über eine SQLite-Datenbank verfügen, die Sie wiederherstellen möchten, wenn der Benutzer Ihre Anwendung erneut installiert, müssen Sie einen benutzerdefinierten BackupAgent erstellen, der die entsprechenden Daten während eines Sicherungsvorgangs liest. Erstellen Sie dann Ihre Tabelle und fügen Sie die Daten während eines Wiederherstellungsvorgangs ein.

Etwas Klarheit bitte.

Wenn ich es wirklich bis zur SQL-Ebene selbst tun muss, mache ich mir Sorgen um die folgenden Themen:

  • Öffnen Sie Datenbanken und Transaktionen. Ich habe keine Ahnung, wie ich sie aus einer solchen Singleton-Klasse außerhalb des Workflows meiner App schließen kann.

  • So benachrichtigen Sie den Benutzer, dass eine Sicherung ausgeführt wird und die Datenbank gesperrt ist. Es kann lange dauern, daher muss ich möglicherweise einen Fortschrittsbalken anzeigen.

  • So machen Sie dasselbe bei der Wiederherstellung. Soweit ich weiß, kann die Wiederherstellung nur dann erfolgen, wenn der Benutzer die App bereits verwendet hat (und Daten in die Datenbank eingibt). Sie können also nicht davon ausgehen, dass nur die Backupped-Daten wiederhergestellt werden (Löschen der leeren oder alten Daten). Sie müssen sich irgendwie daran beteiligen, was für jede nicht triviale Datenbank aufgrund der IDs unmöglich ist.

  • So aktualisieren Sie die App nach der Wiederherstellung, ohne dass der Benutzer an einem - jetzt - nicht erreichbaren Punkt hängen bleibt.

  • Kann ich sicher sein, dass die Datenbank beim Sichern oder Wiederherstellen bereits aktualisiert wurde? Andernfalls stimmt das erwartete Schema möglicherweise nicht überein.

pjv
quelle
Ich habe das gleiche Problem ...
Whynot
Es gibt keine Möglichkeit, einfache Backup-Loch db?
Jin35
Ich muss einen Ordner mit FileBackupHelper sichern. Würde es mir der Ansatz zum Speichern der Datenbank ermöglichen, den Ordner zu speichern, unter dem sich viele Unterordner und Unterdateien befinden?
coolcool1994
Hast du das jemals richtig zum Laufen gebracht?
DeNitE Appz
Ja, siehe unten. Ich beantwortete auch meine eigene Frage.
pjv

Antworten:

21

Ein sauberer Ansatz wäre, eine benutzerdefinierte zu erstellen BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

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

und dann hinzufügen zu BackupAgentHelper:

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}
yanchenko
quelle
2
@logray: Dieser Code ist genau der gleiche wie in der Frage. Unnötige Bearbeitung.
Linuxios
@Linuxios Ich stimme zu, aber ich denke, es ist besser, dem Autor eine korrekte Zuordnung zu geben, als nur blind Code einzufügen ... wenn man bedenkt, dass das ursprüngliche OP / Post einen Link aus seiner App enthielt.
Logray
3
Wichtig : Die (Dokumentation) ( developer.android.com/guide/topics/data/backup.html#Files ) zum Sichern von Dateien besagt, dass der Vorgang nicht threadsicher ist. Sie sollten daher Ihre Datenbankmethoden und Ihre Sicherungsagenten synchronisieren
nicopico
In der @yanchenko-Dokumentation für FileBackupHelper heißt es: "Hinweis: Dies sollte nur mit kleinen Konfigurationsdateien verwendet werden, nicht mit großen Binärdateien." So würde eine Datenbank oft als große Binärdatei qualifiziert ...
IgorGanapolsky
Das funktioniert eigentlich nicht! (es sei denn, ich vermisse etwas ...). Das Problem ist, dass Sie den absoluten Pfad der Datenbank an FileBackupHelper übergeben, der FileBackupHelper dem Pfad jedoch den Dateiverzeichnispfad voranstellt, z. data / data / <Ihr Paket> / files, was zu einem falschen Pfad führt, z. B. data / data / <Ihr Paket> / files / data / data / <Ihr Paket> / database / <DB-Name>, wenn Sie nur das möchten DB absoluter Name und da die Superklasse dem Dateiverzeichnis vorangestellt ist, müssen Sie den Pfad relativ zum Dateiverzeichnis festlegen.
Bitrock
34

Nachdem ich meine Frage erneut geprüft hatte , konnte ich sie zum Laufen bringen, nachdem ich mir angesehen hatte, wie ConnectBot das macht . Danke Kenny und Jeffrey!

Es ist eigentlich so einfach wie das Hinzufügen:

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

zu deinem BackupAgentHelper.

Der Punkt, den ich vermisst habe, war die Tatsache, dass Sie einen relativen Pfad mit " ../databases/" verwenden müssten .

Dies ist jedoch keineswegs eine perfekte Lösung. Die zu FileBackupHelpererwähnenden Dokumente zum Beispiel: " FileBackupHelpersollten nur mit kleinen Konfigurationsdateien verwendet werden, nicht mit großen Binärdateien. " Letzteres ist bei SQLite-Datenbanken der Fall.

Ich würde gerne mehr Vorschläge, Einblicke in das bekommen, was von uns erwartet wird (was die richtige Lösung ist) und Ratschläge, wie dies brechen könnte.

pjv
quelle
1
Ich sollte wahrscheinlich hinzufügen, dass dies zwar ein bisschen inoffiziell ist, aber die ganze Zeit sehr gut funktioniert hat.
pjv
6
Hartcodierte Pfade sind böse.
Zeiger Null
@pjv, machst du also jede DB-Interaktion synchronisiert? Ich mache das Gleiche und versuche herauszufinden, ob ich db.open()durch db.close()oder nur insertAnweisungen synchronisieren muss oder was.
NSouth
22

Hier ist eine noch sauberere Möglichkeit, Datenbanken als Dateien zu sichern. Keine fest codierten Pfade.

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

Hinweis: getFilesDir wird überschrieben, sodass FileBackupHelper im Datenbankverzeichnis und nicht im Dateiverzeichnis funktioniert.

Ein weiterer Hinweis: Sie können auch databaseList verwenden , um alle Ihre DBs und Feed-Namen aus dieser Liste (ohne übergeordneten Pfad) in FileBackupHelper abzurufen. Dann würden alle DBs der App als Backup gespeichert.

Zeiger Null
quelle
Die Kombination mit der von @pjv angegebenen Lösung funktioniert perfekt! Es ist vielleicht nicht genau das, was die Spezifikationen im Sinn hatten ... aber es funktioniert ganz gut!
Tom
1
Das nützt nicht viel, wenn Sie Datenbanken und normale Dateien zum Sichern haben.
Dan Hulme
1
@ Dan: Verwenden Sie in diesem Fall mehrere Agenten.
pjv
@Pointer Null Könnten Sie etwas mehr über getFilesDir () erfahren, warum wird es wirklich benötigt und warum? Wie funktioniert es?
Pulver366
7

Die Verwendung FileBackupHelperzum Sichern / Wiederherstellen von sqlite db wirft einige schwerwiegende Fragen auf:
1. Was passiert, wenn die App den Cursor verwendet, der von abgerufen wurde, ContentProvider.query()und der Sicherungsagent versucht, die gesamte Datei zu überschreiben?
2. Der Link ist ein schönes Beispiel für perfekte Tests (niedrige Entrophie;). Sie deinstallieren die App, installieren sie erneut und die Sicherung wird wiederhergestellt. Das Leben kann jedoch brutal sein. Schauen Sie sich den Link an . Stellen wir uns ein Szenario vor, in dem ein Benutzer ein neues Gerät kauft. Da es keinen eigenen Satz hat, verwendet der Sicherungsagent den Satz eines anderen Geräts. Die App ist installiert und Ihr backupHelper ruft alte Dateien mit einem Datenbankversionsschema ab, das niedriger als das aktuelle ist. SQLiteOpenHelperAufrufe onDowngrademit der Standardimplementierung:

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

Unabhängig davon, was der Benutzer tut, kann er Ihre App auf dem neuen Gerät nicht verwenden.

Ich würde vorschlagen ContentResolver, Daten abzurufen -> serialisieren (ohne _ids) zum Sichern und Deserialisieren -> Daten zum Wiederherstellen einfügen.

Hinweis: Das Abrufen / Einfügen von Daten erfolgt über ContentResolver, wodurch Probleme mit der Cuncurrency vermieden werden. Die Serialisierung erfolgt in Ihrem backupAgent. Wenn Sie Ihre eigene Cursor-<-> Objektzuordnung ausführen, kann die Serialisierung eines Elements so einfach sein wie die Implementierung Serializablemit dem transientFeld _id in der Klasse, die Ihre Entität darstellt.

Ich würde auch Masseneinsatz dh verwende ContentProviderOperation Beispiel und CursorLoader.setUpdateThrottleso , dass der App mit dem Neustart Loader nicht klemmt Prozess auf Datenänderung während der Sicherung wiederherstellen.

Wenn Sie sich in einem Downgrade befinden, können Sie entweder die Wiederherstellungsdaten abbrechen oder ContentResolver mit Feldern wiederherstellen und aktualisieren, die für die herabgestufte Version relevant sind.

Ich bin damit einverstanden, dass das Thema nicht einfach ist, in Dokumenten nicht gut erklärt wird und einige Fragen wie die Größe von Massendaten usw. weiterhin bestehen.

Hoffe das hilft.

Piotr Bazan
quelle
Sie wiederholen einige gültige Fragen. Aber ich sehe nicht, wie die Serialisierung der Datenbank das Problem löst. Es hilft nicht bei Parallelitätsproblemen. Und wenn Sie kein Skript zum Downgrade der Datenbank schreiben können, können Sie kein Skript zum Deserialisieren alter Daten schreiben.
pjv
@pjv Wenn Sie einen ContentResolver verwenden, werden Parallelitätsprobleme für Sie gelöst.
IgorGanapolsky
@IgorGanapolsky Ja, ich denke das ist wahr. Wenn die Serialisierung jedoch langsam genug ist und der Benutzer die App bereits verwendet und Daten eingibt, kann dies zu Konflikten mit den Daten führen, die Sie wiederherstellen. Können Sie dies atomar tun, bevor der Benutzer Zugriff auf die App erhält?
pjv
@pjv Sie können einen ContentObserver registrieren, um die Daten zu synchronisieren, die Sie beobachten, um benachrichtigt zu werden. Daher müssen Sie sich keine Sorgen um Konflikte machen.
IgorGanapolsky
Ihr Standpunkt zu DB-Versionen ist gut - ich denke, wenn Sie Ihre DB-Version speichern (z. B. in gemeinsam genutzten Einstellungen, vorausgesetzt, Sie sichern sie auch), sollten Sie beim Wiederherstellen die vorhandene Logik verwenden können, um die DB auf ihre zu aktualisieren aktuelle Version in der onDowngrade () -Methode. Wenn Sie noch keinen Upgrade-Code haben, müssen Sie sowieso keine Daten speichern!
Ben Neill
3

Ab Android M steht Apps nun eine vollständige Datensicherungs- / Wiederherstellungs-API zur Verfügung. Diese neue API enthält eine XML-basierte Spezifikation im App-Manifest, mit der der Entwickler beschreiben kann, welche Dateien direkt semantisch gesichert werden sollen: 'Sichern der Datenbank mit dem Namen "mydata.db"'. Diese neue API ist für Entwickler viel einfacher zu verwenden - Sie müssen keine Unterschiede nachverfolgen oder explizit einen Sicherungsdurchlauf anfordern, und die XML-Beschreibung der zu sichernden Dateien bedeutet, dass Sie häufig keinen Code schreiben müssen überhaupt.

(Sie können sich sogar an einer vollständigen Datensicherung / -wiederherstellung beteiligen, um beispielsweise beim Wiederherstellen einen Rückruf zu erhalten. Auf diese Weise ist dies flexibel.)

Finden Sie in der Konfiguration der automatischen Sicherung für Apps Abschnitt auf developer.android.com für eine Beschreibung, wie die neue API zu verwenden.

ctate
quelle
Dies gilt nur für Android 6.0 (API 23). Wenn der Benutzer eine ältere Version hat, muss die Sicherung des Schlüsselwerts noch implementiert werden.
Live-Liebe
0

Eine Möglichkeit besteht darin, es in der Anwendungslogik über der Datenbank zu erstellen. Es schreit tatsächlich nach einem solchen Level, denke ich. Ich bin mir nicht sicher, ob Sie dies bereits tun, aber die meisten Leute (trotz des Cursor-Ansatzes von Android Content Manager) werden eine ORM-Zuordnung einführen - entweder eine benutzerdefinierte oder eine orm-lite-Methode. Und was ich in diesem Fall lieber tun würde, ist:

  1. um sicherzustellen, dass Ihre Anwendung einwandfrei funktioniert, wenn die App / Daten im Hintergrund hinzugefügt werden und neue Daten hinzugefügt / entfernt werden, während die Anwendung bereits gestartet wurde
  2. um Java-> Protobuf oder sogar einfach Java-Serialisierungszuordnungen zu erstellen und einen eigenen BackupHelper zu schreiben, um die Daten aus dem Stream zu lesen und sie einfach zur Datenbank hinzuzufügen ....

In diesem Fall also nicht auf DB-Ebene, sondern auf Anwendungsebene.

Jarek Potiuk
quelle
Das Problem besteht nicht darin, wie Daten aus der Datenbank in den BackupHelperAgent abgerufen werden oder umgekehrt. Bitte lesen Sie die 5 Aufzählungszeichen am Ende meiner Frage.
pjv
@JarekPotiuk Du hast gesagt: "Die meisten Leute (trotz Android Content Manager Cursor Ansatz)". Aber was lässt Sie denken, dass die meisten Leute es vermeiden würden, einen ContentProvider und ContentResolver für den Datenbankzugriff zu verwenden?
IgorGanapolsky