воскресенье, 16 февраля 2014 г.

Обновляем файл базы данных без перезапуска приложения

Я уже писал об этом тут Несколько особенностей и вопросы по ним, в комментариях предложили несколько вариантов, которые можно использовать, чтобы переносить изменённые данные из старой базы в новую. В моём сообщении речь шла об обновлении файла базы данных без внесения изменений, нормального решения на тот момент я так и не нашёл. Оставалось только удалять файл базы и перезапускать приложение, чтобы подтянулся новый файл. Но нет таких пользователей, которым нравилось бы каждый раз перезапускать приложение, поэтому необходимо нормальное решение.

Upd (21.04.14). Проверил код на Delphi XE6 и добавил информацию о необходимых изменениях.


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

Итак, представим себе ситуацию:
Мы написали приложение, содержащее в себе базу с кулинарными рецептами (можете подставить сюда что-то своё :). В первой версии приложения содержится 100 рецептов. Через месяц мы добавляем в базу ещё 50 рецептов и раздаём (не через Маркет) пользователям новую версию приложения.

Вроде всё просто, но есть одна особенность, при установке нового приложения поверх старого, база с рецептами из старого приложения сохранится, и пользователь будет видеть только её. Т.е. все данные сохранятся, а нам ведь надо показать пользователю новые рецепты. Вот тут я ни как не мог найти решения и, похоже, не я один…

Сегодня я с гордостью могу заявить, что нашёл/написал решение данной проблемы. Пришлось ещё немного углубиться в Android, но это принесло плоды в виде решения некоторых проблем/задач (Например: Получаем список доступных устройств хранения информации).
Идём далее.

Основы.
В Android все установленные приложения, т.е. «.apk» файлы хранятся в директории «/data/app/». Эта директория не доступна пользователям (без root-прав) также как и любая директория из «/data/».
Задеплоинные файлы, после установки приложения хранятся тут «/data/data/app_name/files». При обновлении/запуске приложения осуществляется проверка файлов и если какой-то файл не найден в папке «/data/data/app_name/files», то он достаётся из «.apk» файла. Значит, чтобы получить новую версию базы данных, нужно удалить старую и вытащить новую из «.apk» файла. Не всё так просто :).

Теперь кодим.
«.apk» файл – это ZIP архив. Посмотрите на скриншот.

Важное замечание: В директории «/data/app/» все «.apk» файлы хранятся с полным именем приложения (пример: «com.embarcadero.Project1») и в конец добавляется тире и цифра (обычно единица или двойка).

Пошагово, на примере обновления базы данных при помощи кнопки (сделано для удобства понимания, можно перенести например на событие TForm1.FormCreate):

  1. При запуске новой версии приложения, пользователь нажимает кнопку «Обновить»
  2. В коде мы отключаемся от базы (если были подключены)
  3. Получаем имя «.apk» файла в директории «/data/app/» (это я сделал при помощи JNI - API Android - getPackageResourcePath())
  4. Теперь удаляем (TFile.Delete) старый файл, который лежит где-то тут «/data/data/app_name/files/test.db»
  5. Открываем «.apk» файл (Delphi умеет работать с Zip - System.Zip)
  6. Извлекаем файл «assets/internal/test.db» в папку «/data/data/app_name/files/»
  7. Закрываем архив и подчищаем память
  8. Подключаемся к базе и вуаля, видим новые рецепты

Код для примера (Delphi XE5 Update 2):
unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, Data.DbxSqlite,
  Data.DB, Data.SqlExpr, Data.FMTBcd, FMX.StdCtrls;

type
  TForm1 = class(TForm)
    test_connect: TSQLConnection;
    SQLQuery1: TSQLQuery;
    Label1: TLabel;
    Button1: TButton;
    Button2: TButton;
    Label2: TLabel;
    procedure test_connectBeforeConnect(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses
 System.IOUtils, System.Zip, Androidapi.JNI.JavaTypes, FMX.Helpers.Android;

procedure TForm1.Button1Click(Sender: TObject);
begin
 SQLQuery1.Active := True;
    SQLQuery1.Open;
    Label1.Text := SQLQuery1.FieldByName('allcount').AsString;
    SQLQuery1.Close;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
 zip: TZipFile;
 PackageName: JString;
begin
 // Отключаемся от базы
 test_connect.Connected := False;

 // Получаем имя apk файла
 PackageName := SharedActivityContext.getPackageResourcePath;

 if TFile.Exists(JStringToString(PackageName)) then
 begin
  // Удаляем старый файл базы
  TFile.Delete(TPath.GetHomePath + PathDelim + 'test.db');
  
  // Извлекаем новый файл базы
  zip := TZipFile.Create;
  zip.Open(JStringToString(PackageName), TZipMode.zmRead);
  zip.Extract('assets/internal/test.db', TPath.GetDocumentsPath, False);
  zip.Close;
  zip.free;
 end
 else
  showmessage('False');

 // Подключаемся к базе
 test_connect.Connected := True;
 
 // Выполняем запрос на количество записей в базе
 SQLQuery1.Active := True;
 SQLQuery1.Open;
 Label2.Text := SQLQuery1.FieldByName('allcount').AsString;
 SQLQuery1.Close;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
    test_connect.Connected := True;
end;

procedure TForm1.test_connectBeforeConnect(Sender: TObject);
begin
 test_connect.Params.Values['Database'] := TPath.GetDocumentsPath + PathDelim + 'test.db';
end;

end.

UPDATE (21.04.14):
Чтобы код заработал в Delphi XE6, необходимо:
в "uses" добавить модуль "Androidapi.Helpers".

Архив обновлён (Добавил комментарий для Delphi XE6)!

Видео:
На видео я показываю, как сначала устанавливаю приложение с двумя записями в базе, а потом обновляю приложение до 3 записей в базе. И всё это без всяких перезагрузок и т.п.




Исходный код: Скачать с Google Drive

На этом всё.

p.s. Есть что добавить, пишите в комменты.

Для тех, кто читает через строчку:
Внимание! Этот способ предназначен для случаев когда в базу не вносятся изменения, а так же обновление приложения происходит вручную, т.е. НЕ через Маркет. Видимо это не ясно из статьи. 

21 комментарий:

  1. Андрей, спасибо за статью.

    Насколько я понимаю этот метод подходит и для решения такой же проблемы при разработке под iOS?

    ОтветитьУдалить
    Ответы
    1. К сожалению, я не могу вам подсказать, т.к. имею возможность работать только с Android. Проще говоря, у меня нет продукции от компании Apple.

      Удалить
    2. В ближайшее время постараюсь протестировать ваше решение на продуктах от Apple. Потом поделюсь результатами.

      Удалить
  2. Плохое решение, тут не учитвается то, что к части рецептов (развиваем кулинарную тему) я уже написал свои коментарии и примечания. После обновления все исчезнет.

    ОтветитьУдалить
    Ответы
    1. Решение отличное, статью не внимательно прочитали…

      Удалить
  3. Да как я уже писал, в данном решения не учитываются настройки пользователя. Может он некоторые рецепты уже добавил в избранное, или еще как-то по своему настроил программу. Можно конечно хранить настройки пользователя и обновляемую инфу в разных файлах БД. но это тоже не отпимальное решение. Да и тем более приложение может обновляться из вне программы. Например из Маркета. В этом случае программа не будет иметь возможность предварительно удалить старую БД... Соответственно для серьезных проектов предложенное решение мало подходит.

    ОтветитьУдалить
    Ответы
    1. Похоже вы прочитали статью через строчку… В самом начале я сказал, что это решение годится для обновления базы в которую не предполагается внесение изменений от пользователя. Про маркет, опять же прочитали через строчку, в статье указано, что обновление приложения происходит не через Маркет. Кстати говоря, в начале статьи есть ссылка на ваше решение для обновления файла базы данных с изменениями.

      Удалить
  4. Для андроида я пишу данные в корень, чтобы легко найти с ПК по шнурку через проводник. Очень удобно смотреть что происходит.
    (http://alhymov.blogspot.ru/2013/12/blog-post.html#gpluscomments)
    initialization
    {$ifdef Android}
    ConfigFile := TPath.GetSharedDocumentsPath;
    ConfigFile := ConfigFile.Substring( 0, ConfigFile.IndexOf( '/Android/' ) ) + '/WhoIsWho/WhoIsWho.Config';
    if not TFile.Exists( ConfigFile ) then begin
    ForceDirectories( TPath.GetDirectoryName(ConfigFile ) );
    TFile.Copy( TPath.GetDocumentsPath + '/WhoIsWho.Config', ConfigFile );
    end;
    {$else}
    ConfigFile :=

    Для iOS, если использовать iTunes, то приложение сначала надо удалить со всеми настройками, т.е. обновляется полностью нормально. Чтобы настройки у пользователя остались я использую TestFlight(http://alhymov.blogspot.ru/2014/02/testflightapp.html). У меня есть в программе команда "обновить", которая удаляет требующий обновления файл. При этом пользовательские данные остаются. Т.е. есть чёткое разделение на R/O(Автор поста подчеркнул даже - без внесения изменений) и на меняющиеся настройки-данные пользователя.
    Если делить на базы не хочется (запросы чтобы строить совместно, например), то можно использовать дополнительный "внешний" файл - save или update (смотря, что тяжелее).
    При старте программы можно проверить версию приложения и сравнить с меткой в файле конфигурации или в базе. Если есть необходимость, то надо выйти, предварительно удалив update или подготовив save и удалив базу. При следующем старте из пакета приложения выделится (см.System.StartUpCopy) обновлённый файл, а мы, проверив версию, поймём: нужно обновить базу из "внешнего" источника.
    Я думаю, что если сказать обоснованно юзеру, что требуется перезагрузка, то он не будет возмущаться. Он только что загрузил новую версию, запустил - программа рассказала, что "да, это новая версия" и вежливо попросила запуститься ещё раз - сценарий вполне рабочий.

    ОтветитьУдалить
    Ответы
    1. Внимание! Видимо это не ясно из коммента :). Спасибо автору за практичный метод для андроида.

      Удалить
    2. По сути, то, что делает ваше приложение (про конфигурационный файл), хорошо только в случае отладки. А по факту приложение мусорит у пользователя на sdcard. Зачем вообще создавать отдельную папку в корне карты, если вам и так доступна папка "/Android/data/com.embarcadero.WhoIsWho/files/", эта папка доступна без root-прав и в неё можно зайти через обычный проводник на компьютере. Разве что лень делать на 3 клика больше… Я бы не стал допускать такое в релиз версию, т.к. если все приложения начнут делать как ваше, то получится это http://beta.hstor.org/storage1/e5deae9f/54a3c20b/8ba76329/55414a1a.jpg.

      А ещё какие-нибудь сервисы типа TestFlight знаете? Было бы классно почитать сравнительный обзор.

      По поводу второго коммента. Ну вот, план (выделение жирным шрифтом) сработал, теперь точно всем понятно, о чём моя статья :) Всегда рад помочь!

      Удалить
    3. >по факту приложение мусорит
      Да. В 1-ю очередь - именно для отладки. Но! "лень делать на 3 клика больше" - достаточно весомый аргумент. Тем более, что для каждого клика нужно хорошо целиться. Это раз. А два - смотрите, что те же Яндекс или Мэйл-ру делают - они никого не стесняются, и лепят свои каталоги туда, где их лучше видно. Чем мы хуже?

      Я сам про TestFlight недавно узнал :) Честно сказать, мне этого хватило, а дальше искать нужды не было.

      Удалить
    4. С отладкой всё ясно.
      По поводу Яндекса и Мэйл, у яндекса по большей части папка создаётся для кеша. И да, это мусор, если приложение удалить, то вся эта папка останется на карте(к слову, вес данной папки у меня был около 2Гбайт).
      Мы не хуже :) и явно никого не стесняемся, но давайте смотреть и со стороны пользователя, ведь у пользователя на карте хранятся не только программы, а ещё куча медийных файлов. Вы смотрели скриншот из моего комментария? Знаете, что делают люди в таких вот ситуациях? Они создают примерно вот такие папки:
      _Видео,
      _Документы,
      _Музыка,
      _Прочее,
      _Карты,
      _Фото.
      Это неправильно, если приложение удаляется, то и все связанные с ним файлы должны удаляться.
      На крайний случай, разработчики Google Android могли бы ввести жёсткое правило для всех приложений: Все данные, которые должны быть легко доступны и оставаться на карте после удаления приложения, должны храниться, например, по такому пути /sdcard/apps/название приложения/. Тогда не было бы такого захламления карты памяти (всегда был бы порядок) и всем бы было хорошо. Но к сожалению я не работаю в Google и не имею там прямых знакомых/друзей, чтобы предложить такой вариант.

      Удалить
    5. Итого, можно сказать, что у насесть по меньшей мере 2 общепринятых места хранения настроек в Андроид. Выбор места зависит от прав. 1 - право приложения на внешнюю запись/чтение (см.пермишенс проекта) и 2 - моральное право программиста :)

      Удалить
    6. Да примерно так…
      Давайте ориентироваться не на Яндекс и Мейл, а на приложения Гугла. Берём Гугл Карты и смотрим, куда сохраняется кэш и не только он /Android/data/com.google.android.apps.maps/, и это бьёт «карту» с Яндексом и Мейлом :)
      В общем, каждый вправе сам выбирать…

      Удалить
  5. Andrey Yefimov - мы можем как-то пообщаться? есть несколько вопросов, некоторые из них я думаю потянут на новую статью
    Если да напиши на antarey@gmail.com

    ОтветитьУдалить
    Ответы
    1. Конечно можем, моя почта указана на странице "О блоге". Продублирую: infocean @ gmail.com (пробелы удалить).

      Удалить
  6. Получается если приложение в Google Play, то обновляться бд будет автоматически? Без ручной установки, засчет автоматической установки сразу от Google?

    ОтветитьУдалить
    Ответы
    1. У меня нет аккаунта разработчика в Google Play, поэтому не могу сказать точно.

      Удалить
    2. Протестировал в Google Play, бд НЕ обновляется автоматически. Delphi XE6. А что делать если нужно сохранить данные старой бд, и добавить новые таблицы/ячейки которые отсутствуют или изменились?

      Удалить
    3. Данил, неужели вы не знаете как "мёржить базы"? К теме статьи это прямого отношения не имеет. Андрей рассказал, как без особого джава-шаманства штатными средствами Делфи извлечь файл из пакета приложения. Замечательно! Проблема изменения БД настолько широка и настолько стара, что не стоит об этом говорить без какой-либо конкретики. Иначе ваш вопрос выглядит довольно странно. -"Что делать если нужно сохранить?" - Сохраните!

      Удалить
    4. Павел, Увы, я с этим еще не сталкивался, но учту что "мёржить" их все же можно. Наверное, изначально не правильно поставил вопрос, пойду изучать что-ли. с:

      Удалить