Sauvegarde / restauration Android: comment sauvegarder une base de données interne?

86

J'ai implémenté un en BackupAgentHelperutilisant le fourni FileBackupHelperpour sauvegarder et restaurer la base de données native que j'ai. Il s'agit de la base de données que vous utilisez généralement avec ContentProviderset qui réside dans /data/data/yourpackage/databases/.

On pourrait penser que c'est un cas courant. Cependant, la documentation n'est pas claire sur ce qu'il faut faire: http://developer.android.com/guide/topics/data/backup.html . Il n'y a pas BackupHelperspécifiquement pour ces bases de données typiques. Par conséquent, j'ai utilisé leFileBackupHelper , je l'ai pointé vers mon fichier .db dans " /databases/", j'ai introduit des verrous autour de toute opération de base de données (comme db.insert) dans my ContentProviders, et j'ai même essayé de créer le /databases/répertoire " " avant onRestore()car il n'existe pas après l'installation.

J'ai implémenté une solution similaire pour le SharedPreferencessuccès dans une application différente dans le passé. Cependant, lorsque je teste ma nouvelle implémentation dans l'émulateur-2.2, je vois une sauvegarde effectuée à LocalTransportpartir des journaux, ainsi qu'une restauration en cours (et onRestore()appelée). Pourtant, le fichier db lui-même n'est jamais créé.

Notez que tout cela se produit après une installation et avant le premier lancement de l'application, une fois la restauration effectuée. En dehors de cela, ma stratégie de test était basée sur http://developer.android.com/guide/topics/data/backup.html#Testing .

Veuillez noter également que je ne parle pas d'une base de données sqlite que je gère moi-même, ni d'une sauvegarde sur SDcard, sur mon propre serveur ou ailleurs.

J'ai vu une mention dans la documentation sur les bases de données conseillant d'utiliser une personnalisation BackupAgentmais cela ne semble pas lié:

Cependant, vous souhaiterez peut-être étendre BackupAgent directement si vous avez besoin de: * Sauvegarder les données dans une base de données. Si vous disposez d'une base de données SQLite que vous souhaitez restaurer lorsque l'utilisateur réinstalle votre application, vous devez créer un BackupAgent personnalisé qui lit les données appropriées lors d'une opération de sauvegarde, puis créez votre table et insérez les données lors d'une opération de restauration.

Un peu de clarté s'il vous plaît.

Si j'ai vraiment besoin de le faire moi-même jusqu'au niveau SQL, je m'inquiète pour les sujets suivants:

  • Ouvrez les bases de données et les transactions. Je ne sais pas comment les fermer à partir d'une telle classe singleton en dehors du flux de travail de mon application.

  • Comment informer l'utilisateur qu'une sauvegarde est en cours et que la base de données est verrouillée. Cela peut prendre un certain temps, donc je devrai peut-être afficher une barre de progression.

  • Comment faire de même lors de la restauration. Si je comprends bien, la restauration peut se produire juste lorsque l'utilisateur a déjà commencé à utiliser l'application (et à saisir des données dans la base de données). Vous ne pouvez donc pas prétendre simplement restaurer les données sauvegardées sur place (en supprimant les données vides ou anciennes). Vous devrez en quelque sorte le rejoindre, ce qui pour toute base de données non triviale est impossible en raison des identifiants.

  • Comment actualiser l'application une fois la restauration terminée sans que l'utilisateur soit bloqué à un point - maintenant - inaccessible.

  • Puis-je être sûr que la base de données a déjà été mise à niveau lors d'une sauvegarde ou d'une restauration? Sinon, le schéma attendu pourrait ne pas correspondre.

pjv
la source
J'ai le même problème ...
Il n'y a aucun moyen de simple trou de sauvegarde db?
Jin35
J'ai besoin de sauvegarder un dossier à l'aide de FileBackupHelper. Est-ce que l'utilisation de l'approche de sauvegarde de la base de données me permettrait de sauvegarder ce dossier qui contient beaucoup de sous-dossiers et de sous-fichiers en dessous?
coolcool1994
Avez-vous déjà fait fonctionner cela correctement?
DeNitE Appz
Oui, voir ci-dessous. J'ai également répondu à ma propre question.
pjv

Réponses:

21

Une approche plus propre consisterait à créer une personnalisation BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

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

puis ajoutez-le à BackupAgentHelper:

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}
Yanchenko
la source
2
@logray: Ce code est exactement le même que dans la question. Modification inutile.
Linuxios
@Linuxios Je suis d'accord, mais je pense qu'il est préférable de fournir une attribution appropriée à l'auteur, plutôt que de simplement coller du code à l'aveugle ... étant donné que l'OP / l'article d'origine comprenait un lien depuis son application.
logray
3
Important : La (documentation) ( developer.android.com/guide/topics/data/backup.html#Files ) pour la sauvegarde des fichiers indique que l'opération n'est pas threadsafe , vous devez donc synchroniser vos méthodes de base de données et vos agents de sauvegarde
nicopico
La documentation @yanchenko pour FileBackupHelper déclare: "Remarque: ceci ne doit être utilisé qu'avec de petits fichiers de configuration, pas de gros fichiers binaires." Ainsi, une base de données serait souvent qualifiée de gros fichier binaire ...
IgorGanapolsky
Cela ne fonctionne pas vraiment! (sauf si je manque quelque chose ...). Le problème est que vous transmettez le chemin absolu de la base de données au FileBackupHelper mais que FileBackupHelper ajoute le chemin d'accès au répertoire des fichiers, par exemple. data / data / <votre package> / files qui aboutit à un chemin incorrect quelque chose comme data / data / <votre package> / files / data / data / <votre package> / databases / <DB Name> quand tout ce que vous voulez est le Nom absolu de la base de données et puisque la super classe ajoute le répertoire des fichiers au début, vous devez rendre le chemin relatif au répertoire des fichiers.
bitrock
34

Après avoir revisité ma question, j'ai pu la faire fonctionner après avoir examiné comment ConnectBot le fait . Merci Kenny et Jeffrey!

C'est en fait aussi simple que d'ajouter:

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

à votre BackupAgentHelper.

Le point qui me manquait était le fait que vous deviez utiliser un chemin relatif avec " ../databases/".

Pourtant, ce n'est en aucun cas une solution parfaite. La documentation FileBackupHelpermentionne par exemple: " FileBackupHelperne doit être utilisé qu'avec de petits fichiers de configuration, pas de gros fichiers binaires. ", Ce dernier étant le cas avec les bases de données SQLite.

J'aimerais avoir plus de suggestions, des informations sur ce que l'on attend de nous (quelle est la bonne solution) et des conseils sur la façon dont cela pourrait casser.

pjv
la source
1
Je devrais probablement ajouter que même si c'est un peu officieux, cela fonctionne très bien tout ce temps.
pjv
6
Les chemins codés en dur sont mauvais.
Pointer Null
@pjv, alors faites-vous synchroniser chaque interaction avec la base de données? Je fais la même chose et j'essaie de déterminer si j'ai besoin de synchroniser à db.open()travers db.close()ou juste des insertdéclarations ou quoi.
NSouth
22

Voici un moyen encore plus propre de sauvegarder les bases de données sous forme de fichiers. Pas de chemins codés en dur.

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

Remarque: il remplace getFilesDir pour que FileBackupHelper fonctionne dans le répertoire des bases de données, pas dans le répertoire des fichiers.

Un autre conseil: vous pouvez également utiliser databaseList pour obtenir tous vos noms de base de données et de flux de cette liste (sans chemin parent) dans FileBackupHelper. Ensuite, toutes les bases de données de l'application seraient enregistrées dans la sauvegarde.

Pointeur nul
la source
Combiner cela avec la solution donnée par @pjv fonctionne parfaitement! Ce n'est peut-être pas exactement ce que les spécifications avaient en tête ... mais cela fonctionne plutôt bien!
Tom
1
Ce n'est pas très utile si vous avez des bases de données et des fichiers normaux à sauvegarder.
Dan Hulme
1
@Dan: utilisez plusieurs agents dans ce cas.
pjv
@Pointer Null Pourriez-vous élaborer un peu plus un getFilesDir (), pourquoi est-il vraiment nécessaire et pourquoi? Comment ça marche?
poudre366
7

Utiliser FileBackupHelperpour sauvegarder / restaurer sqlite db soulève de sérieuses questions:
1. Que se passe-t-il si l'application utilise le curseur récupéré ContentProvider.query()et que l'agent de sauvegarde tente de remplacer le fichier entier?
2. Le lien est un bel exemple de test parfait (faible entrophie;). Vous désinstallez l'application, réinstallez-la et la sauvegarde est restaurée. Cependant, la vie peut être brutale. Jetez un œil au lien . Imaginons un scénario lorsqu'un utilisateur achète un nouvel appareil. Puisqu'il n'a pas son propre ensemble, l'agent de sauvegarde utilise l'ensemble d'un autre périphérique. L'application est installée et votre backupHelper récupère l'ancien fichier avec un schéma de version de base de données inférieur à l'actuel. SQLiteOpenHelperappels onDowngradeavec l'implémentation par défaut:

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

Peu importe ce que fait l'utilisateur, il ne peut pas utiliser votre application sur le nouvel appareil.

Je suggérerais d'utiliser ContentResolverpour obtenir des données -> sérialiser (sans _ids) pour la sauvegarde et désérialiser -> insérer des données pour la restauration.

Remarque: obtenir / insérer des données se fait via ContentResolver, évitant ainsi les problèmes de devises. La sérialisation est effectuée dans votre backupAgent. Si vous faites votre propre mappage d'objet curseur <->, la sérialisation d'un élément peut être aussi simple que l'implémentation Serializableavec le transientchamp _id sur la classe représentant votre entité.

J'utiliserais également un ContentProviderOperation exemple d' insertion en masse, par exemple, et CursorLoader.setUpdateThrottlepour que l'application ne soit pas bloquée avec le redémarrage du chargeur lors du changement de données pendant le processus de restauration de sauvegarde.

Si vous vous trouvez dans une situation de rétrogradation, vous pouvez choisir d'interrompre la restauration des données ou de restaurer et de mettre à jour ContentResolver avec des champs correspondant à la version rétrogradée.

Je conviens que le sujet n'est pas facile, pas bien expliqué dans la documentation et que certaines questions subsistent comme la taille des données en vrac, etc.

J'espère que cela t'aides.

Piotr Bazan
la source
Vous répétez quelques questions valables. Mais je ne vois pas comment la sérialisation de la base de données résout le problème. Cela n'aide pas avec les problèmes de concurrence. Et si vous ne pouvez pas écrire un script pour rétrograder la base de données, vous ne pouvez pas écrire un script pour désérialiser les anciennes données.
pjv
@pjv Si vous utilisez un ContentResolver, il résout les problèmes de concurrence pour vous.
IgorGanapolsky
@IgorGanapolsky Oui, je pense que c'est vrai. Cependant, si la sérialisation est suffisamment lente et que l'utilisateur utilise déjà l'application et saisit des données, cela peut entrer en conflit avec les données que vous restaurez. Pouvez-vous le faire de manière atomique et avant que l'utilisateur n'ait accès à l'application?
pjv
@pjv Vous pouvez enregistrer un ContentObserver pour synchroniser les données que vous observez pour être notifiées. Par conséquent, vous n'avez pas à vous soucier des conflits.
IgorGanapolsky
Votre point sur les versions de base de données est bon - je suppose que si vous enregistrez votre version de base de données (dans les préférences partagées par exemple, en supposant que vous la sauvegardiez également), lorsque vous restaurez, vous devriez pouvoir utiliser la logique existante pour mettre à niveau la version actuelle dans la méthode onDowngrade (). Si vous n'avez pas encore de code de mise à niveau, vous n'êtes de toute façon pas dérangé par la conservation des données!
Ben Neill
3

À partir d'Android M, une API de sauvegarde / restauration complète des données est désormais disponible pour les applications. Cette nouvelle API inclut une spécification basée sur XML dans le manifeste de l'application qui permet au développeur de décrire les fichiers à sauvegarder de manière sémantique directe: «sauvegarder la base de données appelée« mydata.db »». Cette nouvelle API est beaucoup plus facile à utiliser pour les développeurs - vous n'avez pas besoin de suivre les différences ou de demander une passe de sauvegarde explicitement, et la description XML des fichiers à sauvegarder signifie que vous n'avez souvent pas besoin d'écrire de code du tout.

(Vous pouvez vous impliquer même dans une opération de sauvegarde / restauration de données complètes pour obtenir un rappel lorsque la restauration a lieu, par exemple. C'est flexible de cette façon.)

Consultez la section Configuration de la sauvegarde automatique pour les applications sur developer.android.com pour obtenir une description de l'utilisation de la nouvelle API.

ctate
la source
Ce n'est que pour Android 6.0 (API 23), si l'utilisateur a une version plus ancienne, il doit encore implémenter la sauvegarde de la valeur de clé.
live-love
0

Une option sera de le construire dans la logique d'application au-dessus de la base de données. Il crie en fait pour un tel niveau, je pense. Je ne sais pas si vous le faites déjà, mais la plupart des gens (malgré l'approche du curseur du gestionnaire de contenu Android) introduiront un mappage ORM - soit une approche personnalisée, soit une approche orm-lite. Et ce que je préférerais faire dans ce cas est:

  1. pour vous assurer que votre application fonctionne correctement lorsque l'application / les données sont ajoutées en arrière-plan avec de nouvelles données ajoutées / supprimées alors que l'application est déjà lancée
  2. pour créer un mappage de sérialisation Java-> protobuf ou même simplement java et écrire votre propre BackupHelper pour lire les données du flux et l'ajouter simplement à la base de données ....

Donc, dans ce cas, plutôt que de le faire au niveau de la base de données, faites-le au niveau de l'application.

Jarek Potiuk
la source
Le problème n'est pas de savoir comment obtenir des données de la base de données dans BackupHelperAgent ou vice versa. Veuillez lire les 5 points à la fin de ma question.
pjv
@JarekPotiuk Vous avez dit: "la plupart des gens (malgré l'approche du curseur du gestionnaire de contenu Android)". Mais qu'est-ce qui vous fait penser que la plupart des gens éviteraient d'utiliser un ContentProvider et ContentResolver pour accéder à la base de données?
IgorGanapolsky