Delphi-Help

Главная

Работа с транзакциями и их использование в FIBPlus. Часть 4

Оцените материал
(3 голосов)


Работа с транзакциями и их использование в FIBPlus. Часть 4

Транзакция, работающая с двумя и более базами данных

Серверы баз данных InterBase и Firebird обеспечивают работу одной транзакции с несколькими базами данных. Особенности такой работы — двухфазное подтверждение транзакций, появление «зависших» (in limbo) транзакций — подробно описаны в литературе, и мы не будем на этом останавливаться.

Компоненты FIBPlus, разумеется, в полном объеме поддерживают мультибазовые транзакции. Кроме того, здесь содержатся некоторые компоненты, которые удобно использовать при работе с несколькими базами данных. Рассмотрим один небольшой, но весьма полезный пример. За основу возьмем пример проекта UpdateObject с некоторыми изменениями.

Нам понадобятся две базы данных: COUNTRY.GDB и PERSON.GDB. Первая содержит справочник стран REFCOUNTRY, во второй присутствует таблица PERSON, которая была создана следующим оператором:

CREATE TABLE PERSON (
CODPERS INTEGER NOT NULL,
FIRST_NAME VARCHAR(20),
LAST_NAME VARCHAR(20),
COUNTRY CHAR(3),
PRIMARY KEY (CODPERS)
);

Создан генератор GEN_PERSON для получения значения искусственного первичного ключа CODPERS. Создан также обычный триггер для заполнения первичного ключа значением, получаемым из генератора:

CREATE TRIGGER CREATE_PERSON FOR PERSON
ACTIVE BEFORE INSERT POSITION 0
AS BEGIN
IF (NEW.CODPERS IS NULL) THEN
NEW.CODPERS = GEN_ID(GEN_PERSON, 1);
END

В таблицу добавлены 22 записи, взятые из демонстрационной базы данных EMPLOYEE.GDB.

Обратите внимание, что в записи присутствует столбец код страны COUNTRY, который, по сути своей, является внешним ключом, ссылающимся на код страны в таблице REFCOUNTRY (столбец CODCTR), находящейся в другой базе данных COUNTRY.GDB. Поскольку никакие связи на уровне структур данных, описываемых средствами SQL, между таблицами в разных базах данных невозможны, мы не можем указать здесь предложение FOREIGN KEY и молча использовать средства сервера базы данных для поддержания ссылочной целостности данных. Мы даже не можем написать триггер или хранимую процедуру для выполнения соответствующих действий, поскольку и триггер, и процедура могут работать только с одной, «своей», базой данных. Все это нам придется делать самостоятельно.

К счастью, в FIBPlus существуют соответствующие средства, которыми мы воспользуемся.

Создадим новый проект, который назовем MultiBase. Положим на форму два компонента DBGrid — один для справочника стран, другой для списка сотрудников, зададим им соответствующие значения выравнивания (свойство Align), положим разделитель Splitter. Разместим два компонента TpFIBDatabase, два компонента TpFIBTransaction, один для чтения, другой для обновления, два компонента набора данных и два компонента DataSource.

В обоих компонентах TpFIBDatabase установим свойство DefaultTransaction (транзакция по умолчанию) в значение ReadTransaction, а свойство DefaultUpdateTransaction (обновляющая транзакция по умолчанию) в значение WriteTransaction. В результате этого внутренний список баз данных каждой транзакции будет содержать эти две базы. При запуске транзакций они будут стартовать сразу в двух базах каждая. Если мы поместим на форму десять компонентов баз данных, указав таким же образом две транзакции по умолчанию, то каждая транзакция будет запускаться в десяти базах данных.

Для проверки этого факта я добавил в меню еще один элемент — Transact Databases. В обработке вызова этого элемента определялись имена баз данных обеих транзакций:

procedure TFormMain.MTransactDatabasesClick(Sender: TObject);
var i: integer;
S: String;
Begin
S := 'WriteTransaction' + #10#13;
S := S + 'DatabaseCount: ' +
IntToStr(WriteTransaction.DatabaseCount) + #10#13;
for i := 0 to WriteTransaction.DatabaseCount - 1 do
S := S + WriteTransaction.Databases[i].Name + #10#13;
S := S+#10#13 + 'ReadTransaction' + #10#13;
S := S + 'DatabaseCount: ' +
IntToStr(ReadTransaction.DatabaseCount) + #10#13;
for i := 0 to ReadTransaction.DatabaseCount - 1 do
S := S + ReadTransaction.Databases[i].Name + #10#13;
Application.MessageBox(PChar(S), 'ReadTransaction', MB_OK);
end;

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

В компоненте PersonData зададим автоматический запуск и автоматическое подтверждение транзакций (в компоненте CountryData эти установки делать не будем), сформируем соответствующие операторы SQL. Свяжем компоненты между собой обычным образом, устанавливая значения соответствующих свойств.

Положим на форму меню и создадим два элемента: Commit и Rollback для подтверждения и отката обновляющей транзакции, соответственно:

procedure TFormMain.MCommitClick(Sender: TObject);
begin
WriteTransaction.CommitRetaining;
PersonData.FullRefresh;
end;
procedure TFormMain.MRollbackClick(Sender: TObject);
begin
WriteTransaction.RollbackRetaining;
CountryData.FullRefresh;
end;

Подтверждение транзакции имеет смысл, когда мы корректируем справочник стран (при корректировке персонала подтверждение выполняется автоматически). Чтобы на сетке DBGrid отображались изменения в записях персонала, мы обращаемся к методу FullRefresh для этого набора данных.

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

В событии формы OnShow напишем операторы соединения с базами данных, запуска обеих транзакций и открытия обоих наборов данных. Те же действия, с точностью до наоборот, выполним в событии закрытия формы.

0011_01

Рис. 8. Проект MultiBase

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

Воспользуемся возможностями компонента TpFIBUpdateObject, входящего в состав FIBPlus. Положим на форму два компонента TpFIBUpdateObject с именами UpdateObjectEdit и UpdateObjectDelete. Их мы будем использовать для поддержания соответствия таблицы PERSON таблице REFCOUNTRY. Их задачами, соответственно, являются обеспечение функциональности, указываемой для внешнего ключа, — ON UPDATE CASCADE и ON DELETE CASCADE.

Для UpdateObjectEdit установим значение свойства Database в DatabaseData, это означает, что оператор SQL этого компонента работает с базой данных, заданной в компоненте DatabaseData (это база данных PERSON.GDB), и его оператор SQL будет относиться к таблице, находящейся в этой базе данных.

Свойство DataSet установим в CountryData (точнее, выберем из выпадающего списка). Это имя набора данных, на одно из событий которого будет реагировать наш компонент. Этот набор данных относится к таблице, хранящейся в другой базе данных. Конкретное событие мы выберем из выпадающего списка свойства KindUpdate: ukModify. Это означает, что будет выполняться оператор SQL нашего компонента, когда произойдет изменение данных в наборе данных CountryData. Зададим транзакцию для компонента WriteTransaction. Это транзакция, в контексте которой будет выполняться оператор SQL компонента UpdateObjectEdit.

В свойстве SQL создадим следующий оператор:

UPDATE PERSON SET COUNTRY = :CODCTR
WHERE COUNTRY = :OLD_CODCTR

Как это работает? Когда пользователь изменяет любую строку в таблице REFCOUNTRY и подтверждает транзакцию на обновление (щелкает мышью по любой другой строке таблицы), запускается на выполнение оператор SQL нашего компонента. Он заменяет все значения кода страны (COUNTRY) в таблице PERSON на новое значение, получаемое из таблицы REFCOUNTRY (параметр :CODCTR). Изменению подвергаются только те строки, код страны в которых был равен старому значению кода страны из таблицы REFCOUNTRY (параметр :OLD_CODCTR).

Таким образом мы смоделировали предложение ON UPDATE CASCADE в описании внешнего ключа.

Похожим образом зададим свойства компонента UpdateObjectDelete. Только в свойстве KindUpdate мы выберем значение ukDelete, а в свойстве SQL запишем:

DELETE FROM PERSON
WHERE COUNTRY = :OLD_CODCTR

В качестве обработчика события AfterExecute компонента выберем тот же код, что и для компонента UpdateObjectEdit.

Запустите программу на выполнение. Измените в любой строке справочника стран код страны и перейдите к любой другой строке, чтобы отправить изменения на сервер. При этом, поскольку транзакция не подтверждена, во второй таблице мы не можем видеть изменения (транзакция для чтения видит только подтвержденные изменения). Подтвердите транзакцию через соответствующий элемент меню. Тут же вы увидите, что соответствующие коды страны поменялись во всех нужных записях таблицы PERSON. Удалите одну из стран (клавиши Ctrl+Del и подтверждение удаления в диалоговом окне). Подтвердите транзакцию. Будут удалены все записи PERSON, ссылающиеся на данную страну.

Вы можете выполнять произвольное количество изменений и удалений записей стран. Соответствующие изменения в списке персонала появятся только лишь после подтверждения транзакции.

Таким способом мы промоделировали поведение внешнего ключа, правда, пока только в одну сторону.

Для полноты счастья необходимо выполнить проверки на правильность добавления новых строк в таблицу PERSON и изменения значения в этой таблице «внешнего ключа» — кода страны. Нам нужно запретить добавление и изменение этого столбца, если в таблице стран нет записи с соответствующим значением первичного ключа. Поскольку внешний ключ может иметь значение NULL, мы не должны выполнять такую проверку для пустого значения этого столбца.

Для реализации этого поведения создадим исключение и хранимую процедуру для выполнения соответствующей проверки в базе данных COUNTRY.GDB:

CREATE EXCEPTION NO_COUNTR

'В справочнике стран отсутствует страна с указанным кодом';

COMMIT;
SET TERM !! ;
CREATE PROCEDURE TEST_COUNTRY (CODCTR CHAR(3))
AS
DECLARE VARIABLE COUNTRY_NUM integer;
BEGIN
if (:CODCTR != '') then
begin
select count(*) from REFCOUNTRY where CODCTR = :CODCTR
INTO :COUNTRY_NUM;
if (:COUNTRY_NUM = 0) then
EXCEPTION NO_COUNTRY;
End
END !!
SET TERM ; !!
COMMIT;

Положим на форму еще два компонента UpdateObject: UpdateObjectAddChild и UpdateObjectEditChild. Свойства UpdateObjectEditChild показаны на рисунке.

0011_02

Рис. 9. Свойства компонента UpdateObjectEditChild

Свойство SQL содержит обращение к хранимой процедуре:

EXECUTE PROCEDURE TEST_COUNTRY (:CODCTR)

Компонент UpdateObjectAddChild имеет все те же свойства за исключением значения KindUpdate, для которого выбрано ukInsert.

Для набора данных PersonData напишем обработчик события BeforePost:

procedure TFormMain.PersonDataBeforePost(DataSet: TdataSet);
begin
UpdateObjectAddChild.ParamByName(’CODCTR’).AsString :=
PersonData.FieldByName(’COUNTRY’).AsString;
UpdateObjectEditChild.ParamByName(’CODCTR’).AsString :=
PersonData.FieldByName(’COUNTRY’).AsString;
end;

Здесь мы формируем значения параметров CODCTR компонентов UpdateObject.

Если при добавлении новой записи в таблицу PERSON или изменении значения существующей записи значение кода страны не будет найдено в справочнике стран, то выдается исключение с текстом «В справочнике стран отсутствует страна с указанным кодом».

Запустите проект на выполнение, измените в таблице PERSON код страны на несуществующее значение. Вы получите наше исключение. Если же вы просто удалите значение кода страны (ко примет значение NULL), то ничего не произойдет. Что и требовалось.

Замечание. Следует напомнить, что только что проделанная нами работа является лишь иллюстрацией возможностей компонентов FIBPlus по работе с несколькими базами данных. В реальных проектах с целью оптимизации следовало бы использовать более «тонкие» настройки, в частности, задействовать другие события компонентов, использовать свой обработчик ошибочных ситуаций и т.д.

Вложенные транзакции

В InterBase и Firebird существует средство, которое называется «вложенными транзакциями» или, более правильно, пользовательскими точками сохранения транзакции (savepoint).

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

Вообще говоря, с точки зрения программиста здесь все очень просто. Для создания точки сохранения используется оператор SQL SAVEPOINT:

  • SAVEPOINT <идентификатор>; Идентификатор — любое правильное имя объекта базы данных, длиной до 31 символа. Если точка сохранения с тем же именем уже существует, то она заменяется на новую. То есть, старая точка сохранения удаляется, и создается новая с тем же именем. В FIBPlus для этого используется метод компонента транзакции SetSavePoint:
  • SetSavePoint(<идентификатор>);
  • Для возврата (отката) на конкретную точку сохранения в SQL используется следующий оператор: ROLLBACK [WORK] TO [SAVEPOINT] <идентификатор>;
  • В FIBPlus используется метод компонента транзакции SetSavePoint: RollBackToSavePoint(<идентификатор>);
  • Для освобождения точки сохранения используется оператор SQL RELEASE SAVEPOINT: RELEASE SAVEPOINT <идентификатор> [ONLY]; Если не указано ключевое слово ONLY, то будут освобождены и потеряны все точки сохранения, начиная с указанной.

В FIBPlus используется метод компонента транзакции ReleaseSavePoint:

ReleaseSavePoint(<идентификатор>);

Этот метод всегда освобождает все точки, начиная с указанной (по крайней мере, такая реализация метода в версии 6.25).

Для иллюстрации работы с точками сохранения используем существующий пример SavePoint с некоторыми изменениями.

Создадим новый проект. Положим на форму панель и выровняем ее по верхнему краю. Это будет панель инструментов. Разместим на ней кнопку завершения работы, выпадающий список ComboBox и пять кнопок TButton — создания точки сохранения (текст на кнопке Add), отката транзакции на точку сохранения (Rollback), подтверждения транзакции (Commit), освобождения точек сохранения (Release) и создания точки с тем же именем (AddExist).

Положим (красоты ради) StatusBar, где будем указывать количество записей стран, DBGrid и DataSource. Добавим компоненты FIBPlus для работы с базой данных — TpFIBDatabase, TpFIBTransaction, TpFIBDataSet. Обычным образом для набора данных установим значения операторов SQL, сделаем для него длинную транзакцию, то есть, не будем устанавливать в True значение свойства AutoCommit.

0011_03

Рис. 10. Проект SavePoint

В качестве базы данных будем использовать COUNTRY.GDB.

Главное в этом проекте — написание обработчиков. Для начала опишем в области private переменную SavePoint:

SavePoint: Integer;

В ней будет храниться текущий номер точки сохранения.

Обработчик события OnShow для формы выглядит следующим образом:

procedure TFormMain.FormShow(Sender: TObject);
begin
Database.Connected := True;
WriteTransaction.StartTransaction;
CountryData.Open;
SavePoint := 0;
StatusBar1.Panels.Items[1].Text := IntToStr(CountryData.RecordCount);
DBGrid1.SetFocus;
end;

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

Напишем следующий обработчик щелчка по кнопке добавления точки сохранения:

procedure TFormMain.BSAddClick(Sender: TObject);
var NewPoint: string;
begin
SavePoint := SavePoint + 1;
NewPoint := 'SavePoint' + IntToStr(SavePoint);
WriteTransaction.SetSavePoint(NewPoint);
CSavePoints.Items.Add(NewPoint);
CSavePoints.ItemIndex := CSavePoints.Items.Count - 1;
DBGrid1.SetFocus;
end;

Здесь создается имя точки сохранения (оно должно быть уникальным в рамках выполнения транзакции), заносится в список ComboBox. Оператор создания точки сохранения:

WriteTransaction.SetSavePoint(NewPoint);

Выполнение отката транзакции на точку сохранения, выбранную в списке ComboBox, осуществляется при щелчке по кнопке отмены:

procedure TFormMain.BSRollbackClick(Sender: TObject);
var NewPoint: string;
i, NewIndex: Integer;
begin
if CSavePoints.ItemIndex < 0 then exit;
NewIndex := CSavePoints.ItemIndex - 1;
NewPoint := CSavePoints.Items.Strings[CSavePoints.ItemIndex];
WriteTransaction.RollBackToSavePoint(NewPoint);
CountryData.FullRefresh;
for i := CSavePoints.Items.Count - 1 downto NewIndex + 1 do
CSavePoints.Items.Delete(i);
CSavePoints.ItemIndex := NewIndex;
if NewIndex = -1 then CSavePoints.Clear;
SavePoint := CSavePoints.Items.Count;
StatusBar1.Panels.Items[1].Text := IntToStr(CountryData.RecordCount);
DBGrid1.SetFocus;
end;

Собственно оператор отката следующий:

WriteTransaction.RollBackToSavePoint(NewPoint);

Остальные операторы лишь наводят порядок в списке имен точек сохранения.

При выполнении подтверждения транзакции необходимо также выполнить и приведение в начальное состояние списка точек сохранения:

procedure TFormMain.BSCommitClick(Sender: TObject);
begin
WriteTransaction.CommitRetaining;
CountryData.FullRefresh;
CSavePoints.Items.Clear;
CSavePoints.ItemIndex := -1;
SavePoint := 0;
DBGrid1.SetFocus;
end;

Операция по освобождению точек сохранения похожа на операцию отката:

procedure TFormMain.BSReleaseClick(Sender: TObject);
var NewPoint: string;
i, NewIndex: Integer;
begin
if CSavePoints.ItemIndex < 0 then exit;
NewIndex := CSavePoints.ItemIndex - 1;
NewPoint := CSavePoints.Items.Strings[CSavePoints.ItemIndex];
WriteTransaction.ReleaseSavePoint(NewPoint);
CountryData.FullRefresh;
for i := CSavePoints.Items.Count - 1 downto NewIndex + 1 do
CSavePoints.Items.Delete(i);
CSavePoints.ItemIndex := NewIndex;
if NewIndex = -1 then CSavePoints.Clear;
SavePoint := CSavePoints.Items.Count;
DBGrid1.SetFocus;
end;

Поскольку в нашем списке ComboBox точки сохранения располагаются в порядке их создания, то при создании точки сохранения с тем же именем нужно удалить из списка соответствующую строку и в конец списка добавить имя точки сохранения:

procedure TFormMain.BSAddExistClick(Sender: TObject);
var NewPoint: string;
begin
if CSavePoints.ItemIndex < 0 then exit;
NewPoint := CSavePoints.Items.Strings[CSavePoints.ItemIndex];
CSavePoints.Items.Delete(CSavePoints.ItemIndex);
WriteTransaction.SetSavePoint(NewPoint);
CSavePoints.Items.Add(NewPoint);
CSavePoints.ItemIndex := CSavePoints.Items.Count - 1;
DBGrid1.SetFocus;
end;

Ну и в плане красоты необходимо написать обработчик события удаления строки, чтобы скорректировать отображаемое в панели StatusBar количество стран:

procedure TFormMain.CountryDataAfterDelete(DataSet: TDataSet);
begin
StatusBar1.Panels.Items[1].Text := IntToStr(CountryData.RecordCount);
end;

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

Поскольку для пользовательских точек сохранения используется длинная обновляющая транзакция, применять это средство естественнее в монопольном режиме с уровнем изоляции транзакции SNAPSHOT TABLE STABILITY.

Заключение

В этой статье мы довольно подробно рассмотрели основные вопросы использования транзакций в базах данных InterBase/Firebird и действительно «потрогали руками» основные характеристики транзакций, создавая программы, получающие доступ к данным при помощи компонентов FIBPlus.

Из трех уровней изоляции транзакций для одновременной многопользовательской работы больше всего подходит READ COMMITTED. При этом обновляющие транзакции должны быть короткими.

Уровень изоляции SNAPSHOT является довольно неудобным по причине высокой вероятности получения исключений по блокировкам.

Если же требуется получить монопольный доступ к отдельным таблицам базы данных, следует выбрать SNAPSHOT TABLE STABILITY. При этом следует помнить, что при запуске такой транзакции и попытке открыть наборы данных можно получить исключение по блокировке, если какая-либо параллельная транзакция выполнила изменения в таблице и не подтвердила транзакцию. При использовании коротких обновляющих транзакций вероятность такого исключения мала.

При использовании разделенных транзакций для чтения и для обновления читающая транзакция должна иметь уровень изоляции только READ COMMITTED и никакой другой. Пишущая транзакция может иметь любой требуемый условиями задачи уровень изоляции и соответствующий набор дополнительных характеристик.

Для обновляющих транзакций наиболее подходящими являются два варианта набора характеристик: использование режима WAIT совместно с NO RECORD_VERSION и NO WAIT совместно с RECORD_VERSION. В первом случае полностью исключаются ошибки блокировки после отмены или подтверждения блокирующей транзакции (ошибки будут, если транзакция пытается изменять запись, удаленную другим процессом). Во втором случае мы сразу получаем ошибку, после чего принимаем решение о дальнейших действиях.

Как правило, использование режима NO RECORD_VERSION не является хорошей идеей при достаточно большом количестве одновременно работающих пользователей. Если пользователи активно изменяют данные в базе данных, то может оказаться довольно затруднительным запустить на выполнение такую транзакцию. Если же вам действительно нужно получать самые свежие данные, самые последние изменения, этот режим будет самым подходящим.

При грамотном использовании в компонентах FIBPlus двух транзакций — на чтение и на обновление — можно повысить эффективность использования ресурсов сервера и практически свести до нуля вероятность осложнений в работе пользователя в случае появления блокировок.

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

Серверы InterBase/Firebird (и компоненты FIBPlus) имеют возможность запуска транзакций для нескольких баз данных. Способы работы с данными в этом случае не так уж сильно отличаются от обычной работы с одной базой данных. Основная здесь проблема — связь между данными различных баз данных. В FIBPlus есть прекрасное средство обеспечения такой связи: компонент UpdateObject.

Удобные средства «вложенных транзакций» позволяют раздробить длинную обновляющую транзакцию на фрагменты и выполнять откат не всей транзакции, а только некоторой ее части.

Здесь мы все время говорим об уменьшении вероятности блокировок. У кого-то может сложиться впечатление, что блокировки — это зло, с которым нужно бороться всеми доступными средствами. Конечно, это не так. Все зависит от конкретной решаемой задачи. Часто бывает нужным блокировать изменения в базе данных другими клиентами. В реальной жизни могут потребоваться как «жестокие» средства блокировки в виде уровня изоляции SNAPSHOT TABLE STABILITY, так и «мягкие» средства защищенного режима. Серверы InterBase/Firebird и компоненты обращения к базам данных FIBPlus имеют мощные гибкие средства решения любых задач обработки данных в архитектуре клиент-сервер.

Прочитано 10838 раз

Авторизация



Счетчики