воскресенье, 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. Есть что добавить, пишите в комменты.

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