Работа с транзакциями и их использование в FIBPlus. Часть 1
Довольно часто использование механизма транзакций при работе множества пользователей с базами данных, поддерживаемыми системами управления базами данных InterBase или Firebird (да и другими серверами баз данных), вызывает немалые затруднения. Как правило, программисты используют только уровень изоляции READ COMMITTED, независимо от конкретных условий выполнения задачи.
Мы получаем немало писем с просьбой подробнее рассказать о транзакциях и использовании их характеристик при работе с компонентами FIBPlus.
Помимо документации по InterBase Language Reference, Embedded SQL Guide и API Guide, книги Helen Borrie The Firebird Book: A Reference for Database Developers (в издательстве БХВ-Петербург выходит перевод — «Firebird: справочник для разработчиков базы данных», перевод мой, научный редактор Дмитрий Кузьменко) и книги Алексея Ковязина и Сергея Вострикова «Мир InterBase», где, в принципе, содержится полное описание транзакций, существует множество статей, посвященных транзакциям. Мы решили не повторять этих описаний, а дать возможность «потрогать руками» основные характеристики транзакций и посмотреть, каким образом эти характеристики влияют на многопользовательскую работу с базой данных.
Для этого было написано несколько небольших программ, с которыми удобно проводить эксперименты. Для работы с базой данных мы, естественно, использовали компоненты FIBPlus. Результаты таких действий описываются в данной статье. Мы подробно рассмотрим компонент TpFIBTransaction — задаваемые параметры транзакции и их соответствие элементам языка SQL, используемым для описания характеристик транзакций.
Используемая база данных
Для экспериментов создадим базу данных FIBTRANSACT.FDB, поддерживаемую сервером баз данных Firebird 1.5. В базе данных создадим две таблицы: REFCOUNTRY, являющуюся справочником стран, и REFREGION, содержащую список регионов некоторых стран. Это фрагмент реально используемой базы данных. Вначале создадим домены:
CREATE DOMAIN DCodCtr AS CHAR(3);
CREATE DOMAIN DName30 AS VARCHAR(30) COLLATE PXW_CYRL;
CREATE DOMAIN DName60 AS VARCHAR(60) COLLATE PXW_CYRL;
CREATE DOMAIN DDescr AS BLOB SUB_TYPE 1 SEGMENT SIZE 400;
|
Фрагмент скрипта создания таблиц:
/*** Справочник стран REFCOUNTRY ***/
CREATE TABLE REFCOUNTRY
( Name DName30, /* Краткое название страны */ FullName DName60, /* Полное название страны */ CodCtr DCodCtr NOT NULL, /* Код страны */ Capital DName30, /* Столица */ Region DName30, /* Название региона */ Description DDescr, /* Дополнительные сведения */ CONSTRAINT "Country_PRIMARY_KEY" PRIMARY KEY (CodCtr) ); COMMIT; |
/*** Справочник регионов REFREGION ***/
CREATE TABLE REFREGION
( CodCtr DCodCtr NOT NULL, /* Код страны */ CodReg DCodCtr NOT NULL, /* Код региона */ Center DName30, /* Название центра региона */ RegName DName60, /* Название региона */ Description DDescr, /* Дополнительные сведения */ CONSTRAINT "Region_PRIMARY_KEY" PRIMARY KEY (CodCtr, CodReg),
CONSTRAINT "Region_FOREIGN_KEY"
FOREIGN KEY (CodCtr) REFERENCES REFCOUNTRY (CodCtr)
ON DELETE CASCADE
ON UPDATE CASCADE
); COMMIT; |
Заполним таблицы данными по странам и по регионам стран USA и ENGLAND.
Экспериментальная программа
Создадим в IDE Delphi или C++Builder новое приложение. Создадим на форме панель инструментов с кнопками TButton, положим два компонента TDBDrid: DBGridCountry и DBGridRegion. Добавим два компонента TDataSource: DataSourceCountry и DataSourceRegion.
С закладки FIBPlus поместим на форму следующие компоненты: Database1 типа TpFIBDatabase, Transaction1 типа TpFIBTransaction, два компонента типа TpFIBDataSet: DataSetCountry и DataSetRegion. Поместим также компонент ErrorHandler:
Рис. 1. Главная форма экспериментальной программы
Для объекта базы данных зададим необходимые значения.
Имя базы данных (свойство DBName) — FIBTRANSACT.FDB (базу данных следует поместить в тот же каталог, что и сам проект).
Зададим имя пользователя (UserName) SYSDBA,
пароль (Password) masterkey,
набор символов (CharSet) WIN1251,
установим диалект базы данных (SQLDialect) равным 3.
В качестве транзакции по умолчанию (DefaultTransaction) и транзакции для изменений выберем Transaction1:
Рис. 2. Свойства компонента базы данных
Для компонента транзакции Transaction1 установим следующие значения.
Выберем из выпадающего списка имени базы данных (DefaultDatabase) Database1.
Для уровня изоляции транзакции TPBMode из выпадающего списка выберем значение tpbDefault — только в этом случае мы сможем изменять содержимое буфера параметров транзакции (transaction parameter buffer, TPB); при выборе значения tpbReadCommitted или tpbRepeatableRead в момент запуска транзакции в буфер параметров будут помещаться соответствующие константы, и изменить эту ситуацию никакими силами нельзя.
Рис. 3. Свойства компонента транзакции
Для компонента набора данных DataSetCountry большинство свойств можно оставить в том виде, как они задаются по умолчанию. Установим следующие значения.
База данных (Database) — Database1.
Транзакция (Transaction) и транзакция для изменений (UpdateTransaction) — Transaction1.
В списке режимов (Option) для подсвойства poStartTransaction установим значение False. Это важно, поскольку мы собираемся явно управлять запуском и подтверждением (или откатом) транзакций. Подсвойство poKeepSorting установим в True. Для целей исследования поведения транзакций это особой роли не играет, однако бывает полезным для сохранения упорядоченности набора данных в случае внесения изменений в столбцы, по которым осуществляется упорядочивание набора данных (предложение ORDER BY).
В списке PrepareOptions можно (в нашем случае необязательно) установить psAskRecordCount в True. Это бывает полезным, если вам после открытия набора данных нужно в строке состояния указать количество полученных записей.
Вызовем генератор SQL (щелчок правой кнопкой мыши на компоненте и выбор в контекстном меню строки SQL Generator). В списке таблиц выберем REFCOUNTRY и дважды щелкнем мышью по этой строке. Будет сгенерирован оператор SELECT. В конец оператора добавим предложение ORDER BY NAME, чтобы упорядочить получаемый набор данных по именам стран.
Рис. 4. Генерация оператора SELECT
Перейдем на закладку Generate Modify SQLs и щелкнем мышью по кнопке Generate
SQLs. Будут сгенерированы операторы для добавления (Insert), изменения (Update), удаления (Delete) и повторного чтения текущей строки (Refresh).
Рис. 5. Генерация модифицирующих операторов
Компонент DataSourceCountry свяжем с набором данных DataSetCountry (свойство DataSet).
Аналогичные действия нужно выполнить и с компонентом DataSetRegion. Помимо этого, поскольку этот компонент является детальным (дочерним, подчиненным) набором данных в связке master-detail, нужно установить его свойство DataSource в DataSourceCountry, а в свойстве DetailConditions следует установить в True подсвойства dcForceOpen и dcWaitEndMasterScroll. При генерации оператора SELECT в генераторе SQL нужно скорректировать оператор следующим образом:
SELECT
CODCTR,
CODREG,
CENTER,
REGNAME,
DESCRIPTION
FROM
REFREGION
WHERE CODCTR = ?CODCTR
ORDER BY CENTER |
Здесь предложение WHERE CODCTR = ?CODCTR задает выборку только тех строк таблицы регионов, которые относятся к текущей стране. Более подробно про связь главная-подчиненная и ее реализацию при помощи компонентов FIBPlus читайте в соответствующей статье на сайте www.devrace.com.
Далее создадим еще две формы: одна, Transaction's Characteristics, будет использоваться для формирования списка параметров транзакции TRParams, другая, Transaction's Parameters, позволит отображать содержимое буфера параметров транзакции (TPB).
Рис. 6. Форма Transaction's Characteristics
Форма Transaction's Characteristics нам понадобится для создания списка характеристик транзакции. Левому компоненту ListBox присвоим имя AllParameters, правому — SelectedParameters. Напишем следующие обработчики событий щелчка по кнопкам. При щелчке по кнопке «>» выполняется перемещение выбранных строк из левого компонента ListBox в правый:
procedure TFormTrans.Button1Click(Sender: TObject);
var I: Integer;
begin
I := 0;
while (I <= AllParameters.Items.Count - 1) do
begin
if AllParameters.Selected[I] then
begin
SelectedParameters.Items.Add(AllParameters.Items[I]);
AllParameters.Items.Delete(I);
I := I - 1;
end;
I := I + 1;
end;
end; |
Замечание. Здесь не приводится программный текст для C++Builder, так как в принципе очень просто осуществить «перевод» с языка Delphi на C++.
В любом компоненте ListBox можно выбрать несколько строк, удерживая нажатой клавишу Ctrl и щелкая мышью по нужным строкам.
При щелчке по кнопке «>>» выполняется перемещение всех строк из левого компонента ListBox в правый. По правде сказать, эта функция для наших целей не нужна и приводится здесь для сохранения принятого порядка.
procedure TFormTrans.Button2Click(Sender: TObject);
var I: Integer;
begin
for I := 0 to AllParameters.Items.Count - 1 do
SelectedParameters.Items.Add(AllParameters.Items[I]);
AllParameters.Items.Clear;
end; |
При щелчке по кнопке «<» выполняется перемещение выбранных строк из правого компонента ListBox в левый:
procedure TFormTrans.Button4Click(Sender: TObject);
var I: Integer;
begin
I := 0;
while (I <= SelectedParameters.Items.Count - 1) do
begin
if SelectedParameters.Selected[I] then
begin
AllParameters.Items.Add(SelectedParameters.Items[I]);
SelectedParameters.Items.Delete(I);
I := I - 1;
end;
I := I + 1;
end;
end; |
При щелчке по кнопке «<<» выполняется перемещение всех строк из правого компонента ListBox в левый:
procedure TFormTrans.Button3Click(Sender: TObject);
var I: Integer;
begin
for I := 0 to SelectedParameters.Items.Count - 1 do
AllParameters.Items.Add(SelectedParameters.Items[I]);
SelectedParameters.Items.Clear;
end; |
Рис. 7. Форма Transaction's Parameters
Форма Transaction's Parameters позволяет отобразить содержимое списка параметров транзакции (TRParams) и вектора буфера параметров транзакции (TPB). При вызове формы в поле Memo отображается список параметров и числовые значения из TPB.
procedure TFormTransactionParam.FormShow(Sender: TObject);
var I: Integer;
begin
Memo1.Clear;
for I := 0 to FormMain.Transaction1.TRParams.Count - 1 do
Memo1.Lines.Add(FormMain.Transaction1.TRParams.Strings[I]);
Memo1.Lines.Add('============================');
for I := 0 to FormMain.Transaction1.TPBLength - 1 do
Memo1.Lines.Add(IntToStr(Integer(FormMain.Transaction1.TPB[I])));
end; |
Вернемся в главный модуль. Напишем обработчики событий формы. В событии формы OnShow выполним подключение к базе данных, в событии OnClose отключимся от базы данных. Соответственно, это будут операторы:
Database1.Open; |
и
Database1.Close; |
Щелчок по кнопке Open DataSet приводит к открытию набора данных DataSetCountry. При этом автоматически открывается и набор данных DataSetRegion.
procedure TFormMain.BOpenDataSetClick(Sender: TObject);
begin DataSetCountry.Open;
end; |
Щелчок по кнопке Close DataSet закрывает оба набора данных:
DataSetCountry.Close; |
Щелчок по кнопкам BStartTransaction и BStopTransaction приводит, соответственно, к запуску и останову транзакции:
Transaction1.StartTransaction; Transaction1.Active := False; |
Обработка события щелчка по кнопке BCommit подтверждает транзакцию. После подтверждения транзакции она запускается заново, после чего открывается набор данных (он автоматически будет закрыт при завершении транзакции — по COMMIT или ROLL BACK):
procedure TFormMain.BCommitClick(Sender: TObject);
begin
Transaction1.Commit;
Transaction1.StartTransaction;
DataSetCountry.Open;
end; |
Похожим образом обрабатывается событие щелчка по кнопке BRollBack.
procedure TFormMain.BRollBackClick(Sender: TObject);
begin
Transaction1.Rollback;
Transaction1.StartTransaction;
DataSetCountry.Open;
end; |
Щелчок по кнопке Characteristics приводит к обращению к форме Transaction's Characteristics для формирования пользователем списка характеристик транзакции.
procedure TFormMain.BCharactTransactClick(Sender: TObject);
var I: Integer;
begin
if FormTrans.ShowModal <> IDOK then exit;
if Transaction1.Active then
Transaction1.Active := False;
Transaction1.TRParams.Clear;
for I := 0 to FormTrans.SelectedParameters.Items.Count - 1 do
Transaction1.TRParams.Add(
FormTrans.SelectedParameters.Items[I]);
end; |
Щелчок по кнопке ParamTransact приводит к вызову формы Transaction's Parameters, которая отобразит текущие характеристики транзакции.
И, наконец, напишем обработчик ошибок базы данных. Это позволит нам в случае ошибок увидеть не только сообщение сервера базы данных, но и значения кодов SQLCODE и GDSCODE. Для этих целей используем событие OnFIBErrorEvent компонента ErrorHandler:
procedure TFormMain.ErrorHandler1FIBErrorEvent(Sender: TObject;
ErrorValue: EFIBError; KindIBError: TKindIBError;
var DoRaise: Boolean);
var S: String;
begin
S := S + 'SQLCode = ' + IntToStr(ErrorValue.SQLCode) + #10#13;
S := S + 'IBErrorCode = ' + IntToStr(ErrorValue.IBErrorCode) + #10#13;
S := S + 'IBMessage = ' + ErrorValue.IBMessage + #10#13;
Application.MessageBox(PAnsiChar(S), 'Database Error',
MB_OK + MB_ICONSTOP);
DoRaise := False;
end; |
При этом в список используемых модулей программы (предложение uses) необходимо добавить fib.
Замечание. Этот обработчик ошибок только выдает сообщение и не выполняет никаких других действий. Вам нужно вручную остановить транзакцию, заново ее запустить и открыть набор данных, чтобы продолжить работу. В реальной жизни вы, конечно же, проведете в программе грамотный анализ ошибки и выполните все необходимые действия по ее нейтрализации и восстановлению работоспособности программы.