Глава 10. Встраиваемые базы данных

Большинство мобильных приложений работают с данными. Создание приложений баз данных всегда было одной из сильнейших сторон Delphi. В этой главе мы узнаем, как создавать мобильные пользовательские интерфейсы (UI), управляемые данными, в FireMonkey, как использовать фреймворк доступа к базам данных FireDAC и как встраивать базы данных в мобильные устройства.

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

Технические требования

Исходный код примеров, рассмотренных в этой главе, можно найти в репозитории на GitHub.

Приложения, управляемые данными

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

Типичный подход — разделяй и властвуй: разбить большую проблему на более мелкие, которые проще решить. Наиболее распространённый подход в разработке программного обеспечения — разбить всю систему на чётко разделённые уровни (tiers). В приложении, управляемом данными, мы должны иметь возможность выделить как минимум две логические части: UI и логику доступа к данным. Чёткое разделение этих двух уровней позволяет создать плагинную архитектуру, где UI может подключаться к различным блокам доступа к данным стандартным способом, и UI может быть заменён без изменения базового уровня доступа к данным.

В контексте приложения Delphi мы можем разделить наш проект на три независимые сущности, как показано на рисунке 10.1:

Общая архитектура простого приложения базы данных
Рисунок 10.1: Общая архитектура простого приложения базы данных

Теперь давайте построим эту структуру в Delphi:

  1. Создайте новый пустой мультиплатформенный проект Delphi. Сохраните модуль главной формы как uFormToDo, а всё приложение как ToDoList. Измените свойство Name формы на FormToDo.
  2. Выберите File | New | Data Module в главном меню Delphi. Измените свойство Name модуля данных на DMToDo и сохраните его как uDMToDo.
  3. Теперь выберите File | New | Unit в главном меню Delphi, чтобы создать пустой модуль для проекта. Сохраните его как uToDoTypes.
Структура проекта простого приложения базы данных в Delphi
Рисунок 10.2: Структура проекта простого приложения базы данных в Delphi

Наш проект состоит из трёх различных модулей, которые представляют три основных строительных блока нашего приложения, управляемого данными. Теперь давайте создадим зависимости между этими тремя модулями. Модуль uToDoTypes не зависит ни от какого другого модуля в проекте, поэтому в его секции uses нечего добавлять. Этот модуль должен использоваться как UI, так и логикой доступа к данным. С помощью команды File и Use Unit добавьте модуль uToDoTypes в секции uses в интерфейсных частях формы и модуля данных.

Теперь добавьте модуль данных uDMToDo в секцию uses в части implementation главного модуля формы. Это должно соответствовать тому, что мы видели на рисунке 10.1.

Моделирование данных

Модуль uToDoTypes — это место, где мы можем добавить типы данных, которые являются общими для UI и модуля доступа к данным. Чтобы достичь надлежащего уровня разделения между UI и логикой доступа к данным, мы должны определить интерфейс для связи между двумя уровнями приложения. Модуль данных будет его реализовывать, а главная форма приложения должна использовать этот интерфейс как единственный способ связи с модулем данных. Таким образом, мы получаем настоящую плагинную архитектуру. UI может разрабатываться независимо, и мы можем предоставлять различные реализации логики доступа к данным без влияния на остальную часть приложения. Наше приложение, управляемое данными, может использовать различные типы встроенных баз данных и различные фреймворки доступа к данным, или, возможно, позже мы захотим переключиться на хранение данных в обычном файле или облачном хранилище.

Приложение ToDoList будет работать с задачами, поэтому имеет смысл объявить в модуле общих типов тип, который будет представлять одну задачу. Нам нужно решить, должен ли этот тип быть записью (record) или классом. С записями нам не нужно беспокоиться об управлении памятью, и обычно записи имеют лучшую производительность, чем объекты. С другой стороны, определение нашего типа «обёртки данных» как объекта также является хорошим вариантом. Типы записей не могут наследоваться. В более сложных сценариях гибкость, которую даёт использование объектов, может оправдать их использование вместо записей.

Какие атрибуты должна иметь задача? Чтобы сохранить демо простым, давайте просто добавим строковые поля Title и Category. В базах данных также удобно иметь идентификатор для любого элемента; обычно это целочисленное значение.

Вот как мы могли бы представить задачу в коде, в модуле uToDoTypes:

type
  TToDo = record
    Id: integer;
    Title: string;
    Category: string;
  end;

  TToDos = TList<TToDo>;

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

Следующий шаг — определить стандартные операции, которые должен предоставлять модуль данных. В мире баз данных это обычно называется CRUD-операциями. Эта аббревиатура означает Create, Read, Update и Delete (Создание, Чтение, Обновление и Удаление). У нас есть только одна сущность в нашей модели данных, поэтому эти операции должны быть реализованы четырьмя различными методами. Пятый метод, который мы хотим добавить, — это возможность получить список записей.

Чтобы абстрагировать эти операции, давайте объявим новый тип интерфейса IToDoData, который будет определять, какую функциональность должен предлагать наш модуль данных:

type
  IToDoData = interface
    function ToDoCreate(AValue: TToDo): Integer;
    function ToDoRead(Id: Integer; out AValue: TToDo): Boolean;
    function ToDoUpdate(AValue: TToDo): Boolean;
    function ToDoDelete(Id: Integer): Boolean;
    procedure ToDoList(AList: TToDos);
  end;

Это довольно распространённый способ представления CRUD-операций. Функция ToDoCreate принимает элемент TToDo, при необходимости проверяет корректность данных и возвращает идентификатор нового элемента в базовом хранилище данных. Если операция не удалась, она возвращает специальное значение, например, -1.

Идентификатор для нового элемента данных может быть сгенерирован базой данных или создан в коде.

Выбор базы данных

Как упоминалось ранее, большинство мобильных приложений работают с данными. Если вы не создаёте приложение калькулятора или аркадную игру, возможность хранить данные, которые использует ваше приложение, может быть очень ценной. При проектировании приложения вам нужно заранее сделать некоторые архитектурные выборы. Где и как хранятся мои данные? Одно очевидное решение — хранить данные в облаке. Этот подход является наиболее распространённым, но что делать, если у вас нет доступа к сети? Вы всё равно хотите иметь возможность работать с приложением и с последней версией ваших данных. Это главная причина, по которой вы также хотите хранить данные локально на устройстве.

В простых случаях хранение данных в виде обычных файлов, текста или бинарных данных достаточно. Однако также возможно и, как правило, лучше встроить полную систему базы данных локально на устройство. Существует множество реляционных и нереляционных встроенных баз данных. Если мы подумаем о произвольной базе данных как о ещё одном мобильном фреймворке, доступном из мобильной операционной системы, то должно быть возможно интегрировать ваше мобильное приложение Delphi практически с любой базой данных. Однако это может потребовать глубокого понимания внутренней работы вашей конкретной базы данных. В большинстве случаев мы просто хотим выполнить работу, и наиболее продуктивным подходом является использование баз данных, для которых Delphi уже предоставляет поддержку. Ключевым фреймворком доступа к базам данных для мобильных приложений Delphi является FireDAC. Уровень доступа к данным FireDAC обеспечивает подключение ко всем основным базам данных, а также включает поддержку работы со встроенными базами данных. Есть две основные базы данных, которые вы можете встроить в мобильное устройство:

Настройка развертывания приложения на базе InterBase ToGo
Рисунок 10.3: Настройка развертывания приложения на базе InterBase ToGo

Доступ к базам данных с помощью FireDAC

Чтобы сохранить демо-приложение простым, мы будем использовать SQLite в качестве движка базы данных, и наше приложение ToDo List будет иметь только одну таблицу базы данных ToDos.

  1. Поместите компонент TFDConnection на модуль данных. Измените его свойство Name на FDConnToDos.
  2. Дважды щёлкните на компоненте соединения, чтобы отобразить окно FireDAC Connection Editor. Выберите SQLite в комбинированном списке Driver ID.
  3. Введите имя файла базы данных. На вашей Windows-машине создайте каталог, например, C:\Users\Public\Documents\. Имя файла базы данных будет ToDos.db.
  4. Оставьте значения параметров по умолчанию. Значение OpenMode по умолчанию установлено в CreateUTF8.
  5. Нажмите кнопку Test. Вы должны получить сообщение об успешном подключении.
Тестирование соединения SQLite в FireDAC Connection Editor
Рисунок 10.4: Тестирование соединения SQLite в FireDAC Connection Editor

Нажмите на вкладку SQL Script и введите следующий код для создания таблицы:

CREATE TABLE IF NOT EXISTS ToDos(
  Id INTEGER NOT NULL PRIMARY KEY,
  Title TEXT,
  Category TEXT
)

Нажмите на зелёную кнопку со стрелкой, чтобы выполнить запрос.

Запуск SQL-скрипта для создания таблицы ToDos
Рисунок 10.5: Запуск SQL-скрипта для создания таблицы ToDos

Следующие шаги добавляют инициализацию базы данных и подключение к нашему приложению:

  1. Измените свойство LoginPrompt компонента соединения на False.
  2. Дважды щёлкните на событии OnBeforeConnect, чтобы указать расположение динамически:
function TDMToDo.IsMobilePlatform: Boolean;
begin
  Result := (TOSVersion.Platform = pfiOS) or 
            (TOSVersion.Platform = pfAndroid);
end;

procedure TDMToDo.FDConnToDosBeforeConnect(Sender: TObject);
begin
  if IsMobilePlatform then
    FDConnToDos.Params.Values['Database'] := 
      TPath.Combine(TPath.GetDocumentsPath, 'ToDos.db');
end;

Добавьте обработчик события OnAfterConnect для выполнения SQL-кода:

procedure TDMToDo.FDConnToDosAfterConnect(Sender: TObject);
const
  SCreateTableSQL = 
    'CREATE TABLE IF NOT EXISTS ToDos(' +
    'Id INTEGER NOT NULL PRIMARY KEY,' +
    'Title TEXT, Category TEXT)';
begin
  if IsMobilePlatform then
    FDConnToDos.ExecSQL(SCreateTableSQL);
end;

Добавьте IToDoData в объявление класса TDMToDo и нажмите Ctrl+Shift+C для генерации пустых реализаций методов.

Поместите шесть компонентов TFDQuery на форму и измените их имена соответственно на FdqToDoMaxId, FdqToDoInsert, FdqToDoSelect, FdqToDoUpdate, FdqToDoDelete и FdqToDoSelectAll.

Компоненты FireDAC на модуле данных
Рисунок 10.6: Компоненты FireDAC на модуле данных

Реализуйте приватную функцию GetNewId:

function TDMToDo.GetNewId: Integer;
begin
  FdqToDoMaxId.Open;
  try
    var Fld := FdqToDoMaxId.FieldByName('MaxId');
    if Fld.IsNull then
      Result := 1
    else
      Result := Fld.AsInteger + 1;
  finally
    FdqToDoMaxId.Close;
  end;
end;
SQL-запрос для получения максимального значения Id
Рисунок 10.7: SQL-запрос для получения максимального значения Id

Настройте FdqToDoInsert и реализуйте ToDoCreate:

function TDMToDo.ToDoCreate(AValue: TToDo): Integer;
begin
  var Id := GetNewId;
  FdqToDoInsert.ParamByName('Id').AsInteger := Id;
  FdqToDoInsert.ParamByName('Title').AsString := AValue.Title;
  FdqToDoInsert.ParamByName('Category').AsString := AValue.Category;
  try
    FdqToDoInsert.ExecSQL;
    Result := Id;
  except
    Result := -1;
  end;
end;
Параметризованный SQL-запрос для вставки новой записи
Рисунок 10.8: Параметризованный SQL-запрос для вставки новой записи
Управление параметрами в FireDAC Query Editor
Рисунок 10.9: Управление параметрами в FireDAC Query Editor

Реализуйте ToDoRead:

function TDMToDo.ToDoRead(Id: Integer; out AValue: TToDo): Boolean;
begin
  FdqToDoSelect.ParamByName('Id').AsInteger := Id;
  FdqToDoSelect.Open;
  try
    if FdqToDoSelect.RecordCount > 0 then
    begin
      Result := True;
      AValue.Id := Id;
      AValue.Title := FdqToDoSelect.FieldByName('Title').AsString;
      AValue.Category := FdqToDoSelect.FieldByName('Category').AsString;
    end
    else
      Result := False;
  finally
    FdqToDoSelect.Close;
  end;
end;

Реализуйте ToDoUpdate:

function TDMToDo.ToDoUpdate(AValue: TToDo): Boolean;
begin
  FdqToDoUpdate.ParamByName('Id').AsInteger := AValue.Id;
  FdqToDoUpdate.ParamByName('Title').AsString := AValue.Title;
  FdqToDoUpdate.ParamByName('Category').AsString := AValue.Category;
  try
    FdqToDoUpdate.ExecSQL;
    Result := True;
  except
    Result := False;
  end;
end;

Реализуйте ToDoDelete:

function TDMToDo.ToDoDelete(Id: Integer): Boolean;
begin
  FdqToDoDelete.ParamByName('Id').AsInteger := Id;
  try
    FdqToDoDelete.ExecSQL;
    Result := True;
  except
    Result := False;
  end;
end;

Реализуйте ToDoList:

procedure TDMToDo.ToDoList(AList: TToDos);
var
  Item: TToDo;
begin
  if AList <> nil then
  begin
    AList.Clear;
    FdqToDoSelectAll.Open;
    try
      while not FdqToDoSelectAll.Eof do
      begin
        Item.Id := FdqToDoSelectAll.FieldByName('Id').AsInteger;
        Item.Title := FdqToDoSelectAll.FieldByName('Title').AsString;
        Item.Category := FdqToDoSelectAll.FieldByName('Category').AsString;
        AList.Add(Item);
        FdqToDoSelectAll.Next;
      end;
    finally
      FdqToDoSelectAll.Close;
    end;
  end;
end;

Построение UI, управляемого данными

Теперь мы можем построить GUI приложения. Логика доступа к данным доступна через интерфейс IToDoData. Мы реализуем метод GetToDoData в модуле главной формы:

uses uDMToDo;

function TFormToDo.GetToDoData: IToDoData;
begin
  if DMToDo = nil then
    DMToDo := TDMToDo.Create(Application);
  Result := DMToDo;
end;

Поместите компонент TTabControl на форму, выровняйте его по Client и переименуйте в TbctrlMain. Добавьте две вкладки. Переименуйте их в TbiList и TbiEdit. Измените свойство TabPosition элемента управления вкладками на None.

Поместите компонент TActionList и добавьте два ChangeTabActions (CtaList и CtaEdit).

Реализуйте FormCreate и FormDestroy:

procedure TFormToDo.FormCreate(Sender: TObject);
begin
  FToDos := TToDos.Create;
  TbctrlMain.ActiveTab := TbiList;
  RefreshList;
end;

procedure TFormToDo.FormDestroy(Sender: TObject);
begin
  FToDos.Free;
end;

Поместите компонент TListView на первую вкладку, переименуйте его в LstvwToDos и выровняйте по Client.

Первая вкладка формы приложения ToDoList
Рисунок 10.10: Первая вкладка формы приложения ToDoList

Измените Appearance узла Item на DynamicAppearance. Добавьте текстовые объекты с именами Title и Category, а также аксессуар с именем More.

Структура TListView приложения ToDoList
Рисунок 10.11: Структура TListView приложения ToDoList
Внешний вид TListView в режиме проектирования
Рисунок 10.12: Внешний вид TListView в режиме проектирования

Реализуйте метод RefreshList:

procedure TFormToDo.RefreshList;
begin
  GetToDoData.ToDoList(FToDos);
  LstvwToDos.BeginUpdate;
  try
    LstvwToDos.Items.Clear;
    for var Todo in FToDos do
    begin
      var Item := LstvwToDos.Items.Add;
      Item.Tag := Todo.Id;
      Item.Objects.FindObjectT<TListItemText>('Title').Text := Todo.Title;
      Item.Objects.FindObjectT<TListItemText>('Category').Text := Todo.Category;
    end;
  finally
    LstvwToDos.EndUpdate;
  end;
end;

Добавьте элементы управления UI для редактирования (Edits, Labels, SpeedButtons) и реализуйте Actions:

Вторая вкладка главной формы приложения Todo
Рисунок 10.13: Вторая вкладка главной формы приложения Todo
procedure TFormToDo.ActAddExecute(Sender: TObject);
begin
  FCurrentId := -1;
  EdtTitle.Text := '';
  EdtCategory.Text := '';
  CtaEdit.ExecuteTarget(self);
end;

procedure TFormToDo.ActSaveExecute(Sender: TObject);
var
  Todo: TToDo;
begin
  Todo.Title := EdtTitle.Text;
  Todo.Category := EdtCategory.Text;
  if FCurrentId < 0 then
    GetToDoData.ToDoCreate(Todo)
  else
  begin
    Todo.Id := FCurrentId;
    GetToDoData.ToDoUpdate(Todo);
  end;
  RefreshList;
  CtaList.ExecuteTarget(self);
end;

procedure TFormToDo.ActDeleteExecute(Sender: TObject);
begin
  if FCurrentId > 0 then
  begin
    GetToDoData.ToDoDelete(FCurrentId);
    RefreshList;
  end;
  if TbctrlMain.ActiveTab <> TbiList then
    CtaList.ExecuteTarget(self);
end;

procedure TFormToDo.LstvwToDosDeleteItem(
  Sender: TObject; AIndex: Integer);
begin
  FCurrentId := FToDos[AIndex].Id;
  ActDelete.Execute;
end;

procedure TFormToDo.LstvwToDosItemClick(
  const Sender: TObject; const AItem: TListViewItem);
var
  Todo: TToDo;
begin
  FCurrentId := AItem.Tag;
  GetToDoData.ToDoRead(FCurrentId, Todo);
  EdtTitle.Text := Todo.Title;
  EdtCategory.Text := Todo.Category;
  CtaEdit.ExecuteTarget(self);
end;
Первая вкладка приложения Todo, запущенного на Windows
Рисунок 10.14: Первая вкладка приложения Todo, запущенного на Windows
Вторая вкладка приложения Todo, запущенного на Android
Рисунок 10.15: Вторая вкладка приложения Todo, запущенного на Android

Использование визуального связывания (Live Bindings)

В FireMonkey нет концепции data-aware элементов управления, как в VCL. Если вы хотите быстро создавать GUI, управляемые данными, в FireMonkey, вы можете использовать визуальное связывание (visual live bindings). Эта технология позволяет связывать свойства любых двух объектов.

Связывание поля ввода и метки в LiveBindings Designer
Рисунок 10.16: Связывание поля ввода и метки в LiveBindings Designer
Редактор компонента BindingsList
Рисунок 10.17: Редактор компонента BindingsList
Первый экран мастера Delphi LiveBindings Wizard
Рисунок 10.18: Первый экран мастера Delphi LiveBindings Wizard

Чтобы запустить обновление связанных элементов управления вручную через код (например, при нажатии клавиши):

procedure TFormVLB.Edit1KeyUp(
  Sender: TObject; var Key: Word; var KeyChar: Char; 
  Shift: TShiftState);
begin
  BindingsList1.Notify(Edit1, 'Text');
end;
Приложение VLBTest на Windows
Рисунок 10.19: Приложение VLBTest на Windows

Быстрое прототипирование UI

Один из действительно крутых компонентов, которые вы можете использовать для быстрого проектирования UI, управляемого данными, — это TPrototypeBindSource. Этот компонент может эмулировать таблицу данных и генерировать тестовые данные для отображения.

Добавьте компонент TPrototypeBindSource. Добавьте поля, такие как BitmapNames и ColorNames, переименуйте их в TestTitle и TestCategory.

Диалоговое окно Add Field для компонента PrototypeBindSource
Рисунок 10.20: Диалоговое окно Add Field для компонента PrototypeBindSource
Поля, добавленные в компонент PrototypeBindSource1
Рисунок 10.21: Поля, добавленные в компонент PrototypeBindSource1
Visual LiveBindings, связывающий list view с prototype bind source
Рисунок 10.22: Visual LiveBindings, связывающий list view с prototype bind source
List view, заполненный прототипными данными во время проектирования
Рисунок 10.23: List view, заполненный прототипными данными во время проектирования

Резюме

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

В следующей главе мы повторно используем визуальную часть нашего приложения ToDo List и будем использовать облачное хранилище данных в качестве базовой реализации интерфейса IToDoData.