Создание приложений для СУБД Firebird с использованием различных компонент и драйверов: FireDac
В данной статье будет описан процесс создания приложений для СУБД Firebird с использованием компонентов доступа FireDac и среды Delphi XE5. FireDac является стандартным набором компонентов доступа к различным базам данных начиная с Delphi XE3.
Наше приложение будет работать с базой данных модель, которой представлена на рисунке ниже.
В конце данной статьи приведены ссылки на другие статьи, в которых описывается процесс создания базы данных с этой моделью и ссылка на скрипт создания базы данных.
Внимание! Эта модель является просто примером. Ваша предметная область может быть сложнее, или полностью другой. Модель, используемая в этой статье, максимально упрощена для того, чтобы не загромождать описание работы с компонентами описанием создания и модификации модели данных. |
Создайте новый проект File→New→VCL Forms Application — Delphi. В новый проект добавьте новый дата модуль File→New→Other, в появившемся мастере выберите Delphi Projects→Delphi Files→Data Module. Этот дата модуль будет главным в нашем проекте. Он будет содержать некоторые экземпляры глобальных компонентов доступа, которые должны быть доступны всем формам, которые должны работать с данными. Например, таким компонентом является TFDConnection.
Компонент TFDConnection
Компонент TFDConnection обеспечивает подключение к различным типам баз данных. Будем указывать экземпляр этого компонента в свойствах Connection остальных компонентов FireDac. К какому именно типу баз данных будет происходить подключение, зависит от значения свойства DriverName. Для доступа к Firebird нам необходимо выставить это свойство в значение FB. Для того чтобы подключение знало, с какой именно библиотекой доступа необходимо работать, разместим в главном дата модуле компонент TFBPhysFBDriverLink. Его свойство VendorLib позволяет указывать путь до клиентской библиотеки. Если оно не указано, то подключение к Firebird будет осуществляться через библиотеки, зарегистрированные в системе, например в system32, что в ряде случаев может быть нежелательно.
Путь к клиентской библиотеке
Мы будем размещать необходимую библиотеку доступа в папке fbclient, которая расположена в папке приложения. Для этого в коде на событие OnCreate дата модуля пропишем следующий код.
// указываем путь до клиентской библиотеки
xAppPath := ExtractFileDir(Application.ExeName) + PathDelim;
FDPhysFBDriverLink.VendorLib := xAppPath + 'fbclient' + PathDelim + 'fbclient.dll';
Важно! Если вы компилируете 32 разрядное приложение, то вы должны использовать 32 разрядную библиотеку fbclient.dll. Для 64 разрядного — 64 разрядную. Помимо файла fbclient.dll в ту же папку желательно поместить библиотеки msvcp80.dll и msvcr80.dll (для Firebird 2.5), и msvcp100.dll и msvcr100.dll (для Firebird 3.0). Эти библиотеки можно найти либо в подпапке bin (Firebird 2.5), либо в корневой папке сервера (Firebird 3.0). Для того чтобы приложение правильно отображало собственные ошибки firebird, необходимо также скопировать файл firebird.msg. Для Firebird 2.5 и в более ранних версиях он должен находиться на один уровень выше каталога клиентской библиотеки, т.е. в нашем случае в каталоге приложения. Для Firebird 3 он должен находиться в каталоге клиентской библиотеки, т.е. в каталоге fbclient. |
Если вам необходимо чтобы ваше приложение работало без установленного сервера Firebird, т.е. в режиме Embedded, то для Firebird 2.5 необходимо заменить fbclient.dll на fbembed.dll. При желании имя библиотеки можно вынести в конфигурационный файл вашего приложения. Для Firebird 3.0 ничего изменять не требуется (режим работы зависит от строки подключения и значения параметра Providers в файле firebird.conf/databases.conf).
Совет Даже если ваше приложение будет работать с Firebird в режиме Embedded, разработку удобнее вести под полноценным сервером. Дело в том, что в режиме Embedded Firebird работает в одном адресном пространстве с вашим приложением, что может привести к нежелательным последствиям при возникновении ошибок в вашем приложении. Кроме того, в момент разработки среда Delphi и ваше приложение являются отдельными приложениями, использующими Embedded. До версии 2.5 они не могут работать с одной базой одновременно. |
Параметры подключения
Компонент TFDConnection параметры подключения к базе данных содержатся в свойстве Params (имя пользователя, пароль, набор символов соединения и др.). Если воспользоваться редактором свойств TFDConnection (двойной клик на компоненте), то упомянутые свойства будут заполнены автоматически. Набор этих свойств зависит от типа базы данных.
Параметр | Назначение |
---|---|
Pooled | Используется ли пул соединений. |
Database | Путь к базе данных или её псевдоним, определённый в файле конфигурации aliases.conf (или databases.conf) сервера Firebird. |
User_Name | Имя пользователя. |
Password | Пароль. |
OSAuthent | Используется ли аутентификация средствами операционной системы. |
Protocol | Протокол соединения. Допускаются следующие значения:
|
Server | Имя сервера или его IP адрес. Если сервер работает на нестандартном порту, то необходимо также указать порт через слэш, например localhost/3051. |
SQLDialect | Диалект. Должен совпадать с диалектом базы данных. |
RoleName | Имя роли. |
CharacterSet | Имя набора символов соединения. |
Дополнительные свойства:
Connected — управление подсоединением к БД, или проверка состояния соединения. Это свойство должно быть выставлено в True для работы мастеров других компонентов FireDac. Если ваше приложение должно запрашивать данные для авторизации, то важно не забыть сбросить это свойство в False перед компиляцией вашего приложения.
LoginPrompt — запрашивать ли имя пользователя и пароль при попытке соединения.
Transaction — компонент TFDTransaction, который будет использоваться в качестве умолчательного для выполнения различных операций TFDConnection. Если это свойство не назначено явно, TFDConnection создаст себе экземпляр TFDTransaction самостоятельно, его параметры можно указать в свойстве TxOptions.
UpdateTransaction — компонент TFDTransaction, который будет использоваться в качестве умолчательного для одноимённых свойств компонентов TFDQuery. Если это свойство не назначено явно, будет использовано значение из свойства Transaction.
Поскольку параметры подключения, за исключением имени пользователя и пароля, обычно не изменяются в процессе эксплуатации приложения, мы будем считывать их из файла конфигурации.
// считываем параметры подключения
xIniFile := TIniFile.Create(xAppPath + 'config.ini');
try
xIniFile.ReadSectionValues('connection', FDConnection.Params);
finally
xIniFile.Free;
end;
Файл config.ini содержит примерно следующие строки:
[connection]
DriverID=FB
Protocol=TCPIP
Server=localhost/3051
Database=examples
OSAuthent=No
RoleName=
CharacterSet=UTF8
Содержимое секции connection можно получить, скопировав содержимое свойства Params компонента TFDConnection после работы мастера.
Замечание На самом деле общие настройки обычно находятся в %AppData%\Manufacture\AppName и сохраняются туда инсталятором приложения. Однако при разработке удобно чтобы файл настроек лежал где-нибудь поближе, например, в папке с приложением. Учтите, что если ваше приложение будет устанавливаться в папку Program Files и файл настройки будет лежать там же, то либо этот файл будет виртуализироваться в Program Data, либо будут проблемы с его модификацией и последующим чтением новых настроек. |
Подключение к базе данных
Для подключения к базе данных необходимо изменить свойство Connected компонента TFDConnection в значение True или вызвать метод Open. В последний метод можно передать имя пользователя и пароль в качестве параметров. В нашем приложении мы заменим стандартный диалог соединения с базой данных. Дадим возможность ошибиться при вводе регистрационной информации не более трёх раз, после чего приложение будет закрыто. Для этого напишем следующий код в обработчике события OnCreate главного датамодуля.
// делаем максимум 3 попытки входа в систему, потом закрываем приложение
xLoginCount := 0;
xLoginPromptDlg := TLoginPromptForm.Create(Self);
while (xLoginCount < MAX_LOGIN_COUNT) and
(not FDConnection.Connected) do
begin
try
if xLoginPromptDlg.ShowModal = mrOK then
FDConnection.Open(
xLoginPromptDlg.UserName, xLoginPromptDlg.Password)
else
xLoginCount := MAX_LOGIN_COUNT;
except
on E: Exception do
begin
Inc(xLoginCount);
Application.ShowException(E);
end
end;
end;
xLoginPromptDlg.Free;
if not FDConnection.Connected then
Halt;
Компонент TFDTransaction
Компонент TFDTransaction предназначен для явной работы с транзакциями.
Клиентская часть Firebird допускает выполнение любых действий только в контексте транзакции. Поэтому если вы смогли получить доступ к данным без явного вызова TFDTransaction.StartTransaction, то значит где-то в недрах FireDac этот вызов произошёл автоматически. Такое поведение крайне не рекомендуется использовать. Для корректной работы приложений с базой данных желательно управлять транзакциями вручную, то есть явно вызывать методы StartTransaction, Commit и Rollback компонента TFDTransaction.
Параметр | Назначение |
---|---|
Connection | Связь с компонентом FDConnection. |
Options.AutoCommit | Управляет автоматическим стартом и завершением транзакции. Значение по умолчанию True. Если значение этого свойства установлено в True, то FireDAC делает следующее:
В Firebird автоматическое управление транзакциями эмулируется самими компонентами FireDAC. |
Options.AutoStart | Управляет автоматическим запуском транзакции. По умолчанию True. |
Options.AutoStop | Управляет автоматическим завершением транзакции. По умолчанию True. |
Options.DisconnectAction | Действие, которое будет выполнено при закрытии соединения, если транзакция активна. Значение по умолчанию xdCommit. Возможны следующие варианты:
В других компонентах доступа значение по умолчанию для подобного свойства xdRollback. Поэтому необходимо выставлять это свойство вручную в то значение которое действительно требуется. |
Options.EnableNested | Управляет вложенными транзакциями. Значение по умолчанию True. Когда транзакция активна, то следующий вызов StartTransaction создаст вложенную транзакцию. FireDAC эмулирует вложенные транзакции, используя точки сохранения, если СУБД не поддерживает вложенные транзакции в явном виде. Чтобы отключить вложенные транзакции, установите EnableNested в False и следующий вызов StartTransaction вызовет исключение. Firebird не поддерживает вложенные транзакции в явном виде. |
Options.Isolation | Определяет уровень изолированности транзакции. Это самое важное свойство транзакции. Значение по умолчанию xiReadCommited. Возможны следующие варианты:
|
Options.Params | Специфичные для СУБД параметры транзакции. В настоящее время используется только для Firebird и Interbase. Возможные значения:
|
Options.ReadOnly | Указывает является ли транзакция только для чтения. По умолчанию False. Если установлено в True, то любые изменения в рамках текущей транзакции невозможны, в Firebird в этом случае отсутствует значение read в параметрах транзакции. Установка этого свойства в True позволяет СУБД оптимизировать использование ресурсов. |
В отличие от других СУБД в Firebird разрешено использовать сколько угодно компонентов TFDTransaction привязанных к одному соединению. В нашем приложении мы будем использовать одну общую читающую транзакцию для всех справочников и оперативных журналов, и по одной пишущей транзакции на каждый справочник/журнал.
В нашем приложении мы не будем полагаться на автоматический старт и завершение транзакций, а потому во всех транзакциях Options.AutoCommit = False, Options.AutoStart = False и Options.AutoStop = False.
Поскольку читающая транзакция общая для всех справочников и журналов, то удобно разместить её в главном дата модуле. Для обычной работы (показ данных в гриде и т.п.) обычно используются режим изолированности READ COMMITED (Options.Isolation = xiReadCommited), т.к. он позволяет транзакции видеть чужие, committed изменения базы данных просто путём повторного выполнения запросов (перечитывания данных). Поскольку эта транзакция используется только для чтения, установим свойство Options.ReadOnly в значение True. Таким образом, наша транзакция будет иметь параметры read read_commited rec_version. Транзакция с такими параметрами в Firebird может быть открытой сколь угодно долгое время (дни, недели, месяцы), без блокирования других транзакций или влияния на накопление мусора в базе данных (потому что на самом деле, на сервере такая транзакция стартует как committed).
Замечание Такую транзакцию нельзя использовать для отчётов (особенно если они используют несколько последовательных запросов), потому что транзакция с режимом изолированности READ COMMITED во время перечитывания данных будет видеть все новые committed-изменения. Для отчётов рекомендуется использовать короткую транзакцию только для чтения с режимом изолированности SNAPSHOT (Options.Isolation = xiSnapshot и Options.ReadOnly= True). В данном примере работа с отчётами не рассматривается. |
Стартуем читающую транзакцию сразу после успешной установки соединения с базой данных, вызвав trRead.StartTransaction в событии OnCreate главного датамодуля, и завершаем перед закрытием соединения, вызвав tRead.Commit в событии OnDestroy главного датамодуля. Значение свойства Options.DisconnectAction равное xdCommit по умолчанию, подходит для транзакции только для чтения.
Пишущая транзакция будет отдельной для каждого справочника/журнала. Её мы разместим на форме, которая относится непосредственно к нужному журналу. Пишущая транзакция должна быть максимально короткой для того, чтобы не удерживать Oldest Active Transaction, которая не даёт собрать мусор, что в свою очередь приводит к деградации производительности. Поскольку пишущая транзакция очень короткая мы можем использовать уровень изолированности SNAPSHOT. Таким образом, наша пишущая транзакция будет иметь параметры Options.ReadOnly=False и Options.Isolation = xiSnapshot. Для пишущих транзакций значение свойства Options.DisconnectAction по умолчанию не подходит, его необходимо выставить в значение xdRollback.
Датасеты
Работать с данными в FireDac можно при помощи компонент FDQuery, FDTable, FDStoredProc, FDCommand, но FDCommand не является датасетом.
TFDQuery, TFDTable и TFDStoredProc унаследованы от TFDRdbmsDataSet. Помимо наборов данных для работы непосредственно с базой данных, в FireDac существует также компонент TFDMemTable, который предназначен для работы с набором данных в памяти, является аналогом TClientDataSet.
Основным компонентом для работы с наборами данных является TFDQuery. Возможностей этого компонента хватает практически для любых целей. Компоненты TFDTable и TFDStoredProc всего лишь модификации, либо чуть расширенные, либо усеченные. Мы не будем их рассматривать и применять в нашем приложении. При желании вы можете ознакомиться с ними в документации по FireDac.
Назначение компонента — буферизация записей, выбираемых оператором SELECT, для представления этих данных в Grid, а также для обеспечения «редактируемости» записи (текущей в буфере (гриде)). В отличие от компонента IBX.IBDataSet компонент FDQuery не содержит свойств RefreshSQL, InsertSQL, UpdateSQL и DeleteSQL. Вместо этого «редактируемость» обеспечивается компонентом FDUpdateSQL, который устанавливается в свойство UpdateObject.
Замечание В ряде случаев можно сделать компонент FDQuery редактируемым без установки свойства UpdateObject и прописывания запросов Insert/Update/Delete, просто установив свойство UpdateOptions.RequestLive = True, при этом модифицирующие запросы будут сгенерированы автоматически. Однако такой подход имеет множество ограничений на основной SELECT запрос, поэтому не стоит полагаться на него. |
Параметр | Назначение |
---|---|
Connection | Связь с компонентом FDConnection. |
MasterSource | Ссылка на Master-источник данных (TDataSource) для FDQuery, используемого в качестве Detail. |
Transaction | Транзакция, в рамках которой будет выполняться запрос, прописанный в свойстве SQL. Если свойство не указано будет использоваться транзакция по умолчанию для подключения. |
UpdateObject | Связь с компонентом FDUpdateSQL, который обеспечивает «редактируемость» набора данных, когда SELECT запрос не отвечает требованиям для автоматического формирования модифицирующих запросов при установке UpdateOptions.RequestLive = True. |
UpdateTransaction | Транзакция, в рамках которой будут выполняться модифицирующие запросы. Если свойство не указано, будет использована транзакция из свойства Transaction. |
UpdateOptions.CheckRequired | Если свойство CheckRequired установлено в True, то FireDac контролирует свойство Required соответствующих полей, т.е. полей с ограничением NOT NULL. По умолчанию установлено в True. Если CheckRequired=True и в поле имеющее свойство Required=True не присвоено значение, то при вызове метода Post будет возбуждено исключение. Это может быть нежелательно в том случае, если значение этого поля может быть присвоено позже в BEFORE триггерах. |
UpdateOptions.EnableDelete | Определяет, позволяется ли удаление записи из набора данных. Если EnableDelete=False, то при вызове метода Delete будет возбуждено исключение. |
UpdateOptions.EnableInsert | Определяет, позволяется ли вставка записи в набор данных. Если EnableInsert=False, то при вызове метода Insert/Append будет возбуждено исключение. |
UpdateOptions.EnableUpdate | Определяет, позволяется ли изменение записи в наборе данных. Если EnableUpdate=False, то при вызове метода Edit будет возбуждено исключение. |
UpdateOptions.FetchGeneratorsPoint | Управляет моментом получения следующего значения генератора указанного в свойстве UpdateOptions.GeneratorName или свойстве GeneratorName автоинкрементного поля AutoGenerateValue = arAutoInc. Имеет следующие варианты значений:
Значение по умолчанию gpDeffered. |
UpdateOptions.GeneratorName | Имя генератора для извлечения следующего значения автоинкрементного поля. |
UpdateOptions.ReadOnly | Указывает, является ли набор данных только для чтения. По умолчанию False. Если значение этого свойства установлено в True, то значения свойств EnableDelete, EnableInsert и EnableUpdate будут автоматически выставлены в False. |
UpdateOptions.RequestLive | Установка RequestLive в True делает запрос «живым», т.е. редактируемым, если это возможно. При этом запросы Insert/Update/Delete будут сгенерированы автоматически. Эта опция накладывает множество ограничений на SELECT запрос, введена для обратной совместимости с BDE и не рекомендуется. |
UpdateOptions.UpdateMode | Отвечает за проверку модификации записи. Это свойство позволяло контролировать возможное «перекрытие» обновлений для случаев, когда пользователь выполняет редактирование записи «долго», а другой пользователь может успеть отредактировать эту же запись и сохранить её раньше. То есть, первый пользователь на этапе редактирования даже не будет знать, что запись уже изменилась, возможно не один раз, и сумеет «затереть» эти обновления своим:
Последняя проверка соответствует генерируемому автоматически для UpdateSQL запросу. Поэтому, при возможных конфликтах обновлений в многопользовательской среде необходимо дописывать условия к where самостоятельно. И, разумеется, также необходимо при реализации аналога upWhereChanged удалять лишние изменения столбцов в update table set… — то есть, оставлять в перечне set только действительно изменённые столбцы, иначе запрос перепишет чужие обновления этой записи. Как вы понимаете, это означает необходимость динамического конструирования запроса UpdateSQL. Если вы хотите задать настройки обнаружения конфликтов обновления индивидуально для каждого поля, то вы можете воспользоваться свойством ProviderFlags для каждого поля. |
CachedUpdates | Определяет, будет ли набор данных кэшировать изменения без немедленного внесения их в базу данных. Если это свойство установлено в значение True, то любые изменения (Insert/Post, Update/Post, Delete) вносятся в базу данных не сразу, а сохраняется в специальном журнале. Приложение должно явно применить изменения, вызвав метод ApplyUpdates. В этом случае все изменения будут выполнены в течение малого промежутка времени и в одной короткой транзакции. По умолчанию значение этого свойства False. |
SQL | Содержит SQL запрос. Если это свойство содержит SELECT запрос, то его необходимо выполнять методом Open. В противном случае необходимо использовать методы Execute или ExecSQL. |
Компонент TFDUpdateSQL
Компонент TFDUpdateSQL позволяет переопределять SQL команды, сгенерированные для автоматического обновления набора данных. Он может быть использован для внесения обновлений в компоненты TFDQuery, TFDTable и TFDStoredProc. Использование TFDUpdateSQL является необязательным для компонентов TFDQuery и TFDTable, потому что эти компоненты способны автоматически генерировать команды для публикации обновлений из набора данных в СУБД. Использование TFDUpdateSQL является обязательным для возможности обновления набора данных TFDStoredProc. Рекомендуем применять его всегда, даже для самых простых случаев, чтобы получать полный контроль над тем какие запросы выполняются в вашем приложении.
Для того чтобы указать SQL команды на этапе проектирования, используйте редактор TFDUpdateSQL времени проектирования, который вызывается двойным щелчком по компоненту.
Замечание Для работы многих редакторов времени проектирования FireDac требуется, чтобы было активно подключение к базе данных (TFDConnection.Connected = True) и транзакция находилась в режиме автостарта (TFDTransaction.Options.AutoStart = True). Но такие настройки могут мешать при работе приложения. Например, пользователь должен входить в программу под своим логином, а TFDConnection подключается к базе данных под SYSDBA. Поэтому после каждого использования редакторов времени проектирования рекомендуем проверять свойство TFDConnection.Connected и сбрасывать его. Кроме того, вам придётся включать и выключать автостарт транзакции предназначенной только для чтения. |
На закладке Generate вы можете упростить себе задачу по написанию Insert/Update/Delete/Refresh запросов. Для этого выберете таблицу для обновления, её ключевые поля, поля для обновления, и поля которые будут перечитаны после обновления, и нажмите на кнопку «Generate SQL».
После чего запросы будут сгенерированы автоматически, и вы перейдёте на закладку «SQL Commands», где можете поправить каждый из запросов.
Замечание Поскольку product_id не включено в Updating Fields, оно отсутствует в генерируемом запросе insert. Предполагается, что этот столбец заполняется автоматически триггером (с генератором), или же этот это IDENTITY столбец (начиная с Firebird 3.0). При получении значения генератора для этого столбца с сервера, рекомендуется вручную добавить столбец PRODUCT_ID в предложение RETURNING оператора INSERT. |
На закладке Options находятся некоторые свойства, которые могут повлиять на генерацию запросов. Эти свойства не относятся к самому компоненту TFDUpdateSQL, а являются ссылками на свойства UpdateOptions набора данных, у которого указан текущий TFDUpdateSQL в свойстве UpdateObject. Так сделано исключительно ради удобства.
Параметр | Назначение |
---|---|
Connection | Связь с компонентом FDConnection. |
DeleteSQL | SQL запрос для удаления записи. |
FetchRowSQL | SQL запрос для возврата одной текущей (обновлённой, вставленной) записи. |
InsertSQL | SQL запрос для вставки записи. |
LockSQL | SQL запрос для блокировки одной текущей записи. (FOR UPDATE WITH LOCK). |
ModifySQL | SQL запрос для модификации записи. |
UnlockSQL | SQL запрос для разблокировки текущей записи. В Firebird не применяется. |
Как вы уже заметили, у компонента TFDUpdateSQL нет свойства Transaction. Это потому, что компонент не выполняет модифицирующие запросы непосредственно, а лишь заменяет автоматически сгенерированные запросы в наборе данных, который является предком TFDRdbmsDataSet.
Компонент TFDCommand
Компонент TFDCommand предназначен для выполнения SQL запросов. Он не является наследником TDataSet, а потому удобен для выполнения SQL запросов, не возвращающих набор данных.
Параметр | Назначение |
---|---|
Connection | Связь с компонентом FDConnection. |
Transaction | Транзакция, в рамках которой будет выполняться SQL команда. |
CommandKind | Тип команды.
Обычно тип команды определяется автоматически по тексту SQL запроса. |
CommandText | Текст SQL запроса. |
Создание справочников
В нашем приложении мы создадим два справочника: справочник товаров и справочник заказчиков. Каждый из справочников представляет собой форму с сеткой TDBGrid, источником данных TDataSource, набором данных TFDQuery, пишущей транзакции TFDTransaction.
Замечание Компонент trRead не виден, потому что находится не на форме, а в модуле dmMain. |
Рассмотрим создание справочников на примере справочника заказчиков.
Разместим компонент TFDQuery на форме с именем qryCustomers. Этот набор данных будет указан в свойстве DataSet источника данных DataSource. В свойстве Transaction укажем ReadOnly транзакцию trRead, которая была создана в главном датамодуле проекта. В свойстве UpdateTransaction указываем транзакцию trWrite, в свойстве Connection — соединение расположенное в главном датамодуле. В свойстве SQL напишем следующий запрос:
SELECT
customer_id,
name,
address,
zipcode,
phone
FROM
customer
ORDER BY name
Пишущая транзакция trWrite должна быть максимально короткой, и иметь режим изолированности SNAPSHOT. Мы не будем полагаться на автоматический старт и завершение транзакции, а будем стартовать и завершать транзакцию явно. Таким образом, наша транзакция должна иметь следующие свойства:
Options.AutoStart = False
Options.AutoCommit = False
Options.AutoStop = False
Options.DisconnectAction = xdRollback
Options.Isolations = xiSnapshot
Options.ReadOnly = False
На самом деле необязательно устанавливать режим изолированности SNAPSHOT для простых INSERT/UPDATE/DELETE. Однако если у таблицы есть сложные триггеры, или вместо простых запросов INSERT/UPDATE/DELETE вызывается хранимая процедура, то желательно использовать уровень изолированности SNAPSHOT.
Дело в том, что уровень изолированности READ COMMITED не обеспечивает атомарности оператора в пределах одной транзакции (statement read consistency). Таким образом, оператор SELECT может возвращать данные, которые попали в базу данных после начала выполнения запроса. В принципе режим изолированности SNAPSHOT можно рекомендовать почти всегда, если транзакция будет короткой.
Для возможности редактирования набора данных необходимо заполнить свойства InsertSQL, ModifySQL, DeleteSQL и FetchRowSQL. Эти свойства могут быть сгенерированы мастером, но после этого может потребоваться некоторая правка. Например вы можете дописать предложение RETURNING, удалить модификацию некоторых столбцов, или же вовсе заменить автоматически сгенерированный запрос на вызов хранимой процедуры.
InsertSQL:
INSERT INTO customer (customer_id,
name,
address,
zipcode,
phone)
VALUES (:new_customer_id,
:new_name,
:new_address,
:new_zipcode,
:new_phone)
ModifySQL:
UPDATE customer
SET name = :new_name,
address = :new_address,
zipcode = :new_zipcode,
phone = :new_phone
WHERE (customer_id = :old_customer_id)
DeleteSQL:
DELETE FROM customer
WHERE (customer_id = :old_customer_id)
FetchRowSQL:
SELECT
customer_id,
name,
address,
zipcode,
phone
FROM
customer
WHERE customer_id = :old_customer_id
В этом справочнике будем получать значение генератора перед вставкой записи в таблицу. Для этого необходимо установить значение свойств компонента TFDQuery в следующие значения UpdateOptions.GeneratorName = GEN_CUSTOMER_ID и UpdateOptions.AutoIncFields = CUSTOMER_ID. Есть другой способ, когда значение генератора (автоинкрементного поля) возвращается после выполнения INSERT запроса с помощью предложения RETURNING. Этот способ будет показан позже.
Для добавления новой записи и редактирования существующей принято использовать модальные формы, по закрытию которых с результатом mrOK изменения вносятся в базу данных. Обычно для создания таких форм используются DBAware компоненты, которые позволяют отображать значения некоторого поля в текущей записи и немедленно вносить изменения в текущую запись набора данных в режимах Insert/Edit, т.е. до Post. Но перевести набор данных в режим Insert/Edit можно только стартовав пишущую транзакцию. Таким образом, если кто-то откроет форму для внесения новой записи и уйдёт на обед, не закрыв эту форму, у нас будет висеть активная транзакция до тех пор, пока сотрудник не вернётся с обеда и не закроет форму. Это в свою очередь приведёт к тому, что активная транзакция будет удерживать сборку мусора, что позже приведёт к снижению производительности. Эту проблему можно решить одним из двух способов:
- Использовать режим CachedUpdates, что позволяет держать транзакцию активной только на очень короткий промежуток времени, а именно на время внесения изменений.
- Отказаться от применения DBAware компонентов. Однако этот путь потребует от вас дополнительных усилий.
Мы покажем применение обоих способов. Для справочников гораздо удобнее использовать первый способ. Рассмотрим код редактирования записи поставщика
procedure TCustomerForm.actEditRecordExecute(Sender: TObject);
var
xEditor: TEditCustomerForm;
begin
xEditor := TEditCustomerForm.Create(Self);
try
xEditor.OnClose := CustomerEditorClose;
xEditor.CustomerForm := Self;
xEditor.Caption := 'Edit customer';
qryCustomer.CachedUpdates := True;
qryCustomer.Edit;
xEditor.ShowModal;
finally
xEditor.Free;
end;
end;
По коду видно, что перед переводом набора данных в режим редактирования мы устанавливаем ему режим CachedUpdates, а вся логика обработки редактирования происходит в модальной форме.
procedure TCustomerForm.CustomerEditorClose (Sender: TObject;
var Action: TCloseAction);
begin
if TForm(Sender).ModalResult <> mrOK then
begin
// отменяем все изменения
qryCustomer.Cancel;
qryCustomer.CancelUpdates;
// возвращаем набор данных в обычный режим обновления
qryCustomer.CachedUpdates := False;
// и позволяем закрыть форму
Action := caFree;
Exit;
end;
try
// подтверждаем изменения на уровне набора данных
qryCustomer.Post;
// стартуем транзакцию
trWrite.StartTransaction;
// если в наборе данных есть изменения
if (qryCustomer.ApplyUpdates = 0) then
begin
// записываем их в БД
qryCustomer.CommitUpdates;
// и подтверждаем транзакцию
trWrite.Commit;
end
else begin
raise Exception.Create(qryCustomer.RowError.Message);
end;
qryCustomer.CachedUpdates := False;
Action := caFree;
except
on E: Exception do
begin
// откатываем транзакцию
if trWrite.Active then
trWrite.Rollback;
Application.ShowException(E);
// Не закрываем окно, даём возможность исправить ошибку
Action := caNone;
end;
end;
end;
Из кода видно, что до тех пор, пока кнопка OK не нажата, пишущая транзакция не стартует вовсе. Таким образом, пишущая транзакция активна только на время переноса данных из буфера набора данных в базу данных. Поскольку мы копим в буфере не более одной записи, транзакция будет активна очень короткое время, что и требовалось.
Справочник товаров делается аналогично справочнику заказчиков. Однако в нём мы продемонстрируем другой способ получения автоинкрементных значений.
Основной запрос будет выглядеть следующим образом:
SELECT
product_id,