Kopia zapasowa / przywracanie systemu Android: jak wykonać kopię zapasową wewnętrznej bazy danych?

86

Zaimplementowałem BackupAgentHelperużycie dostarczonego FileBackupHelperdo tworzenia kopii zapasowych i przywracania natywnej bazy danych, którą mam. Jest to baza danych, z której zwykle korzystasz ContentProvidersi która znajduje się w /data/data/yourpackage/databases/.

Można by pomyśleć, że to powszechny przypadek. Jednak dokumentacja nie jest jasna, co należy zrobić: http://developer.android.com/guide/topics/data/backup.html . Nie ma BackupHelperspecjalnie dla tych typowych baz danych. Dlatego użyłemFileBackupHelper , wskazałem na mój plik .db w " /databases/", wprowadziłem blokady wokół wszelkich operacji db (takich jak db.insert) w moim ContentProviders, a nawet próbowałem utworzyć /databases/katalog " " wcześniej, onRestore()ponieważ nie istnieje po instalacji.

W przeszłości wdrożyłem podobne rozwiązanie dla SharedPreferencespomyślnie w innej aplikacji. Jednak kiedy testuję moją nową implementację w emulatorze-2.2, widzę tworzenie kopii zapasowej LocalTransportz dzienników, a także wykonywanie (i onRestore()wywoływanie) przywracania . Jednak sam plik db nigdy nie jest tworzony.

Zwróć uwagę, że dzieje się to po instalacji, a przed pierwszym uruchomieniem aplikacji, po wykonaniu przywracania. Poza tym moja strategia testowa opierała się na http://developer.android.com/guide/topics/data/backup.html#Testing .

Proszę również zauważyć, że nie mówię o jakiejś bazie danych sqlite, którą sam zarządzam, ani o tworzeniu kopii zapasowych na karcie SD, na własnym serwerze lub w innym miejscu.

Widziałem wzmiankę w dokumentach o bazach danych zalecających używanie niestandardowego BackupAgent ale nie wydaje się to być powiązane:

Jednak możesz bezpośrednio rozszerzyć BackupAgent, jeśli potrzebujesz: * Utworzyć kopię zapasową danych w bazie danych. Jeśli masz bazę danych SQLite, którą chcesz przywrócić, gdy użytkownik ponownie zainstaluje Twoją aplikację, musisz utworzyć niestandardowy BackupAgent, który odczytuje odpowiednie dane podczas operacji tworzenia kopii zapasowej, a następnie utwórz tabelę i wstaw dane podczas operacji przywracania.

Proszę o wyjaśnienie.

Jeśli naprawdę muszę to zrobić samodzielnie do poziomu SQL, martwię się następującymi tematami:

  • Otwarte bazy danych i transakcje. Nie mam pojęcia, jak zamknąć je z takiej pojedynczej klasy poza przepływem pracy mojej aplikacji.

  • Jak powiadomić użytkownika, że ​​trwa tworzenie kopii zapasowej, a baza danych jest zablokowana. Może to zająć dużo czasu, więc może być konieczne wyświetlenie paska postępu.

  • Jak zrobić to samo przy przywracaniu. Jak rozumiem, przywrócenie może nastąpić właśnie wtedy, gdy użytkownik zaczął już korzystać z aplikacji (i wprowadzać dane do bazy danych). Nie możesz więc zakładać, że po prostu przywrócisz kopię zapasową danych w miejscu (usuwając puste lub stare dane). Będziesz musiał jakoś się do niego przyłączyć, co dla każdej nietrywialnej bazy danych jest niemożliwe ze względu na identyfikator.

  • Jak odświeżyć aplikację po zakończeniu przywracania bez utknięcia użytkownika w jakimś - teraz - nieosiągalnym punkcie.

  • Czy mogę mieć pewność, że baza danych została już zaktualizowana podczas tworzenia kopii zapasowej lub przywracania? W przeciwnym razie oczekiwany schemat może nie być zgodny.

pjv
źródło
Mam ten sam problem ...
Nie ma możliwości prostej kopii zapasowej db dziury?
Jin35,
Muszę utworzyć kopię zapasową folderu przy użyciu programu FileBackupHelper. Czy użycie metody zapisywania bazy danych pozwoli mi zapisać ten folder, który zawiera wiele podfolderów i plików podrzędnych?
coolcool1994
Czy kiedykolwiek działało to poprawnie?
DeNitE Appz
Tak, patrz poniżej. Odpowiedziałem również na swoje pytanie.
pjv

Odpowiedzi:

21

Czystszym podejściem byłoby stworzenie niestandardowego BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

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

a następnie dodaj do BackupAgentHelper:

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}
yanchenko
źródło
2
@logray: Ten kod jest dokładnie taki sam jak w pytaniu. Niepotrzebna edycja.
Linuxios,
@Linuxios Zgadzam się, ale myślę, że lepiej jest podać autorowi odpowiednie przypisanie, niż ślepo wklejać kod ... biorąc pod uwagę, że oryginalny OP / post zawierał link z jego aplikacji.
logray
3
Ważne : (Dokumentacja) ( developer.android.com/guide/topics/data/backup.html#Files ) dotycząca tworzenia kopii zapasowych plików stwierdza, że ​​operacja nie jest bezpieczna wątkowo , dlatego należy zsynchronizować metody bazy danych i agentów kopii zapasowych
Nicopico
@yanchenko Dokumentacja dla FileBackupHelper stwierdza: „Uwaga: powinno to być używane tylko z małymi plikami konfiguracyjnymi, a nie dużymi plikami binarnymi”. Więc baza danych często kwalifikowałaby się jako duży plik binarny ...
Igor Ganapolsky
To tak naprawdę nie działa! (chyba że czegoś mi brakuje ...). Problem polega na tym, że podajesz bezwzględną ścieżkę do bazy danych do FileBackupHelper, ale FileBackupHelper poprzedza ścieżkę ścieżką do katalogu plików, np. data / data / <your package> / files, co skutkuje nieprawidłową ścieżką, taką jak data / data / <your package> / files / data / data / <your package> / databases / <DB Name>, gdy wszystko, czego chcesz, to Bezwzględna nazwa DB, a ponieważ superklasa poprzedza katalog plików, musisz ustawić ścieżkę względem katalogu plików.
bitrock
34

Po ponownym przejrzeniu mojego pytania udało mi się uruchomić je po sprawdzeniu, jak robi to ConnectBot . Dzięki Kenny i Jeffrey!

W rzeczywistości jest to tak proste, jak dodanie:

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

do twojego BackupAgentHelper.

Brakowało mi tego, że musiałbyś użyć ścieżki względnej z „ ../databases/”.

Nie jest to jednak idealne rozwiązanie. Dokumenty, FileBackupHelperna przykład, wspominają: „ FileBackupHelperpowinny być używane tylko z małymi plikami konfiguracyjnymi, a nie dużymi plikami binarnymi ”, przy czym to drugie dotyczy baz danych SQLite.

Chciałbym uzyskać więcej sugestii, wglądu w to, czego się od nas oczekuje (jakie jest właściwe rozwiązanie) i porady, jak to może się zepsuć.

pjv
źródło
1
Powinienem chyba dodać, że chociaż jest to trochę nieoficjalne, to przez cały ten czas działa bardzo dobrze.
pjv
6
Zakodowane ścieżki są złe.
Pointer Null
@pjv, więc czy synchronizujesz każdą interakcję z bazą danych? Robię to samo i próbuję dowiedzieć się, czy muszę zsynchronizować się db.open()za pomocą instrukcji, czy db.close()tylko insert, czy co.
NSpołudnie
22

Oto jeszcze czystszy sposób tworzenia kopii zapasowych baz danych w postaci plików. Brak zakodowanych ścieżek.

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

Uwaga: zastępuje getFilesDir dzięki czemu FileBackupHelper działa w katalogu baz danych, a nie w katalogu plików.

Kolejna wskazówka: możesz również użyć databaseList, aby pobrać wszystkie swoje bazy danych i nazwy kanałów z tej listy (bez ścieżki nadrzędnej) do FileBackupHelper. Wtedy wszystkie bazy danych aplikacji zostaną zapisane w kopii zapasowej.

Pointer Null
źródło
Połączenie tego z rozwiązaniem podanym przez @pjv działa idealnie! Może nie jest to dokładnie to, co miała na myśli specyfikacja ... ale działa całkiem nieźle!
Tom
1
Nie jest to zbyt przydatne, jeśli masz bazy danych i zwykłe pliki do tworzenia kopii zapasowych.
Dan Hulme,
1
@Dan: w takim przypadku użyj wielu agentów.
pjv
@Pointer Null Czy mógłbyś opisać nieco więcej funkcji getFilesDir (), dlaczego jest to naprawdę potrzebne i dlaczego? Jak to działa?
proszek366
7

Użycie FileBackupHelperdo tworzenia kopii zapasowych / przywracania sqlite db rodzi poważne pytania:
1. Co się stanie, jeśli aplikacja użyje kursora pobranego z, ContentProvider.query()a agent kopii zapasowej spróbuje zastąpić cały plik?
2. Link jest dobrym przykładem doskonałego (niska entrofia;) testowania. Odinstaluj aplikację, zainstaluj ją ponownie i kopia zapasowa zostanie przywrócona. Jednak życie może być brutalne. Spójrz na link . Wyobraźmy sobie scenariusz, w którym użytkownik kupuje nowe urządzenie. Ponieważ nie ma własnego zestawu, agent kopii zapasowych używa zestawu innego urządzenia. Aplikacja zostanie zainstalowana, a program backupHelper pobierze stary plik ze schematem wersji bazy danych niższym niż bieżący. SQLiteOpenHelperwywołania onDowngradez domyślną implementacją:

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

Bez względu na to, co robi użytkownik, nie może korzystać z Twojej aplikacji na nowym urządzeniu.

Sugerowałbym użycie ContentResolverdo pobierania danych -> serializuj (bez_id s) do tworzenia kopii zapasowych i deserializacji -> wstaw dane do przywrócenia.

Uwaga: pobieranie / wstawianie danych odbywa się za pośrednictwem ContentResolver, co pozwala uniknąć problemów z walutami. Serializacja jest wykonywana w backupAgent. Jeśli wykonujesz własne mapowanie obiektów kursora <->, serializowanie elementu może być tak proste, jak implementacja Serializableztransient polem _id w klasie reprezentującej Twoją jednostkę.

Użyłbym również wstawiania zbiorczego, tj. ContentProviderOperation Przykład iCursorLoader.setUpdateThrottle tak, że aplikacja nie jest zablokowany z ponownym Loader na zmianę danych w trakcie procesu przywracania kopii zapasowych.

Jeśli zdarzy się, że jesteś w sytuacji obniżenia wersji, możesz przerwać przywracanie danych lub przywrócić i zaktualizować ContentResolver z polami dotyczącymi obniżonej wersji.

Zgadzam się, że temat nie jest łatwy, nie jest dobrze wyjaśniony w dokumentach, a niektóre pytania nadal pozostają takie jak zbiorczy rozmiar danych itp.

Mam nadzieję że to pomoże.

Piotr Bazan
źródło
Powtarzasz kilka ważnych pytań. Ale nie widzę, jak serializacja bazy danych rozwiązuje problem. To nie pomaga w przypadku problemów ze współbieżnością. A jeśli nie możesz napisać skryptu w celu obniżenia wersji bazy danych, nie możesz napisać skryptu do deserializacji starych danych.
pjv
@pjv Jeśli używasz ContentResolver, rozwiązuje on za Ciebie problemy ze współbieżnością.
Igor Ganapolsky
@IgorGanapolsky Tak, myślę, że to prawda. Jeśli jednak serializacja jest wystarczająco powolna, a użytkownik już korzysta z aplikacji i wprowadza dane, może to powodować konflikt z przywracanymi danymi. Czy możesz to zrobić atomowo i zanim użytkownik uzyska dostęp do aplikacji?
pjv
@pjv Możesz zarejestrować ContentObserver, aby synchronizować dane, które obserwujesz, aby otrzymywać powiadomienia. Dlatego nie musisz martwić się konfliktami.
Igor Ganapolsky
Twój punkt widzenia na temat wersji DB jest dobry - myślę, że jeśli zapiszesz swoją wersję DB (na przykład w preferencjach współdzielonych, zakładając, że również ją utworzysz), to podczas przywracania powinieneś być w stanie użyć istniejącej logiki, aby zaktualizować DB do jej aktualna wersja w metodzie onDowngrade (). Jeśli nie masz jeszcze kodu aktualizacyjnego, i tak nie przejmujesz się przechowywaniem danych!
Ben Neill
3

Począwszy od systemu Android M, dla aplikacji dostępny jest teraz interfejs API pełnego tworzenia kopii zapasowych / przywracania danych. Ten nowy interfejs API zawiera w manifeście aplikacji specyfikację opartą na języku XML, która umożliwia programistom opisanie plików, których kopie zapasowe mają być tworzone w sposób bezpośredni semantyczny: „wykonaj kopię zapasową bazy danych o nazwie„ mydata.db ””. Ten nowy interfejs API jest znacznie łatwiejszy w użyciu dla programistów - nie musisz śledzić różnic ani jawnie żądać przebiegu kopii zapasowej, a opis XML plików do utworzenia kopii zapasowej oznacza, że ​​często nie musisz pisać żadnego kodu w ogóle.

(You can wziąć udział nawet w pełnym wymiarze kopii zapasowej danych / przywrócenia działania, aby uzyskać oddzwanianie, gdy przywróci się dzieje, na przykład. Jest elastyczny w ten sposób).

Zobacz sekcję Konfigurowanie automatycznej kopii zapasowej dla aplikacji na developer.android.com, aby zapoznać się z opisem korzystania z nowego interfejsu API.

ctate
źródło
Dotyczy to tylko Androida 6.0 (API 23), jeśli użytkownik ma starszą wersję, nadal musi zaimplementować kopię zapasową wartości klucza.
miłość na żywo
0

Jedną z opcji będzie zbudowanie go w logice aplikacji powyżej bazy danych. Myślę, że to rzeczywiście krzyczy na taki poziom. Nie jestem pewien, czy już to robisz, ale większość ludzi (pomimo podejścia kursora menedżera treści w Androidzie) wprowadzi pewne mapowanie ORM - niestandardowe lub niektóre podejście orm-lite. A w tym przypadku wolałbym:

  1. aby upewnić się, że aplikacja działa dobrze, gdy aplikacja / dane są dodawane w tle, a nowe dane są dodawane / usuwane, gdy aplikacja jest już uruchomiona
  2. aby zrobić trochę Java-> protobuf lub nawet po prostu mapowanie serializacji java i napisać własny BackupHelper, aby odczytać dane ze strumienia i po prostu dodać je do bazy danych ....

Więc w tym przypadku zamiast robić to na poziomie db, zrób to na poziomie aplikacji.

Jarek Potiuk
źródło
Problem nie polega na tym, jak pobrać dane z bazy danych do BackupHelperAgent lub odwrotnie. Przeczytaj 5 punktorów na końcu mojego pytania.
pjv
@JarekPotiuk Powiedziałeś: „większość ludzi (pomimo podejścia kursora menedżera treści na Androida)”. Ale co sprawia, że ​​myślisz, że większość ludzi unika używania ContentProvider i ContentResolver do dostępu do bazy danych?
Igor Ganapolsky