Резервне копіювання / відновлення Android: як зробити резервну копію внутрішньої бази даних?


86

Я BackupAgentHelperзастосував надане FileBackupHelperдля резервного копіювання та відновлення власної бази даних, яку маю. Це база даних, якою ти зазвичай користуєшся ContentProvidersі яка знаходиться /data/data/yourpackage/databases/.

Можна подумати, що це звичайний випадок. Однак у документах незрозуміло, що робити: http://developer.android.com/guide/topics/data/backup.html . Там немає BackupHelperспеціально для цих типових баз даних. Тому я використавFileBackupHelper , вказав його на мій файл .db у " /databases/", ввів замки навколо будь-якої операції з db (наприклад, db.insert) у моїй ContentProvidersі навіть спробував створити /databases/каталог " " раніше, onRestore()оскільки він не існує після встановлення.

SharedPreferencesРаніше я впроваджував подібне рішення для успішного використання в іншому додатку. Однак, коли я тестую свою нову реалізацію в емуляторі-2.2, я бачу, як виконується резервне копіювання LocalTransportз журналів, а також виконується (і onRestore()викликається) відновлення . Проте сам файл db ніколи не створюється.

Зверніть увагу, що це все після встановлення та до першого запуску програми, після відновлення. Окрім цього, моя стратегія тестування базувалася на http://developer.android.com/guide/topics/data/backup.html#Testing .

Зауважте також, що я не кажу про якусь базу даних sqlite, якою я сам керую, а також про резервне копіювання на SDcard, власний сервер чи деінде.

Я бачив у документах згадування про бази даних, які радять використовувати спеціальний, BackupAgentале, схоже, це не пов’язано:

Однак, можливо, ви захочете розширити програму BackupAgent безпосередньо, якщо вам потрібно: * Резервне копіювання даних у базі даних. Якщо у вас є база даних SQLite, яку ви хочете відновити, коли користувач переінсталює вашу програму, вам потрібно створити власний BackupAgent, який зчитує відповідні дані під час операції резервного копіювання, а потім створіть таблицю та вставте дані під час операції відновлення.

Будь ласка, ясність.

Якщо мені справді потрібно зробити це до рівня SQL, то мене турбують такі теми:

  • Відкриті бази даних та транзакції. Я не уявляю, як закрити їх із такого однокласного класу поза робочим процесом мого додатка.

  • Як повідомити користувача про те, що виконується резервне копіювання, а база даних заблокована. Це може зайняти багато часу, тому, можливо, мені доведеться показати індикатор прогресу.

  • Як зробити те ж саме при відновленні. Як я розумію, відновлення може відбутися саме тоді, коли користувач вже почав користуватися програмою (і вводити дані в базу даних). Тому ви не можете припустити, що просто відновите резервні копії даних на місці (видаливши порожні або старі дані). Вам доведеться якось приєднатися до цього, що для будь-якої нетривіальної бази даних неможливо через ідентифікатори.

  • Як оновити програму після відновлення, не застрягаючи користувача в якійсь - зараз - недосяжній точці.

  • Чи можу я бути впевненим, що базу даних вже оновлено при резервному копіюванні чи відновленні? В іншому випадку очікувана схема може не збігатися.


У мене така сама проблема ...

Не існує простого отвору для резервного копіювання db?
Jin35

Мені потрібно створити резервну копію папки за допомогою FileBackupHelper. Чи дозволить мені використання підходу до збереження бази даних зберегти ту папку, яка має багато підпапок та підфайлів під нею?
coolcool1994

Ви коли-небудь працювали правильно?
DeNitE Appz

Так, дивіться нижче. Я також відповів на власне запитання.
pjv

Відповіді:


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
Важливо : (Документація) ( developer.android.com/guide/topics/data/backup.html#Files ) для резервного копіювання файлів говорить про те, що операція не є безпечною , тому вам слід синхронізувати методи бази даних та агенти резервного копіювання
nicopico

Документація @yanchenko для FileBackupHelper говорить: "Примітка. Це слід використовувати лише з малими файлами конфігурації, а не з великими двійковими файлами." Тож база даних часто кваліфікується як великий двійковий файл ...
Ігор Ганапольський

Це насправді не працює! (якщо я чогось не пропускаю ...). Проблема полягає в тому, що ви передаєте абсолютний шлях db до FileBackupHelper, але FileBackupHelper додає шлях до шляху до каталогу файлів, наприклад. data / data / <ваш пакет> / files, що призводить до неправильного шляху щось на зразок data / data / <ваш пакет> / files / data / data / <ваш пакет> / databases / <DB Name>, коли все, що вам потрібно, це Абсолютне ім'я БД, а оскільки супер клас додає каталог файлів, вам потрібно зробити шлях щодо каталогу файлів.
bitrock

34

Переглянувши своє запитання, я зміг змусити його працювати, подивившись, як це робить ConnectBot . Дякую Кенні та Джеффрі!

Насправді це так само просто, як додати:

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

до вашого BackupAgentHelper.

Суть, якого мені не вистачало, полягає в тому, що вам доведеться використовувати відносний шлях із " ../databases/".

Проте це далеко не ідеальне рішення. Документи для FileBackupHelperзгадування, наприклад: " FileBackupHelperслід використовувати лише з малими файлами конфігурації, а не великими двійковими файлами. ", Останнє стосується баз даних SQLite.

Я хотів би отримати більше пропозицій, розуміння того, що від нас очікують (яке є правильне рішення), та поради щодо того, як це може зламатися.


1
Слід, мабуть, додати, що, хоча це і трохи неофіційно, воно / працює дуже добре весь цей час.
pjv

6
Шляхи жорсткого кодування - це зло.
Нуль покажчика

@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, працює чудово! Можливо, це не зовсім те, що враховували технічні характеристики ... але це працює досить добре!
Том,

1
Це мало корисно, якщо у вас є бази даних і звичайні файли для резервного копіювання.
Dan Hulme

1
@Dan: У такому випадку використовуйте кілька агентів.
pjv

@Pointer Null Чи могли б ви трохи більше розробити один getFilesDir (), чому він справді потрібний і чому? Як це працює?
пудра366

7

Використання FileBackupHelperдля резервного копіювання / відновлення sqlite db викликає кілька серйозних питань:
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для отримання даних -> серіалізувати (без _ids) для резервного копіювання та десеріалізувати -> вставити дані для відновлення.

Примітка: отримання / вставка даних здійснюється через ContentResolver, таким чином уникаючи проблем з валютою. Серіалізація виконується у вашому резервному агенті. Якщо ви робите свій власний курсор <->, відображення об'єкта серіалізацією елемента може бути таким самим простим, як реалізаціяSerializable допомогою transientполя _id у класі, що представляє вашу сутність.

Я б також використав об'ємну вставку, тобто ContentProviderOperation приклад, і CursorLoader.setUpdateThrottleщоб програма не застрягла при перезавантаженні завантажувача при зміні даних під час процесу відновлення резервної копії.

Якщо трапилося, що ви потрапили в ситуацію зниження версії, ви можете вибрати або скасувати відновлення даних, або відновити та оновити ContentResolver за допомогою полів, що стосуються пониженої версії.

Я погоджуюсь, що тема непроста, недостатньо добре пояснена в документах, і деякі питання все ще залишаються, як-от об'ємний обсяг даних тощо.

Сподіваюся, це допомагає.


Ви повторюєте кілька слушних питань. Але я не бачу, як серіалізація бази даних вирішує проблему. Це не допомагає у вирішенні проблем паралельності. І якщо ви не можете написати сценарій для зниження рівня бази даних, то ви не можете написати сценарій для десериалізації старих даних.
pjv

@pjv Якщо ви використовуєте ContentResolver, він вирішує проблеми з паралельністю.
Ігор Ганапольський

@IgorGanapolsky Так, я думаю, це правда. Однак, якщо серіалізація відбувається досить повільно і користувач вже використовує програму та вводить дані, це може суперечити даним, які ви відновлюєте. Чи можете ви це зробити атомно і до того, як користувач отримає доступ до програми?
pjv

@pjv Ви можете зареєструвати ContentObserver для синхронізації даних, за якими спостерігаєте, щоб отримувати сповіщення. Тому вам не доведеться турбуватися про конфлікти.
Ігор Ганапольський

Ваша думка щодо версій БД є доброю - я гадаю, якщо ви збережете свою версію БД (наприклад, у спільних налаштуваннях, наприклад, якщо ви також зробите резервну копію), тоді під час відновлення ви зможете використовувати існуючу логіку для оновлення БД до її поточна версія в методі onDowngrade (). Якщо у вас ще немає коду оновлення, ви все одно не турбуєтесь збереженням даних!
Бен Ніл

3

Що стосується Android M, то тепер для програм доступний API для резервного копіювання та відновлення даних. Цей новий API включає специфікацію на основі XML в маніфесті програми, яка дозволяє розробнику безпосередньо семантично описувати файли для резервного копіювання: 'резервне копіювання бази даних з назвою "mydata.db"'. Цей новий API набагато простіший у використанні для розробників - вам не потрібно відстежувати відмінності або явно вимагати проходження резервної копії, а опис XML файлів для резервного копіювання означає, що вам часто не потрібно писати код зовсім.

(Ви можете взяти участь навіть у операції повного резервного копіювання / відновлення даних, щоб отримати зворотний виклик, коли відбудеться відновлення, наприклад. Це гнучко.)

Див. Розділ Налаштування автоматичного резервного копіювання для програм на developer.android.com, щоб отримати опис використання нового API.


Це лише для Android 6.0 (API 23), якщо користувач має стару версію, одна все одно повинна реалізувати резервне копіювання ключа.
live-love

0

Одним із варіантів буде його побудова в логіці програми над базою даних. Я думаю, це насправді кричить про такий рівень. Не впевнений, що ви вже це робите, але більшість людей (незважаючи на підхід до курсора управління вмістом android) запровадять певне зіставлення ORM - або користувальницький, або якийсь підхід orm-lite. І що я волів би зробити в цьому випадку, це:

  1. щоб переконатися, що ваша програма працює нормально, коли додаток / дані додаються у фоновому режимі та додають / видаляють нові дані, коли програма вже запущена
  2. зробити якесь Java-> protobuf або навіть просто відображення серіалізації Java та написати свій власний BackupHelper, щоб прочитати дані з потоку та просто додати їх до бази даних ....

Тож у цьому випадку замість того, щоб робити це на рівні db, робіть це на рівні програми.


Проблема не в тому, як отримати дані з бази даних у BackupHelperAgent або навпаки. Будь ласка, прочитайте 5 кульок в кінці мого запитання.
pjv

@JarekPotiuk Ви сказали: "більшість людей (незважаючи на підхід до курсора управління вмістом Android)". Але що змушує вас думати, що більшість людей уникатимуть використання ContentProvider та ContentResolver для доступу до бази даних?
Ігор Ганапольський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.