Опубликован: 07.05.2010 | Уровень: специалист | Доступ: свободно
Лекция 24:

Транзакции

< Лекция 23 || Лекция 24: 12 || Лекция 25 >

Уровни изолированности транзакций

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

InterBase имеет три уровня изолированности:

READ COMMITTED - Читать подтвержденные данные. Этот уровень изоляции позволяет транзакции видеть изменения, сделанные другими, транзакциями, если эти изменения были подтверждены. К примеру, стартовали транзакции Т1 и Т2. Обе они изменили запись. Однако Т1 не увидит изменений, сделанных Т2, пока та не подтвердит сделанных ей изменений (по Commit ). Этот уровень изоляции считается самым низким, открытым, позволяя транзакции видеть "самые свежие" данные.

SNAPSHOT - Моментальный снимок данных. Это средний уровень изолированности. Он позволяет транзакции видеть только те данные, которые существовали в момент старта транзакции. Если другие заинтересованные транзакции изменили запись и подтвердили изменения, то SNAPSHOT - транзакция все равно не увидит этих изменений. Однако она не блокирует данные, с которыми работает.

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

Параметры транзакций

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

Таблица 24.1. Возможные параметры транзакций
Параметр Описание
Read Разрешается чтение данных.
Write Разрешается запись данных.
Wait При возникновении конфликта с другой транзакцией, текущая транзакция ожидает определенное время (по умолчанию, 10 сек.), прежде чем попытается решить этот конфликт.
NoWait При возникновении конфликта с другой транзакцией сразу генерируется ошибка.
Read_committed Rec_Version Read _committed позволяет читать подтвержденные изменения данных, сделанные другими транзакциями. Дополнительный параметр Rec_Version, кроме того, позволяет читать и неподтвержденные изменения.
Read_committed No_Rec_Vesion Read _committed позволяет читать подтвержденные изменения данных, сделанные другими транзакциями. Дополнительный параметр No_Rec_Version используется по умолчанию, и не позволяет читать неподтвержденные изменения.
Concurrency Создает уровень изоляции SNAPSHOT.
Consistency Создает уровень изоляции SNAPSHOT TABLE STABILITY

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

Read
Write

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

Если транзакция используется только для чтения, например, для вывода данных в сетку запросом SELECT, то она должна иметь параметры, позволяющие только читать данные, что значительно ускорит работу транзакции. Причем данные должны быть самыми "свежими", даже из неподтвержденных транзакций. И, кроме того, транзакция не должна ожидать разрешения возможного конфликта. Набор параметров тут может быть следующим:

Read
Read_Committed
Rec_Version
NoWait

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

Read
Concurrency
NoWait

И, наконец, если транзакция предназначена для изменения данных в БД, то ей лучше задать более жесткие параметры, например:

Write
Concurrency
NoWait

Практика применения транзакций

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

Прежде всего, убедитесь, что сервер InterBase запущен. Далее, создайте в Delphi новый проект. На главную форму поместите DBNavigator, сетку DBGrid и простую кнопку:

Главная форма приложения

Рис. 24.1. Главная форма приложения

Пока мы не подключим сетку и навигатор к таблице, данные не будут отображаться. Сохраните проект в отдельную папку под именем IBXTrans. Поскольку и навигатор, и сетка нам нужны только для чтения данных, выделите навигатор, откройте сложное свойство VisibleButtons и сделайте невидимыми все кнопки, кроме nbFirst, nbPrior, nbNext, nbLast и nbRefresh. Отрегулируйте ширину навигатора, чтобы кнопки стали квадратными, как на рисунке выше. А у сетки свойство ReadOnly переведите в True. С главной формой закончили.

Командой File -> New -> Data Module создайте новый модуль данных. Свойство Name окна переименуйте в fDM, а сам модуль сохраните под именем DM. Перейдите в главное окно, и командой File -> Use Unit подключите к нему модуль DM.

В модуль поместите следующие компоненты из вкладки InterBase:

IBDatabase, IBTable, IBQuery, два компонента IBTransaction и один DataSource из вкладки Data Access.

Займемся настройкой этих компонентов.

Выделите компонент IBDatabase. Его свойство Name переименуйте в IBDB, чтобы было короче. Откройте свойство Params (откроется окно редактора параметров). Там укажите следующие параметры:

user_name=sysdba
password=masterkey
lc_ctype=win1251

Обратите внимание, все параметры можно писать маленькими буквами, пробелы перед- и после знака "=" недопустимы. Далее, в свойство DatabaseName найдите и поместите файл нашей базы данных First.gdb. Чтобы при загрузке программы не запрашивался логин и пароль, свойство LoginPrompt переведите в False. После этого свойство Connected переведите в True. Если вы все сделали правильно, база откроется, ошибок не возникнет.

Теперь займемся компонентами транзакций. Выделите первый IBTransaction, его свойство Name переименуйте в IBTrans1. Этот компонент будет создавать транзакции для считывания данных в сетку и навигатор на главной форме. Откройте свойство Params (откроется окно редактора параметров), и впишите следующие параметры:

read
read_committed
rec_version

В свойстве DefaultDatabase выберите IBDB, а у IBDB в свойстве DefaultTransaction в свою очередь, выберите IBTrans1. После чего переведите свойство Active транзакции IBTrans1 в True.

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

write
concurrency
nowait

В свойстве DefaultDatabase этой транзакции также выберите IBDB, а вот свойство Active оставьте False - транзакция будет автоматически запускаться, когда мы попытаемся изменить данные в таблице.

Перейдем к компоненту IBTable. Свойство Name переименуйте в TTovar, в свойстве Database выберите IBDB, в свойстве Transaction выберите читающую транзакцию IBTrans1. Теперь в свойстве TableName найдите и откройте таблицу TOVAR, после чего свойство Active переведите в True. Таблица открыта.

Выделите DataSource и переименуйте Name в DSTovar. В свойстве DataSet выберите таблицу TTovar. Теперь можно перейти на главную форму, выделить сетку и навигатор, и в их свойстве DataSource выбрать fDM.DSTovar. Данные должны отобразиться в сетке, а некоторые кнопки навигатора станут недоступны.

Вернемся в модуль данных. Выделите запрос IBQuery. Свойство Name переименуйте в Q1. В свойстве Database выберите IBDB, а в свойстве Transaction - IBTrans2. Более ничего делать не нужно, запросы будем строить программно.

Для добавления новых записей и редактирования существующих создадим еще одну форму командой File -> New -> Form. Окно редактора будет очень простым:

Окно редактора

Рис. 24.2. Окно редактора

Форму назовите fEditor, сохраните в папку проекта, дав модулю имя Editor (так как последним мы открывали файл First.gdb, то при попытке сохранения окна Delphi по умолчанию предложит папку с БД. Измените ее на папку с проектом.). Командой File -> Use Unit подключите к редактору модуль DM. На форме расположите два компонента Label, два простых Edit и две кнопки, как на рисунке 24.2. Не помешает в свойстве BorderStyle формы выбрать bsDialog, а в свойстве Position - poMainFormCenter. И не забудьте очистить текст у компонентов Edit, а у кнопок и компонентов Label изменить свойство Caption в соответствии с рисунком.

Вернемся к главной форме. Командой File -> Use Unit подключите к главной форме модуль Editor. Сгенерируйте событие нажатия на кнопку "Добавить запись". В полученной процедуре впишите код закрытия Q1 (на случай, если ранее запрос был активен), и вызова редактора:

{Добавить запись}
procedure TfMain.Button1Click(Sender: TObject);
begin
  fDM.Q1.Close;
  fEditor.ShowModal;
end;

Таким образом, мы будем добавлять новую запись. А для редактирования существующей записи, выделите сетку DBGrid и сгенерируйте для нее событие OnDblClick. То есть, открывать редактор мы будем по двойному щелчку на записи. При этом в таблице TTovar станет текущей нужная нам запись. Прежде, чем вызывать редактор, нам нужно в Q1 получить нужную запись. Для этого мы создадим запрос, откроем Q1 и только потом вызовем редактор. Итак, код события OnDblClick следующий:

{Двойной щелчок по сетке - редактируем запись}
procedure TfMain.DBGrid1DblClick(Sender: TObject);
begin
  fDM.Q1.SQL.Clear;
  fDM.Q1.SQL.Add('select * from Tovar where ID = '+
     IntToStr(fDM.TTovar.FieldByName('ID').AsInteger));
  fDM.Q1.Open;
  fEditor.ShowModal;
end;

Как видно из кода, в запросе Q1 мы генерируем запрос вроде такого:

SELECT * 
FROM TOVAR
WHERE ID = 3

Разумеется, номер ID будет зависеть от того, по какой записи мы щелкнули. При открытии Q1, в нее попадет лишь одна интересующая нас запись. Больше ничего в главной форме делать не нужно. Переходим к окну редактора.

Тут у нас может быть два варианта: либо мы добавляем новую запись ( Q1 закрыта), либо редактируем существующую ( Q1 открыта и содержит нужную запись). В первом случае нам нужно будет очистить компоненты Edit, если там был текст, а во втором наоборот, вписать в них значения полей Nazvanie и Stoimost.

В раздел глобальных переменных добавим переменную i, необходимую для хранения ID записи:

var
  fEditor: TfEditor;
  i: Integer; //идетификатор записи

Затем выделим форму редактора, и сгенерируем для нее событие OnShow. Код события следующий:

{При показе редактора}
procedure TfEditor.FormShow(Sender: TObject);
begin
  if fDM.Q1.Active then
    i := fDM.Q1.Fields[0].AsInteger
  else i := 0;
  //очищаем или заполняем эдиты:
  if i = 0 then begin
    Edit1.Text := '';
    Edit2.Text := '';
  end //if
  else begin
    Edit1.Text := fDM.Q1.Fields[1].AsString;
    Edit2.Text := fDM.Q1.Fields[2].AsString;
  end; //esle
end;

Как видно из приведенного кода, как только форма станет видимой, мы проверяем - активна ли Q1. Если да, то в переменную i прописываем идентификатор текущей записи, иначе i делаем равным нулю. Это нам нужно для того, чтобы в дальнейшем знать, с какой записью работать. Ведь в Q1 будут помещаться другие запросы, и она уже не будет содержать нужную запись.

Далее, если i = 0 (новая запись), мы очищаем компоненты Edit, иначе считываем в них значения второго и третьего поля запроса ( Nazvanie и Stoimost ).

Пойдем дальше. Для кнопки "Отменить" введем код простого закрытия формы:

//закрываем форму:
  Close;

Так как пользователь может закрыть окно не только кнопкой "Отменить", но и клавишами <Alt + F4> или просто нажав на крестик в правом верхнем углу окна, то проверку на активность транзакции нужно делать в событии формы OnClose.

Сгенерируем для формы редактора событие OnClose, в котором будем проверять, не в работе ли транзакция IBTrans2, и если да, то сделаем откат транзакции:

if fDM.IBTrans2.InTransaction then 
    fDM.IBTrans2.RollbackRetaining;

Прежде, чем перейдем к кнопке "Подтвердить", подумаем вот о чем: для InterBase в вещественных числах между целой частью и дробной должен быть разделитель точка, а пользователь может ввести запятую. Кроме того, в существующих записях разделитель также отображается как запятая, и если мы при показе формы автоматически заполним Edit2, то там тоже будет запятая. Значит, для Edit2 сгенерируем событие OnChange, где устроим простую проверку:

{Изменение данных в Edit2}
procedure TfEditor.Edit2Change(Sender: TObject);
var
  ind: Byte;
  s: String;
begin
  s:= Edit2.Text;
  for ind:= 1 to Length(s) do
    if s[ind] = ',' then s[ind]:= '.';
  Edit2.Text := s;
end;

Здесь, если в тексте будет обнаружена запятая, то она автоматически поменяется на точку. Однако приложение демонстрационное, поэтому мы не делаем проверку на то, что пользователь может ввести не только цифру, точку или запятую, но и какой-нибудь другой символ, например, пробел или букву. Вы можете сделать более детальную проверку, если желаете.

Нам осталось лишь сгенерировать нажатие на кнопку "Подтвердить". И здесь у нас будет два варианта: либо мы добавляем новую запись ( i = 0 ), и тогда мы используем оператор INSERT, либо мы изменяем существующую. В последнем случае будет применяться оператор UPDATE. Код нажатия на кнопку следующий:

{Подтвердить}
procedure TfEditor.Button1Click(Sender: TObject);
begin
  //очищаем SQL-запрос:
  fDM.Q1.SQL.Clear;
  //создаем новый запрос, в зависимости от показателя i:
  if i = 0 then begin //если i = 0, то это новая запись
    fDM.Q1.SQL.Add('insert into Tovar(Nazvanie, Stoimost)');
    fDM.Q1.SQL.Add('values('+QuotedStr(Edit1.Text)+', '+Edit2.Text+')');
  end //if
  else begin //модифицируем существующую запись
    fDM.Q1.SQL.Add('update Tovar set Nazvanie='+
    QuotedStr(Edit1.Text)+
                    ', Stoimost='+
     Edit2.Text+' where ID ='+IntToStr(i));
  end; //else
  try
    //производим изменения:
    fDM.Q1.ExecSQL;
    //подтверждаем транзакцию:
    fDM.IBTrans2.CommitRetaining;
  except
    ShowMessage('Изменения данных не прошли!');
    fDM.IBTrans2.RollbackRetaining;
  end; //try
  //обновляем НД TTovar:
  fDM.TTovar.Refresh;
  //закрываем форму:
  Close;
end;

В случае добавления записи формируется запрос типа

INSERT INTO TOVAR(Nazvanie, Stoimost) VALUES('Товар', 10.00)

В случае редактирования существующей записи формируется другой запрос:

UPDATE TOVAR 
SET Nazvanie = 'Товар', Stoimost = 10.00
WHERE ID = 3

Разумеется, название товара, его стоимость и идентификатор ID будут зависеть от того, что именно введет пользователь в компоненты Edit, и какую строку в таблице выберет.

Обратите внимание, что у компонента IBTrans2 вместо методов Commit (подтверждение) и Rollback (откат) используются методы CommitRetaining и RollbackRetaining, которые также подтверждают или откатывают транзакцию, но не закрывают ее при этом, оставляя активной. В многопользовательской среде, где идет активная работа с базой данных, транзакции лучше закрывать, чтобы в базе данных не "висело" множество активных транзакций.

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

В заключение заметим, что в проектах, которые интенсивно работают с базой данных, совершенно недопустимым будет использование только одной транзакции, это может привести к многочисленным конфликтам. Также недопустимым будет использование отдельной транзакции для каждого набора данных. Находите компромисс: разделяйте наборы данных на читающие, пишущие и НД для отчетов. И для каждой группы используйте отдельный компонент IBTransaction, с соответствующими задаче параметрами.

< Лекция 23 || Лекция 24: 12 || Лекция 25 >
Евгений Медведев
Евгений Медведев

В лекции №2 вставляю модуль данных. При попытке заменить name на  fDM выдает ошибку: "The project already contains a form or module named fDM!". Что делать? 

Анна Зеленина
Анна Зеленина

При вводе типов успешно сохраняется только 1я строчка. При попытке ввести второй тип вылезает сообщение об ошибке "project mymenu.exe raised exception class EOleException with message 'Microsoft Драйвер ODBC Paradox В операции должен использоваться обновляемый запрос'.