Backup / ripristino Android: come eseguire il backup di un database interno?


86

Ho implementato un BackupAgentHelperutilizzo del fornito FileBackupHelperper eseguire il backup e il ripristino del database nativo che ho. Questo è il database che di solito usi insieme ContentProviderse in cui risiede /data/data/yourpackage/databases/.

Si potrebbe pensare che questo sia un caso comune. Tuttavia, i documenti non sono chiari su cosa fare: http://developer.android.com/guide/topics/data/backup.html . Non esiste uno BackupHelperspecifico per questi database tipici. Quindi ho usato ilFileBackupHelper , l'ho indirizzato al mio file .db in " /databases/", ho introdotto blocchi su qualsiasi operazione db (come db.insert) nel mio ContentProviders, e ho anche provato a creare la /databases/directory " " prima onRestore()perché non esiste dopo l'installazione.

Ho implementato una soluzione simile per il SharedPreferencessuccesso in un'app diversa in passato. Tuttavia, quando provo la mia nuova implementazione nell'emulatore-2.2, vedo che viene eseguito un backup LocalTransportdai log, così come viene eseguito (e onRestore()chiamato) un ripristino . Tuttavia, il file db stesso non viene mai creato.

Tieni presente che tutto ciò avviene dopo un'installazione e prima del primo avvio dell'app, dopo che è stato eseguito il ripristino. A parte questo, la mia strategia di test era basata su http://developer.android.com/guide/topics/data/backup.html#Testing .

Si noti inoltre che non sto parlando di alcuni database sqlite che gestisco da solo, né di eseguire il backup su SDcard, sul proprio server o altrove.

Ho visto una menzione nei documenti sui database che consigliano di utilizzare un custom BackupAgentma non sembra correlato:

Tuttavia, potresti voler estendere BackupAgent direttamente se hai bisogno di: * Eseguire il backup dei dati in un database. Se si dispone di un database SQLite che si desidera ripristinare quando l'utente reinstalla l'applicazione, è necessario creare un BackupAgent personalizzato che legga i dati appropriati durante un'operazione di backup, quindi creare la tabella e inserire i dati durante un'operazione di ripristino.

Un po 'di chiarezza per favore.

Se ho davvero bisogno di farlo da solo fino al livello SQL, allora sono preoccupato per i seguenti argomenti:

  • Database aperti e transazioni. Non ho idea di come chiuderli da una classe così singleton al di fuori del flusso di lavoro della mia app.

  • Come notificare all'utente che è in corso un backup e il database è bloccato. Potrebbe volerci molto tempo, quindi potrei aver bisogno di mostrare una barra di avanzamento.

  • Come fare lo stesso durante il ripristino. Da quanto ho capito, il ripristino potrebbe avvenire proprio quando l'utente ha già iniziato a utilizzare l'app (e a inserire dati nel database). Quindi non puoi presumere di ripristinare solo i dati di backup in posizione (eliminando i dati vuoti o vecchi). Dovrai in qualche modo unirti a esso, il che per qualsiasi database non banale è impossibile a causa degli ID.

  • Come aggiornare l'app dopo il ripristino senza che l'utente si blocchi in un punto - ora - irraggiungibile.

  • Posso essere sicuro che il database sia già stato aggiornato durante il backup o il ripristino? In caso contrario, lo schema previsto potrebbe non corrispondere.


Ho lo stesso problema ...

Non c'è modo di semplice backup buco db?
Jin35

Ho bisogno di eseguire il backup di una cartella utilizzando FileBackupHelper. L'utilizzo dell'approccio di salvataggio del database mi consentirebbe di salvare quella cartella che ha molte sottocartelle e sotto file sotto di essa?
coolcool1994

Hai mai fatto funzionare correttamente?
DeNitE Appz

Sì, vedi sotto. Ho risposto anche alla mia domanda.
pjv

Risposte:


21

Un approccio più pulito sarebbe creare un custom BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

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

e poi aggiungilo a BackupAgentHelper:

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

2
@logray: quel codice è lo stesso della domanda. Modifica non necessaria.
Linuxios

@Linuxios Sono d'accordo, tuttavia penso che sia meglio fornire una corretta attribuzione all'autore, piuttosto che incollare ciecamente il codice ... considerando che l'OP / post originale includeva un collegamento dalla sua app.
logray

3
Importante : la (documentazione) ( developer.android.com/guide/topics/data/backup.html#Files ) per il backup dei file afferma che l'operazione non è sicura per i thread , quindi è necessario sincronizzare i metodi del database e gli agenti di backup
nicopico

@yanchenko Documentation for FileBackupHelper afferma: "Nota: deve essere utilizzato solo con file di configurazione piccoli, non con file binari di grandi dimensioni." Quindi un database si qualifica spesso come un file binario di grandi dimensioni ...
IgorGanapolsky

Questo in realtà non funziona! (a meno che non mi manchi qualcosa ...). Il problema è che si passa il percorso assoluto del db a FileBackupHelper ma FileBackupHelper antepone al percorso il percorso della directory dei file, ad es. data / data / <il tuo pacchetto> / files che risulta in un percorso errato qualcosa come data / data / <il tuo pacchetto> / files / data / data / <il tuo pacchetto> / databases / <DB Name> quando tutto ciò che vuoi è il DB nome assoluto e poiché la super classe antepone la directory dei file è necessario rendere il percorso relativo alla directory dei file.
bitrock

34

Dopo aver rivisto la mia domanda, sono riuscito a farlo funzionare dopo aver visto come lo fa ConnectBot . Grazie Kenny e Jeffrey!

In realtà è facile come aggiungere:

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

al tuo BackupAgentHelper.

Il punto che mi mancava era il fatto che avresti dovuto usare un percorso relativo con " ../databases/".

Tuttavia, questa non è affatto una soluzione perfetta. La documentazione per FileBackupHelperesempio: " FileBackupHelperdovrebbe essere usata solo con piccoli file di configurazione, non con file binari di grandi dimensioni. ", Quest'ultimo è il caso dei database SQLite.

Vorrei ricevere ulteriori suggerimenti, approfondimenti su ciò che ci si aspetta da noi (qual è la soluzione corretta) e consigli su come questo potrebbe non funzionare.


1
Probabilmente dovrei aggiungere che, anche se questo è un po 'non ufficiale, ha funzionato / ha funzionato molto bene per tutto questo tempo.
pjv

6
I percorsi hard-coding sono malvagi.
Pointer Null

@pjv, quindi fai sincronizzare ogni interazione db? Sto facendo la stessa cosa e sto cercando di capire se devo sincronizzarmi db.open()tramite db.close()o solo insertdichiarazioni o cosa.
NSouth

22

Ecco ancora un modo più pulito per eseguire il backup dei database come file. Nessun percorso hardcoded.

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

Nota: sovrascrive getFilesDir in modo che FileBackupHelper funzioni nella directory dei database, non nella directory dei file.

Un altro suggerimento: puoi anche usare databaseList per ottenere tutti i tuoi DB e nomi di feed da questo elenco (senza percorso genitore) in FileBackupHelper. Quindi tutti i DB dell'app verranno salvati nel backup.


La combinazione di questo con la soluzione data da @pjv funziona perfettamente! Potrebbe non essere esattamente quello che le specifiche avevano in mente ... ma funziona abbastanza bene!
Tom

1
Non è molto utile se si dispone di database e file normali di cui eseguire il backup.
Dan Hulme

1
@ Dan: usa più agenti in quel caso.
pjv

@Pointer Null Potresti elaborare un po 'di più getFilesDir (), perché è davvero necessario e perché? Come funziona?
polvere 366

7

L'utilizzo del FileBackupHelperbackup / ripristino di sqlite db solleva alcune serie domande:
1. Cosa succede se l'app utilizza il cursore recuperato da ContentProvider.query()e l'agente di backup tenta di sovrascrivere l'intero file?
2. Il collegamento è un bell'esempio di test perfetto (entrophy basso;). Disinstalli l'app, la installi di nuovo e il backup viene ripristinato. Tuttavia la vita può essere brutale. Dai un'occhiata al link . Immaginiamo lo scenario in cui un utente acquista un nuovo dispositivo. Poiché non dispone di un proprio set, l'agente di backup utilizza il set di un altro dispositivo. L'app è installata e il tuo backupHelper recupera il vecchio file con schema di versione db inferiore a quello attuale. SQLiteOpenHelperchiama onDowngradecon l'implementazione predefinita:

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

Indipendentemente da ciò che fa l'utente, non può utilizzare la tua app sul nuovo dispositivo.

Suggerirei di utilizzare ContentResolverper ottenere i dati -> serializzare (senza _ids) per il backup e deserializzare -> inserire i dati per il ripristino.

Nota: il recupero / inserimento dei dati viene eseguito tramite ContentResolver evitando così problemi di valuta. La serializzazione viene eseguita nel backupAgent. Se fai il tuo cursore <-> mappatura degli oggetti, serializzare un elemento può essere semplice come implementarlo Serializablecon il transientcampo _id sulla classe che rappresenta la tua entità.

Userei anche l'inserimento di massa, ad esempio l' ContentProviderOperation esempio e in CursorLoader.setUpdateThrottlemodo che l'app non si blocchi con il riavvio del caricatore sulla modifica dei dati durante il processo di ripristino del backup.

Se ti trovi in ​​una situazione di downgrade, puoi scegliere di interrompere il ripristino dei dati o ripristinare e aggiornare ContentResolver con i campi relativi alla versione downgrade.

Sono d'accordo che l'argomento non è facile, non è ben spiegato nei documenti e alcune domande rimangono ancora come la dimensione dei dati di massa ecc.

Spero che sia di aiuto.


Ripeti alcune domande valide. Ma non vedo come la serializzazione del database risolva il problema. Non aiuta con problemi di concorrenza. E se non puoi scrivere uno script per eseguire il downgrade del database, non puoi scrivere uno script per deserializzare i vecchi dati.
pjv

@pjv Se usi ContentResolver, risolve i problemi di concorrenza per te.
IgorGanapolsky

@IgorGanapolsky Sì, penso che sia vero. Tuttavia, se la serializzazione è sufficientemente lenta e l'utente sta già utilizzando l'app e inserendo i dati, potrebbe essere in conflitto con i dati che si stanno ripristinando. Puoi farlo atomicamente e prima che l'utente abbia accesso all'app?
pjv

@pjv È possibile registrare un ContentObserver per sincronizzare i dati che si stanno osservando per ricevere una notifica. Quindi non devi preoccuparti dei conflitti.
IgorGanapolsky

Il tuo punto sulle versioni del DB è positivo: immagino che se salvi la tua versione del DB (ad esempio nelle preferenze condivise, supponendo che tu ne esegua anche il backup), quando stai ripristinando dovresti essere in grado di utilizzare la logica esistente per aggiornare il DB al suo versione corrente nel metodo onDowngrade (). Se non hai già il codice di aggiornamento, non ti preoccupi comunque di conservare i dati!
Ben Neill

3

A partire da Android M, ora è disponibile un'API di backup / ripristino completo dei dati per le app. Questa nuova API include una specifica basata su XML nel manifesto dell'app che consente allo sviluppatore di descrivere i file di cui eseguire il backup in modo semantico diretto: 'eseguire il backup del database chiamato "mydata.db"'. Questa nuova API è molto più facile da usare per gli sviluppatori: non è necessario tenere traccia delle differenze o richiedere esplicitamente un passaggio di backup e la descrizione XML dei file di cui eseguire il backup significa che spesso non è necessario scrivere alcun codice affatto.

possibile essere coinvolti anche in un backup completo dei dati / operazione di ripristino per ottenere un callback quando il ripristino avviene, per esempio. È flessibile in questo modo.)

Vedere la sezione Configurazione del backup automatico per le app su developer.android.com per una descrizione di come utilizzare la nuova API.


Questo è solo per Android 6.0 (API 23), se l'utente ha una versione precedente deve ancora implementare il backup del valore chiave.
live-love

0

Un'opzione sarà crearlo nella logica dell'applicazione sopra il database. In realtà urla per un tale livello, credo. Non sono sicuro se lo stai già facendo, ma la maggior parte delle persone (nonostante l'approccio del cursore del gestore dei contenuti di Android) introdurrà alcune mappature ORM, personalizzate o un approccio orm-lite. E quello che preferirei fare in questo caso è:

  1. per assicurarti che la tua applicazione funzioni correttamente quando l'app / i dati vengono aggiunti in background con nuovi dati aggiunti / rimossi mentre l'applicazione è già avviata
  2. per creare un po 'di Java-> protobuf o anche semplicemente una mappatura della serializzazione java e scrivere il proprio BackupHelper per leggere i dati dal flusso e aggiungerlo semplicemente al database ....

Quindi in questo caso, invece di farlo a livello di database, fallo a livello di applicazione.


Il problema non è come ottenere i dati dal database in BackupHelperAgent o viceversa. Si prega di leggere i 5 punti alla fine della mia domanda.
pjv

@JarekPotiuk Hai detto: "la maggior parte delle persone (nonostante l'approccio del cursore del gestore dei contenuti Android)". Ma cosa ti fa pensare che la maggior parte delle persone eviterebbe di utilizzare ContentProvider e ContentResolver per l'accesso al database?
IgorGanapolsky
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.