Глава 10. Встраиваемые базы данных
Большинство мобильных приложений работают с данными. Создание приложений баз данных всегда было одной из сильнейших сторон Delphi. В этой главе мы узнаем, как создавать мобильные пользовательские интерфейсы (UI), управляемые данными, в FireMonkey, как использовать фреймворк доступа к базам данных FireDAC и как встраивать базы данных в мобильные устройства.
Хотя мобильные приложения полагаются на данные из интернета, создание быстрого и отзывчивого приложения всегда требует локального кэширования некоторых данных. По этой причине использование базы данных в мобильном приложении очень важно, и эта глава поможет вам понять, как это сделать. В качестве примера мы создадим простое мобильное приложение для управления списком задач.
Технические требования
Исходный код примеров, рассмотренных в этой главе, можно найти в репозитории на GitHub.
Приложения, управляемые данными
В этой главе мы пройдём через этапы создания простого мобильного приложения со встроенной базой данных для управления списком задач. Прежде чем перейти к написанию кода, давайте сначала рассмотрим общую архитектуру приложения. Чем сложнее система, которую мы хотим построить, тем важнее правильно её структурировать.
Типичный подход — разделяй и властвуй: разбить большую проблему на более мелкие, которые проще решить. Наиболее распространённый подход в разработке программного обеспечения — разбить всю систему на чётко разделённые уровни (tiers). В приложении, управляемом данными, мы должны иметь возможность выделить как минимум две логические части: UI и логику доступа к данным. Чёткое разделение этих двух уровней позволяет создать плагинную архитектуру, где UI может подключаться к различным блокам доступа к данным стандартным способом, и UI может быть заменён без изменения базового уровня доступа к данным.
В контексте приложения Delphi мы можем разделить наш проект на три независимые сущности, как показано на рисунке 10.1:
- Одна или несколько визуальных форм, которые будут служить графическим интерфейсом (GUI)
- Один или несколько модулей данных с невизуальными компонентами для работы с базой данных
- Один или несколько автономных модулей с общими типами и вспомогательными функциями, используемыми обоими уровнями приложения
Теперь давайте построим эту структуру в Delphi:
- Создайте новый пустой мультиплатформенный проект Delphi. Сохраните модуль главной формы как
uFormToDo, а всё приложение какToDoList. Измените свойство Name формы наFormToDo. - Выберите File | New | Data Module в главном меню Delphi. Измените свойство Name модуля данных на
DMToDoи сохраните его какuDMToDo. - Теперь выберите File | New | Unit в главном меню Delphi, чтобы создать пустой модуль для проекта. Сохраните его как
uToDoTypes.
Наш проект состоит из трёх различных модулей, которые представляют три основных строительных блока нашего приложения, управляемого данными. Теперь давайте создадим зависимости между этими тремя модулями. Модуль 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 — SQL реляционная система управления базами данных от Embarcadero. Она имеет очень лёгкий след, само настраивается и почти не требует администрирования. Варианты включают IBLite (бесплатно, до 100 МБ) и IBToGo (коммерческий, без ограничений, шифрование).
- SQLite — более простая, общественная реляционная база данных. Развёртывание очень простое, потому что сам движок уже установлен как на iOS, так и на Android.
Доступ к базам данных с помощью FireDAC
Чтобы сохранить демо-приложение простым, мы будем использовать SQLite в качестве движка базы данных, и наше приложение ToDo List будет иметь только одну таблицу базы данных ToDos.
- Поместите компонент
TFDConnectionна модуль данных. Измените его свойство Name наFDConnToDos. - Дважды щёлкните на компоненте соединения, чтобы отобразить окно FireDAC Connection Editor. Выберите SQLite в комбинированном списке Driver ID.
- Введите имя файла базы данных. На вашей Windows-машине создайте каталог, например,
C:\Users\Public\Documents\. Имя файла базы данных будетToDos.db. - Оставьте значения параметров по умолчанию. Значение
OpenModeпо умолчанию установлено вCreateUTF8. - Нажмите кнопку Test. Вы должны получить сообщение об успешном подключении.
Нажмите на вкладку SQL Script и введите следующий код для создания таблицы:
CREATE TABLE IF NOT EXISTS ToDos(
Id INTEGER NOT NULL PRIMARY KEY,
Title TEXT,
Category TEXT
)
Нажмите на зелёную кнопку со стрелкой, чтобы выполнить запрос.
Следующие шаги добавляют инициализацию базы данных и подключение к нашему приложению:
- Измените свойство
LoginPromptкомпонента соединения наFalse. - Дважды щёлкните на событии
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.
Реализуйте приватную функцию 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;
Настройте 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;
Реализуйте 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.
Измените Appearance узла Item на DynamicAppearance. Добавьте текстовые объекты с именами Title и Category, а также аксессуар с именем More.
Реализуйте метод 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:
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;
Использование визуального связывания (Live Bindings)
В FireMonkey нет концепции data-aware элементов управления, как в VCL. Если вы хотите быстро создавать GUI, управляемые данными, в FireMonkey, вы можете использовать визуальное связывание (visual live bindings). Эта технология позволяет связывать свойства любых двух объектов.
Чтобы запустить обновление связанных элементов управления вручную через код (например, при нажатии клавиши):
procedure TFormVLB.Edit1KeyUp(
Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
begin
BindingsList1.Notify(Edit1, 'Text');
end;
Быстрое прототипирование UI
Один из действительно крутых компонентов, которые вы можете использовать для быстрого проектирования UI, управляемого данными, — это TPrototypeBindSource. Этот компонент может эмулировать таблицу данных и генерировать тестовые данные для отображения.
Добавьте компонент TPrototypeBindSource. Добавьте поля, такие как BitmapNames и ColorNames, переименуйте их в TestTitle и TestCategory.
Резюме
В этой главе мы узнали, как создавать приложения, управляемые данными, и встраивать базы данных в мобильные устройства. Мы рассмотрели создание базы данных, построение уровня отображения данных и создание связанного с ним GUI. Доступ к базе данных является критически важной функцией для большинства приложений. База данных также может быть локальным кэшем удалённой базы данных, к которой вы можете получить доступ с помощью различных методов для создания облачных клиентов или многоуровневых архитектур, которые являются темами следующих глав.
В следующей главе мы повторно используем визуальную часть нашего приложения ToDo List и будем использовать облачное хранилище данных в качестве базовой реализации интерфейса IToDoData.