Резервное копирование / восстановление Android: как сделать резервную копию внутренней базы данных?

86

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

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

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

Обратите внимание, что это все после установки и перед первым запуском приложения, после того как было выполнено восстановление. Кроме того, моя стратегия тестирования была основана на http://developer.android.com/guide/topics/data/backup.html#Testing .

Также обратите внимание, что я не говорю ни о какой-то базе данных sqlite, которой я управляю сам, ни о резервном копировании на SD-карту, собственный сервер или где-то еще.

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

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

Некоторая ясность, пожалуйста.

Если мне действительно нужно сделать это самому до уровня SQL, то меня беспокоят следующие темы:

  • Открытые базы данных и транзакции. Я понятия не имею, как закрыть их из такого одноэлементного класса вне рабочего процесса моего приложения.

  • Как уведомить пользователя о том, что выполняется резервное копирование и база данных заблокирована. Это может занять много времени, поэтому мне может потребоваться показать индикатор выполнения.

  • Как сделать то же самое при восстановлении. Насколько я понимаю, восстановление может произойти только тогда, когда пользователь уже начал использовать приложение (и вводить данные в базу данных). Таким образом, вы не можете себе позволить просто восстановить данные из резервной копии на месте (удалив пустые или старые данные). Вам придется как-то присоединиться к нему, что для любой нетривиальной базы данных невозможно из-за идентификатора.

  • Как обновить приложение после завершения восстановления, чтобы пользователь не застрял в какой-то - сейчас - недоступной точке.

  • Могу ли я быть уверен, что база данных уже была обновлена ​​при резервном копировании или восстановлении? В противном случае ожидаемая схема может не совпадать.

pjv
источник
У меня такая же проблема ...
Нет возможности простой бэкап дырки 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 / <your package> / files, что приводит к неправильному пути, например data / data / <your package> / files / data / data / <your package> / 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.

Я хотел бы получить больше предложений, понимание того, что от нас ожидается (какое решение является правильным), и советы о том, как это может сломаться.

pjv
источник
1
Я, наверное, должен добавить, что, хотя это немного неофициально, все это время он работал / работал очень хорошо.
pjv
6
Пути жесткого кодирования - это зло.
Pointer Null
@pjv, Так ты делаешь синхронизацию всех взаимодействий с db? Я делаю то же самое , и пытаюсь выяснить , если мне нужно синхронизировать 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
Это не очень полезно, если у вас есть базы данных и обычные файлы для резервного копирования.
Дэн Халм
1
@Dan: В этом случае используйте несколько агентов.
pjv
@Pointer Null Не могли бы вы подробнее рассказать об одном getFilesDir (), почему он действительно нужен и почему? Как это работает?
Powder366
7

Использование FileBackupHelpersqlite db для резервного копирования / восстановления вызывает ряд серьезных вопросов:
1. Что произойдет, если приложение использует полученный курсор, ContentProvider.query()а агент резервного копирования попытается переопределить весь файл?
2. Ссылка - хороший пример идеального (с низкой энтрофией;) тестирования. Вы удаляете приложение, устанавливаете его снова, и резервная копия восстанавливается. Однако жизнь может быть жестокой. Взгляните на ссылку . Представим себе сценарий, когда пользователь покупает новое устройство. Поскольку у него нет собственного набора, агент резервного копирования использует набор другого устройства. Приложение установлено, и ваш backupHelper извлекает старый файл со схемой версии базы данных ниже текущей. 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, что позволяет избежать проблем с валютой. Сериализация выполняется в вашем агенте резервного копирования. Если вы выполняете собственное отображение объекта cursor <->, сериализацию элемента можно так же просто, как реализовать его 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.

ctate
источник
Это только для Android 6.0 (API 23), если у пользователя более старая версия, ему все еще необходимо реализовать резервное копирование ключевого значения.
live-love
0

Один из вариантов - встроить его в логику приложения над базой данных. Думаю, он действительно кричит о таком уровне. Не уверен, что вы это уже делаете, но большинство людей (несмотря на подход курсора к диспетчеру контента Android) представят некоторое сопоставление ORM - либо индивидуальный, либо некоторый подход orm-lite. И в этом случае я бы предпочел:

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

Поэтому в этом случае, а не на уровне базы данных, сделайте это на уровне приложения.

Ярек Потюк
источник
Проблема не в том, как получить данные из базы данных в BackupHelperAgent или наоборот. Пожалуйста, прочтите 5 пунктов в конце моего вопроса.
pjv
@JarekPotiuk Вы сказали: «большинство людей (несмотря на подход курсора в диспетчере контента Android)». Но почему вы думаете, что большинство людей будет избегать использования ContentProvider и ContentResolver для доступа к базе данных?
Игорь Ганапольский