FIBPlus事务

来源:互联网 发布:linux 同步时区 编辑:程序博客网 时间:2024/06/07 18:59
 InterBase/Firebird中的事务:在FIBPlus中的应用(第一部)

引言

事务在InterBase/Firebird(及其他数据库服务器)的多用户环境下是非常重要的课题.程序员大多无视任务的环境而直接使用READ COMMITTED隔离级别.

我将讨论在FIBPlus控件中使用事务以及特性.

除了InterBase语言参考、嵌入式SQL指南、API指南及Helen Borrie 所著的FireBird书籍:数据库开发者参考等文档外,还有很多关于事务的文章。这里我不会重复这些内容,但会描述他们的主要特性以及对多用户数据库环境的影响.

我写了一些测试FIBPlus事务的应用程序。其中一些是基于TpFIBTransaction控件的,检查事务参数是否与直接使用SQL语言描述事务的特性一致.

数据库描述

我创建了一个新的数据库FIBTRANSACT.FDB,支持FireBird1.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;

数据表创建脚本(不完全)

 /*** Country reference REFCOUNTRY ***/
CREATE TABLE REFCOUNTRY
( Name DName30, /* Short name of country */
FullName DName60, /* Full name of country */
CodCtr DCodCtr NOT NULL, /* Country code */
Capital DName30, /* Capital */
Region DName30, /* Region name */
Description DDescr, /* Additional information */
CONSTRAINT "Country_PRIMARY_KEY" PRIMARY KEY (CodCtr)
);
COMMIT;
/*** Region reference REFREGION ***/
CREATE TABLE REFREGION
( CodCtr DCodCtr NOT NULL, /* Country code */
CodReg DCodCtr NOT NULL, /* Region code */
Center DName30, /* Name of the region centre */
RegName DName60, /* Region name */
Description DDescr, /* Additional information */
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;

这些表已经使用美国及英国的国家和地区进行了填充.

测试应用程序

Test application

使用Delphi或C++Builder IDE创建新的应用程序;使用TButton创建工具栏;放置两个TDBGrid控件:DBGridContry和DBGridRegion;和两个TDataSource控件:DataSourceCountry和DataSourceRegion.

然后从FIBPlus页签添加如下组件:Database1(TpFIBDatabase类型),Transaction1(TpFIBTransaction类型),两个TpFIBDataSet组件:DataSetCountry和DataSetRegion;和一个ErrorHandler组件:

图1.测试程序主窗体

如何设置必要的Database组件属性值:

数据库名称(DBName属性)-- FIBTRANSACT.FDB (将数据库放置在项目的同目录中).

用户名(UserName)SYSDBA,

密码(Password)masterkey,

字符集(CharSet)WIN1251,

数据库SQL方言(SQLDialect)等于3.

设置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 clause)后数据自动重新排序。

也可在PrepareOptions中将psAskRecordCount属性设置为True。使用这个选项可以在重新打开数据集时获取记录数在状态栏中显示.

调用SQL生成器(双击控件或在右键菜单中选择SQL Generator项)。在数据表列表中选择并双击REFCOUNTRY 项,生成SELECT操作,向其最后面添加ORDER BY NAME子句,使查询出得数据按国家名称排序。

图4.选择生成器.

在Generate Modify SQLs页签中点击Generate SQLs按钮,将自动生成Insert, Update, Delete和Refresh操作语句.

图5.生成修改操作

设置DataSetCountry组件的dataset属性为DataSourceCountry,然后对DataSetRegion做同样操作。为了设置数据集的主细表关系处理要将对DataSetRegion控件指定一个主表的DataSource外,还要将DetailConditions中的dcForceOpen和dcWaitEndMasterScroll属性置为True。修改SQL生成器自动生成的SELECT操作语句:

SELECT CODCTR, CODREG, CENTER, REGNAME, DESCRIPTION
FROM REFREGION WHERE CODCTR = ?CODCTR

ORDER BY CENTER

条件子句WHERE CODCTR = ?CODCTR表明只选择当前国家相关联的记录。更多关于FIBPlus主细表关系的内容请参照www.devrace.com.

现在创建两个窗体:事务特性创建将列出所有的事务参数-- TRParams,事务参数体现出事务参数缓冲区(TPB)中的内容.

图6.事务特性窗体

你需要为事务参数窗体创建事务特性列表。设置左边的ListBox控件名称为AllParameters,设置右边的ListBox名称为SelectedParameters。添加如下TButton.OnClick事件处理函数。点击“>”按钮将左侧ListBox选中的字符串移到右侧的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++很Easy.

可以按下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;

  点击“<”将右侧选中的字符串移到左侧:

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;

点击“<<”将所有右侧字符串移到左侧:

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.事务参数窗体

事务参数窗体显示事务参数列表(TRParams)及事务参数缓冲区向量(TPB)。调用这个窗体后,Memo1控件显示参数列表和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提交事务。提交事务后需要重新启动,然后数据集打开(在事务提交或回滚后会自动关闭):

procedure TFormMain.BCommitClick(Sender: TObject);
begin
  Transaction1.Commit;
  Transaction1.StartTransaction;
  DataSetCountry.Open;
end;

BRollBack按钮的TButton.OnClick事件同样实现方式.

procedure TFormMain.BRollBackClick(Sender: TObject);
begin
  Transaction1.Rollback;
  Transaction1.StartTransaction;
  DataSetCountry.Open;
end;

  点击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按钮调用事务参数窗体显示当前事务特性. 

最后写数据库异常处理功能.当发生异常时可以通过SQLCODE和GDSCODE编码值查看数据库服务器信息。为此需要使用ErrorHandler组件的OnFIBErrorEvent事件:

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;

将fib单元添加到程序模块(uses子句).

注意.这个异常处理语句只显示出异常信息而没有做其他处理.你需要手动关闭事务,要继续工作需要重新启动事务和数据集。当然在实际应用场景中你需要分析错误原因并避免发生异常。本例中的方式适合于测试目的.

InterBase/Firebird中的事务:在FIBPlus中的应用(第二部)

事务特性

所有数据库操作(修改数据及元数据,查询数据等)都在一个事务上下文中执行。这个上下文中的所有修改都可以由事务进行提交(如果没有异常)或回滚。对发生异常的事务操作是不能提交的,只能回滚所有操作.

启动事务和设置事务特性通常使用SQL的SET TRANSACTION命令,而在InterBase/Firebird API中使用isc_start_transaction()函数和事务参数缓冲区(TPB).

SQL的COMMIT操作等同于API的isc_commit_transaction()方法,用来提交事务,ROLLBACK 或 API的isc_rollback_transaction()方法用来回滚事务.

FIBPlus使用相应的API函数与数据库服务器交互。在特定特性下启动事务,开发者需要以所有可能的方式设置事务特性,然后调用组件的StartTransaction方法或设置Active为True.

调用Commit方法提交事务,调用RollBack方法回滚事务。如果提交或回滚了事务,所有的数据集将会关闭。有几个API函数和事务组件的FIBPlus方法可以避免这种情况,提交事务并保持事务上下文(isc_commit_retaining函数,CommitRetaining方法),相应的回滚事务并保持事务上下文(isc_rollback_retaining()函数,RollbackRetaining方法)。这些方法帮助开发者实现软提交/回滚,使工作更加轻松。如不适用这些特性,程序员需要保存当前记录的位置(通常保存记录的主键值),重启事务,打开记录并定位到当前记录。使用CommitRetaining和RollbackRetaining开发者避免了这些操作,但是也会占用更多的服务器资源(缺点)。而且在SNAPSHOT隔离级别下事务软提交或回滚后,其他进程看不到任何修改的数据(需要将事务的Active设为False)。

InterBase/Firebird和FIBPlus也能创建事务的Save Point并可以回滚的Save Point。这是一个非常强大的特性.

为通过事务参数缓冲区(TPB)中设置事务特性需要使用易于记忆的常量。不幸的是常量名称通常与SQL中SET TRANSACTION子句元素不一致.

这里是简单的SET TRANSACTION语法:

SET TRANSACTION
[READ WRITE | READ ONLY] /* access mode */
[WAIT | NO WAIT] /* permission locking mode */
[[ISOLATION LEVEL] /* isolation level */
{SNAPSHOT |
SNAPSHOT TABLE STABILITY |
READ COMMITTED [{RECORD_VERSION |
NO RECORD_VERSION}]]
[RESERVING <reserving clause>]

RESERVING子句设置预留表。RESERVING子句语法如下:

<table> [, <table> ...]
[FOR [SHARED | PROTECTED] {READ | WRITE}]
[, <reserving clause> ...]

  默认事务特性值(如果没有在SET TRANSACTION中定义特性或事务参数缓冲区为空)为:

SET TRANSACTION READ WRITE WAIT SNAPSHOT;

默认特性对应TPB参数为:

isc_tpb_concurrency
isc_tpb_write
isc_tpb_wait

  使用FIBPlus组件可以在TpFIBTransaction组件的TRParams属性中,使用定义在ibase.pas模块的易于记忆的常量来组织事务参数缓冲区.

最简单的特性是存取模式。READ WRITE(isc_tpb_write)是开发者可以在事务上下文中读取和修通数据库数据。READ ONLY(isc_tpb_read)则只能读取数据.

最重要的事务特性是隔离级别。下表展示出三种可能的隔离级别.

InterBase/Firebird中事务隔离级别.

SQL

TPB常量(Constant)

值(Value)

READ COMMITTED

isc_tpb_read_committed

读取已提交的更改。事务可以看到其他事务最近提交的数据库变更。这个隔离级别有两个相反的参数:

默认为NO RECORD_VERSION(isc_tpb_no_rec_version in TPB)要求其他事务提交所有的数据变更后才可见变更数据。

RECORD_VERSION (isc_tpb_rec_version in TPB)可以在其他事务没有提交完毕时即可读取到其已提交的数据变更。

SNAPSHOT

isc_tpb_concurrency

快照级别是默认设置;其另外一个名字是可重复读。在事务启动前获取数据库快照,这个事务看不到其他事务的修改,只能看到自己的修改.

SNAPSHOT TABLE STABILITY

isc_tpb_consistency

这是一个隔离的快照,是串行化的。与SNAPSHOT相似,唯一不同之处在于其他事务可以读取本事务操作的表,但不能修改.

除了上表列出的隔离级别,还有其他的级别:脏读(Dirty Read)或未提交读(Read Uncommitted),在数据库相关书籍中描述。这个隔离级别帮助读取到其他事务未提交的修改。InterBase和Firebird不支持脏读.

另一个重要的事务特性是锁许可模式。如果WAIT(isc_tpb_wait)属性设置为True并出现冲突,事务将会等待锁许可直到其他事务提交或回滚释放了锁。如果NO WAIT(isc_tpb_nowait)设置为True,发生锁定时事务将会报异常并返回相应错误码。WAIT是默认设置。更多信息在后期相应文档中进行描述.

  如果设置事务的TPBMode属性为tpbReadCommitted(READ COMMITTED)隔离级别,这个值将会在控件放置到窗体后进行自动设置,事务启动后这个属性将设置为如下常量值(不管是否指定了TRParams属性值):

isc_tpb_write
isc_tpb_nowait
isc_tpb_rec_version
isc_tpb_read_committed

同样,如果你设置了TPBMode为tpbRepeatableRead(SNAPSHOT隔离级别),TRParams的值变为为:

isc_tpb_write
isc_tpb_nowait
isc_tpb_rec_version

除此之外都保留为默认设置:

isc_tpb_concurrency

因此如果你要在应用程序中直接管理事务特性,需要在TPBMode中设置tpbDefault=True.

注意:发送到TPB的第一个参数总是isc_tpb_version3,用来设置事务版本。使用FIBPlus不需要发送这个参数,因为其将会被自动添加后在发送给API函数isc_start_transaction()。运行应用程序点击“ParamTransact“按钮在弹出窗体中可看到易于记忆的事务参数名称(设置到TRParams的属性)及TPB中定义的真实数据。第一个数字总是3--这是参数isc_tpb_version3的值.

  下面是TPB中的数字参数值。本文不全部列出。参数定义在ibase.pas中.

isc_tpb_version1 = 1;
isc_tpb_version3 = 3;
isc_tpb_consistency = 1;
isc_tpb_concurrency = 2;
isc_tpb_shared = 3;
isc_tpb_protected = 4;
isc_tpb_exclusive = 5;
isc_tpb_wait = 6;
isc_tpb_nowait = 7;
isc_tpb_read = 8;
isc_tpb_write = 9;
isc_tpb_lock_read = 10;
isc_tpb_lock_write = 11;
isc_tpb_verb_time = 12;
isc_tpb_commit_time = 13;
isc_tpb_ignore_limbo = 14;
isc_tpb_read_committed = 15;
isc_tpb_autocommit = 16;
isc_tpb_rec_version = 17;
isc_tpb_no_rec_version = 18;
isc_tpb_restart_requests = 19;
isc_tpb_no_auto_undo = 20;
isc_tpb_last_tpb_constant = isc_tpb_no_auto_undo;

测试事务特性

我们开始测试事务特性.注意程序调用了一个长事务:用户手动启动事务,做不同的数据修改后,最后提交或回滚事务。这将会引起锁定。短事务范例是:用户在数据添加/编辑窗体中点击OK按钮,应用程序调用相应数据集的Insert或Edit方法,设置列的值调用Post方法向数据库发送变更,更新事务将在调用Post方法时启动,Post方法执行完毕后事务提交。这种短事务只生存不到一毫秒,在多用户 环境下降低了锁定的可能性。这里为了检查锁定情况因此使用长事务.

READ COMMITTED隔离级别

READ COMMITTED隔离级别最常用。可使事务读取其他事务提交的修改。我将演示如何使用这个级别。启动两个测试应用程序,点击第一个应用程序的Characteristics按钮设置如下事务特性:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_rec_version
isc_tpb_nowait

  这意味着事务可以读取和写入数据(isc_tpb_write)并且拥有READ COMMITTED隔离级别,等等.事务可以读取到其他事务提交的更改.事务可以见到所有其他事务的更改,即使还有部分记录未提交(isc_tpb_rec_version).如果发生了锁定冲突事务不会等待解决冲突而是直接抛出异常(isc_tpb_nowait).

第二个事务设置如下特性:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_rec_version
isc_tpb_wait

  与第一个事务唯一不同的地方是最后一个字符串.如果发生了锁定冲突第二个事务将会等待其他事务结束.

所有隔离级别中的锁定冲突都发生在两个事务试图同时修改同一条记录.

准备好了吗?现在我可以创建一个冲突并进行解决.

在第一个应用程序中启动一个事务,打开数据集并在DBGridRegion中修改一个列的值.然后点击/导航定位到其他记录.触发的Post操作向数据库发送修改.

在第二个应用程序中做同样的操作:启动事务,打开数据集,修改同一个记录(与第一个应用程序相同),可以修改很多个列的值.定位到其他记录上向数据库发送修改.

发生了什么?第二个应用程序发生了问题:通过观察,有时会看不到鼠标光标.关键点是对第二个应用程序设置了(isc_tpb_wait)模式(等待锁定冲突的解决).在第一个应用程序中提交事务,第二个应用程序不会报出错误信息(事件处理函数是这样处理的).

重复同样的操作:在第一个程序中修改一条记录,在第二个应用程序中修改同一条记录.第二个程序开始等待.第一个程序回滚事务,第二个程序将会无错的将修改提交到数据库.

结论1.长事务中不要使用isc_tpb_wait模式.

在第二个应用程序中修改等待模式:从事务特性中移除isc_tpb_wait参数,并设置isc_tpb_nowait.

注意! isc_tpb_wait模式默认是开启的.需要显示的设置为isc_tpb_nowait

测试中的一个简单范例:准备写一个Demo来验证多用户环境下短事务的行为(见文档"Advantages of using FIBPlus components"),我制造了一个简单的错误并创建了一个长事务.我的十一个学生在客户端机器上同时修改同一条记录并定位到其他记录以便于向服务器上提交修改.如果使用短事务,除了两个机器外都可以很好的并发.而使用长事务的情况下有十台机器发生了异常错误只有第十一台机器更新成功.

现在我将展示其他冲突.在第一个应用程序中按下Ctrl+Del删除USA记录;确保在标准的对话框窗体中进行删除操作.不要提交事务.在第二个程序中删除同样记录.将会获得一个冲突.两个都进行回滚.删除的记录将在次出现在Grid列表中.在程序1中再次删除.尝试在程序2中删除或修改同一个国家的地区记录.会触发其他冲突.在程序1中修改主键值,在程序2中修改或删除区域记录也会发生同样情况.区域表的CodCtr是外键,引用到国家表.外键定义如下:

CONSTRAINT "Region_FOREIGN_KEY"
FOREIGN KEY (CodCtr) REFERENCES REFCOUNTRY (CodCtr)
ON DELETE CASCADE
ON UPDATE CASCADE

这意味着修改主表的主键值会自动修改细表的外键值.

换句话说在试图修改(编辑或删除)其他事务涉及到的表中任意记录都会引起冲突.

注意.约束名称在外键定义中被显示的设置.实际上给列或表设置一个明确的约束名称更有利于获取到数据库服务器上违反约束时的错误信息.

现在做一些其他方面的实验.在第二个程序中关闭数据集.在第一个程序中修改记录但不提交事务.打开程序2的数据集,一切都很正常,第二个程序显示未修改的记录版本.修改程序2的事务特性.移除isc_tpb_rec_version参数并设置为isc_tpb_no_rec_version.启动程序2的事务并尝试打开数据集.将得到一个异常.

这是因为设置了isc_tpb_no_rec_version参数.其要求本事务中涉及的表中的记录如果被其他事务修改必须都进行提交.这里会发生更糟糕的错误:如果在程序1事务中未提交修改的记录,在第二个程序中回滚事务并尝试重新打开数据集,也会得到异常.

结论2.在长事务或多客户端的情况下不要将isc_tpb_no_rec_version和isc_tpb_nowait一同使用.

注意. isc_tpb_no_rec_version参数是默认设置的.需要显示的设置isc_tpb_rec_version.

现在尝试将isc_tpb_no_rec_version和isc_tpb_wait一同设置,将得到很有趣的结果.当然记住如果设置为isc_tpb_rec_version和isc_tpb_wait,冲突的情况下,第二个程序将会处于等待模式;如果第一个程序提交了事务,第二个程序会抛出异常.

如果设置了isc_tpb_no_rec_version和isc_tpb_wait,第二个程序无论在第一个程序提交事务还是回滚事务都不会抛出异常.这个特性在多用户下可以减少甚至避免锁定冲突.在描述分离事务时将详细阐述如何使用这些事务特性..

我们看一下如何设置isc_tpb_read参数.给事务设置这个参数并移除掉isc_tpb_write后尝试修改数据.将得到一个异常.具有isc_tpb_read的事务将禁止数据修改.

是时候描述死锁的情形了:两个程序都等待对方提交或回滚.

将两个程序的事务都设置为如下特性:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_wait
isc_tpb_rec_version

启动事务并打开数据集.在程序1中修改记录,并在程序2中修改其他记录.然后程序2尝试修改程序1中修改的同样的记录.这时程序2因为其事务特性设置了isc_tpb_wait而进入等待模式(等待其他事务解决锁定冲突).现在在程序1中修改程序2修改的记录.程序1也开始等待并死锁,两个应用程序互相等待对方.

实际上发生的情况不是很严重,几秒后第一个应用程序将抛出异常:“死锁.并发更新冲突”.数据库服务器有解决互锁的特性.锁管理器定时分析锁定的持久性可以提高应用程序的性能.在FB1.5中的firebird.conf文件的DeadlockTimeout参数,或InterBase的ibconfig配置文件中的DEADLOCK_TIMEOUT选项上设置锁管理器的分析间隔.默认间隔为10秒.

最后一个测试.在两个程序中回滚事务重新打开数据集.在程序1中修改一条记录并提交事务.在程序2中修改同一条记录并提交.操作成功.现在在两个程序中重新打开数据集,数据库只保存了最后一次操作的修改.

提醒. READ COMMITTED级别的事务要看到其他事务提交的变更只需要重新打开数据集(FIBPlus提供了FullRefresh方法).这样就不需要停止并重启事务了.

SNAPSHOT隔离级别

SNAPSHOT隔离级别是默认的.这时只能读取到事务开始时的数据库数据.在事务中无法看到其他事务修改的数据.事务只能看到其自己修改的变更.

在第一个应用程序中为READ COMMITTED隔离级别设置如下参数:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_rec_version
isc_tpb_nowait

第二个应用程序设置为:

isc_tpb_write
isc_tpb_concurrency
isc_tpb_nowait

在第一个程序中修改一条记录但不提交事务.在第二个应用程序中修改同一条记录,得到一个锁定异常.现在在程序1中修改任意记录并提交.在程序2中关闭数据集再重新打开(注意程序2中事务没有重启).程序2的事务是SNAPSHOT隔离级别的看不到其他事务的提交.这是正确的,因为SNAPSHOT隔离级别显示事务开始时的数据库快照,这个快照不能被其他并发事务修改.其他事务的修改在程序2停止事务后重启事务,打开数据集后才可查看.

一个很有趣的测试:在程序1中修改任意记录并提交.在程序2中试图修改同样记录并提交(看不到程序1的修改).会得到一个异常.我想很多用户都会对这个行为感到疑惑.

这也是我最想阐述的有关SNAPSHOT隔离级别的结论.使用它的情况适合于:或者你不想了解任何外部情况(事务启动后的修改),或者想得到不受控制的异常.

SNAPSHOT TABLE STABILITY隔离级别

这是一个串行化的隔离快照.是最简单的快照,唯一不同之处在于无论何种隔离级别的其他事务都只能读取本事务操作的表,不能修改数据.

修改第二个应用程序的事务特性;使用如下参数:

isc_tpb_write
isc_tpb_nowait
isc_tpb_consistency

启动事务并打开数据集.

在第一个应用程序中尝试修改或删除主表或细表数据会得到异常. SNAPSHOT TABLE STABILITY可以确保独自操作表;其他人不能同时操作.这里有个不是很明显的问题:如果以SNAPSHOT TABLE STABILITY级别启动事务,而其他事务修改了数据但没有提交,打开数据集时会抛出异常.

我们来验证一下.在程序2中停止事务.在程序1的国家表中修改一条记录.现在你可以进行这个操作是因为程序2的事务没有启动.然后在程序2中启动事务并尝试打开数据集.将得到一个异常信息:”非等待事务上发生锁冲突”.在第二个应用程序中停止事务,在第一个应用程序中提交/回滚修改,并在程序2中打开数据集.

在程序1中首先提交修改并在程序2中抛出异常信息中点击OK按钮后观察发生了什么.第二个程序启动时,你将看到程序1中数据集并未修改、执行和未提交!正如你理解的一样,应该在事件处理函数中谨慎应对. 

InterBase/Firebird中的事务:在FIBPlus中的应用(第三部)

如何预留表

SNAPSHOT TABLE STABILITY隔离级别可为事务预留表.这个附加特性使你可以为事务预留表或在SNAPSHOT TABLE STABILITY隔离级别下允许其他事务修改表.在SQL语言中有一个预留操作,在TPB中有一组常量isc_tpb_lock_read, isc_tpb_lock_write, isc_tpb_exclusive, isc_tpb_shared, isc_tpb_protected.有一个范例展示如何在实际中使用它们.

READ COMMITTED隔离级别下编辑表

运行两个应用程序的拷贝.程序1设置如下事务参数:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_nowait
isc_tpb_rec_version
isc_tpb_lock_write=REFCOUNTRY
isc_tpb_protected

对应的SQL语句如下:

SET TRANSACTION
READ COMMITTED READ WRITE
NO WAIT
RECORD_VERSION
RESERVING REFCOUNTRY FOR PROTECTED WRITE;

注意! isc_tpb_protected参数必须紧跟在预留表的后面.

程序2设置标准的READ COMMITTED事务:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_nowait
isc_tpb_rec_version

在两个程序中启动事务并打开数据集.在程序2中尝试修改记录.由于REFCOUNTRY被预留将得到一个锁异常.

现在在应用程序中停止事务并关闭数据集.在程序1中启动事务但不打开数据集.在程序2中启动事务,打开数据集并尝试更改.异常又出现了.可见在事务启动后表就被预留了,而不是数据集打开的时候(像SNAPSHOT TABLE STABILITY).

在第一个事务中使用isc_tpb_exclusive替代isc_tpb_protected.结果是同样的.将isc_tpb_lock_write=REFCOUNTRY 修改为 isc_tpb_lock_read=REFCOUNTRY,结果也是一致的.如果设置了isc_tpb_shared而没有预留表将与READ COMMITTED隔离级别相同.如果在程序2中移除SNAPSHOT和SNAPSHOT TABLE STABILITY隔离级别结果也是一样的.最后一种情况下载程序1中不能修改数据.

因此为READ COMMITTED事务预留的表,将防止被其他任何隔离级别的事务修改.

SNAPSHOT隔离级别下预留表

在程序1中设置如下事务特性:

isc_tpb_write
isc_tpb_concurrency
isc_tpb_nowait
isc_tpb_lock_read=REFCOUNTRY
isc_tpb_exclusive

做同READ COMMITTED一样的测试.得到绝对一致的结果.

在in SNAPSHOT TABLE STABILITY隔离级别下预留表

SNAPSHOT TABLE STABILITY隔离级别不允许其他事务修改其占用的表.但是预留表示允许被修改.在程序1中设置如下事务特性:

isc_tpb_write
isc_tpb_nowait
isc_tpb_lock_write=REFCOUNTRY
isc_tpb_shared
isc_tpb_consistency

如果第二个事务是READ COMMITTED或SNAPSHOT,可以修改指定的表.当然不能并发的修改同一个记录.

其他事务打开数据集.在程序2中可以修改表(isc_tpb_shared).

注意. isc_tpb_shared, isc_tpb_exclusive 和 isc_tpb_protected参数必须放置在isc_tpb_lock_write (isc_tpb_lock_read)之后.被预留操作提及的表名称不能重复.

使用单独的事务

FIBPlus组件允许用户同时使用两个事务操作数据库:一个读取一个更新/写入.我将尽量详细的阐述事务特性.在现有项目的基础上创建一个新的项目Transaction2.在原有测试项目上稍作修改,新添加一个事务组件,命名为Transaction2.作为更新/写入事务.在数据库组件和两个数据集中设置Transaction2作为更新事务(数据库组件的DefaultUpdateTransaction属性和数据集的UpdateTransaction属性).设置数据集的AutoCommit和poStartTransaction(在Options列表中)属性为True.这是一个短更新事务.将会在Post操作(向数据库提交变更)后启动,如果没有异常发生将自动提交.

设置第二个事务的TPBMode为tpbDefault, TRParams属性如下:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_nowait
isc_tpb_rec_version

注意. Transaction2程序只是对测试程序的稍微修改.

启动应用程序并设置第一个事务特性:

isc_tpb_read
isc_tpb_concurrency
isc_tpb_nowait

这些特性相当于SNAPSHOT隔离级别.启动事务,打开数据集并修改记录.点击其他记录触发Post操作并提交修改记录.显示的记录还是原来没修改的列值!只是正确的;读取事务Transaction1看到的是同程序中更新事务Transaction2未提交时的数据版本.即使是关闭数据集重新打开,也看不到变更.需要停止事务,重启事务并打开数据集,这时才可看到新的列值.即使在同一个应用程序中读取事务也看不到其他事务的修改.

我展示了一种很糟糕的情况,但是还能更糟.对读取事务设置如下特性:

isc_tpb_read
isc_tpb_nowait
isc_tpb_consistency

读取事务具有SNAPSHOT TABLE STABILITY隔离级别,因此其他事务不能修改它占有的表.启动事务,重新打开数据集并尝试修改记录.将得到一个锁定冲突.

当然通常读取事务设置如下参数:

isc_tpb_read
isc_tpb_nowait
isc_tpb_read_committed
isc_tpb_rec_version

此外读取事务不能预留表.而更新/写入事务则可以选择任意的隔离级别和附加特性.

如果有一个独立的长读取事务(READ ONLY 和 isc_tpb_read模式),会非常节省数据库服务器资源.

FIBPlus组件高级应用                       

对连接到同一个数据库上的应用程序的锁定问题已经花费了很多时间并作出很多努力.现在我们使用所有的FIBPlus高级特性来创建正确的程序并降低锁的发生.

正确使用分离事务

运行Transaction2程序.设置读取事务Transaction1参数如下:

isc_tpb_read
isc_tpb_read_committed
isc_tpb_nowait
isc_tpb_rec_version

这是一个长只读的READ COMMITTED事务.其好处是最小化占用服务器资源. 

更新事务Transaction2是短事务.在调用Post方法向服务器上提交变更时启动,Post方法后自动提交(如没有异常发生).可以在事务Transaction2的事件中截获事务的启动和提交, AfterStart 或 BeforeStart截获启动, AfterEnd (BeforeEnd)截获关闭事务.测试程序将会在这些事件中输出相应信息(实际应用中可以隐藏).

启动程序2并对读取事务设置相同参数.现在在两个应用程序中修改同一条记录得到一个锁.由于使用了短更新事务,在同一个机器上很难获得锁定.十一个学生和我做了很多次验证,尝试在局域网中并发修改数据库服务器上同一条记录,同时删除一个新的记录并提交事务.这时只得到0到3个锁定冲突.

使用分离的事务可以避免更新冲突.对更新事务设置如下参数:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_wait
isc_tpb_no_rec_version

这时发生锁定冲突的并发进程的事务将会进入等待模式.由于更新事务是很短的因此不会持续很长时间.当并发事务回滚或提交,进程将会继续执行.数据库会保存最后的更改.在网络中的11台机器上验证没有得到冲突.

好与坏完全取决于你,这与所开发的任务有关.

注意.当然当一个进程删除一条记录,另一个进程也尝试产生这条记录还是会发生冲突的,将会抛出异常.

因此如果知道如何正确使用分离的事务,将很少发生锁定异常.

使用poProtectedEdit模式

回到第一个应用程序.设置数据集组件DataSetCountry的Option. poProtectedEdit属性为True.将锁定正在修改的记录.

运行两个程序.设置两个事务的隔离级别都为READ COMMITTED.在程序1中尝试修改一条记录但不提交事务.在程序2中修改同一条记录.

得到一个锁定冲突,但当尝试提交至少一条记录变更时异常信息马上抛出,而不是程序2尝试提交事务(见第一次试验). 使用这个特性可以避免如下繁琐的情形:一个用户花费很多时间修改数据库的一条记录,但由于其他用户已经修改了同一条记录而导致无法提交.

这种FIBPlus模拟更新机制可以帮助避免这种情况:为预留一条记录防止并发修改, FIBPlus显示做一次更新,但对记录不会做任何修改(如设置一个列的同样的值).服务器创建一个不同于最后提交版本的新的记录版本并锁定这个记录.这时在事务提交之前,其他并发用户不能修改这个记录.

注意.这样就可以锁定任意多得记录.当在事务上下文中开始修改新的记录时,这个记录变成了保护/锁定状态.除了提交或回滚事务没有其他方式取消记录的锁定.

InterBase/Firebird中的事务:在FIBPlus中的应用(第四部)

在事务中操作多数据库

InterBase和Firebird允许开发者使用一个事务操作多个数据库.两阶段提交边界事务和其他简单的特性已在其他文档中描述,这里不再详述.

FIBPlus组件对多数据库事务有很强的支持.此外,还有对多数据库操作的其他组件支持.本例我们使用UpdateObject属性(简单修改即可):

这里有两个数据库: COUNTRY.GDB和PERSON.GDB. COUNTRY.GDB中有一个REFCOUNTRY表, PERSON.GDB 有一个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

范例数据库EMPLOYEE.GDB中的表有22条记录.

注意:记录有一个COUNTRY编码列.这是一个外键,使用一个国家编码引用到存储在其他数据库中的REFCOUNTRY表的CODCTR列.无法使用SQL语言来描述不同数据库中表结构间的关系.因此这里不能使用外键和数据库服务器的数据连接完整性特性.也不能使用触发器或存储过程来维护各自的数据库.因此只能使用自己的方式了.

幸运的是FIBPlus提供了必要的方法,现在来讨论一下.

首先创建一个新的MultiBase项目.在窗体中放置两个DBGrid组件:一个是相关的国家,另一个是雇员列表;设置Align属性并放置一个Splitter控件.而后放置两个TpFIBDatabase控件,两个TpFIBTransaction控件(一个读事务,一个写事务),两个数据集控件,两个DataSource控件.将两个TpFIBDatabase控件的DefaultTransaction属性设置为ReadTransaction, DefaultUpdateTransaction设置为WriteTransaction.这样两个数据库都会添加在每个事务的内部数据库列表中.事务会在两个数据库中同时启动.如果在窗体中放置10个数据库组件并设置了默认事务,每个事务都会在这10个数据库中启动.

为验证这个事实我加入一个菜单项: 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 - 1do
    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 - 1do
    S := S + ReadTransaction.Databases[i].Name + #10#13;
  Application.MessageBox(PChar(S), 'ReadTransaction', MB_OK);
end;

如果添加了一个新的数据库组件并设置其默认事务,将会看到事务的数据库列表的包括了这个数据库.

在PersonData组件中我通过写SQL操作设置了事务自动启动,事务自动提交(在CountryData中没有设置).并设置其他的组件属性值使其相互连接.

将菜单放置到窗体中并创建两个项:提交和回滚,负责提交和回滚事务:

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事件中写命令连接事务,启动事务,打开两个数据集.在窗体的Close事件中关闭事务,提交或回滚事务,断开连接.

图8. MultiBase项目

所有功能都正常:两个事务在两个数据库中共享.可以独立的查看和编辑两个表.但事实上我们需要连接到放置在不同数据库上的两个表.

我将使用FIBPlus 的TpFIBUpdateObject组件:在窗体上放置两个TpFIBUpdateObject组件(UpdateObjectEdit和UpdateObjectDelete),用来同步保证PERSON表与REFCOUNTRY表相一致.他们负责实现外键功能: ON UPDATE CASCADE和ON DELETE CASCADE.

UpdateObjectEdit中设置数据库属性为DatabaseData.使SQL执行器在DatabaseData指向的数据库(PERSON.GDB)上执行命令.SQL执行器将引用这个数据库中的表.

设置DataSet属性为CountryData.因此需要设置数据集的名称,并设置触发的数据集操作事件.这个数据集指向另外一个数据库中的表.这里设置其数据集操作事件KindUpdate为ukModify.这样当CountryData数据修改后会自动执行这个组件的SQL命令.设置事务为WriteTransaction组件.这是UpdateObjectEdit组件执行SQL命令是的事务.

写如下SQL属性:

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

这是如何工作的呢?如果用户修改了REFCOUNTRY表并提交更新事务(点击其他记录),组件的SQL开始执行.使用REFCOUNTRY表中新的国家代码替换PERSON表中的原有国家代码.修改仅仅对国家代码等于原国家代码的记录(参数:OLD_CODCTR).

这样就模仿了外键的ON UPDATE CASCADE功能.

同样方式设置UpdateObjectDelete属性.唯一不同的是需要为KindUpdate属性选择ukDelete值并设置如下SQL属性:

DELETE FROM PERSON
WHERE COUNTRY = :OLD_CODCTR

UpdateObjectEdit设置同样的AfterExecute事件处理函数.

运行应用程序.修改国家编码并在服务器上替换外键值.由于事务没有提交,在第二个表中还无法看到修改(写入事务只能查看提交的变更).点击相应菜单项提交事务.现在可以看到PERSON表中的国家代码已经被修改.删除一个国家(Ctrl+Del并在弹出窗体中点击OK).提交事务后所有PERSON表中相关记录都被删除.

可做任意修改和删除操作.人员列表只有在提交事务后才可见.

已经模拟出外键的功能.

现在需要检验如何正确的向PERSON表添加记录并修改外键值.如果国家代码不存在需要禁止修改或插入新记录.外键值可以是NULL,不需要检查列的NULL值.

在COUNTRY.GDB中创建一个异常和存储过程检查NULL值.

CREATE EXCEPTION NO_COUNTRY
 'There is no country with such a code in the country reference';
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的属性如下图设置.

图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;

这里组织UpdateObject组件的CODCTR参数值.

如果新记录添加到PERSON或国家代码不存在,抛出异常” There is no country with such a code in the country reference”.

运行应用程序修改PERSON表的国家代码为一个不存在的值.将得到一个异常.删除国家代码值(设置为NULL),不会发生异常.这正是我们需要的行为.

注意.这个范例只是阐述FIBPlus如何在多数据库环境下工作.实际应用中需要优化代码设置更多选项,尤其是使用其他组件事件,处理自己的异常等.

嵌套事务

InterBase和Firebird支持嵌套事务,也被叫做SavePoint.

如果在一个长事务中嵌套事务可以设置多个数据库状态的修改点.应用程序命名这些SavePoint.查看这些SavePoint可以获取这些时刻的数据库状态(事务提交或回滚前).这时早期的SavePoint被保留,当前的SavePoint(回滚到的)和其后的被取消.

用起来很简单.创建SavePoint的SQL命令为:

SAVEPOINT <identifier>;

Identifier可以是任意少于31个字符的合法数据库对象名称.如果已经存在同名的SavePoint,其将被替换为新的SavePoint.这将会使原有的SavePoint被删除,新的同名SavePoint被创建.

FIBPlus使用事务的SetSavePoint:

SetSavePoint(<identifier>);

使用如下SQL命令回滚到特定SavePoint:

ROLLBACK [WORK] TO [SAVEPOINT] <identifier>;

FIBPlus使用事务组件的方法回滚SavePoint:

RollBackToSavePoint(<identifier>);

使用SQL命令释放SavePoint:

RELEASE SAVEPOINT <identifier> [ONLY];

如果不指定名称将释放同事务中所有的SavePoint,并进行回滚.指定名称会释放其后及其本身所有SavePoint并回滚.

FIBPlus使用事务组件的ReleaseSavePoint方法进行释放:

ReleaseSavePoint(<identifier>);

这个方法释放指定SavePoint及其后面所有的SavePoint.

为验证如何使用SavePoint,创建一个SavePoint范例(通过修改已有程序).创建一个新项目.在窗体中放置一个TPanel,右对齐作为工具栏.放一个关闭应用程序的按钮,一个ComboBox和五个按钮:创建SavePoint,回滚到SavePoint,提交事务,释放,使用已存在名称添加SavePoint.

放置状态栏(显示国家记录数),DBGrid和DataSource.添加FIBPlus组件: TpFIBDatabase, TpFIBTransaction, TpFIBDataSet.

设置数据集的SQL命令,设置长事务(不设置AutoCommit为True).

图10.SavePoint项目

COUNTRY.GDB是范例数据库.

本例主要阐述如何写事件处理程序.首先设置一个私有的SavePoint变量.

  其中保存当前SavePoint序号.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;

应用程序结束后关闭数据集并断开数据库连接.默认激活的事务将回滚.增加SavePoint按钮的点击事件为:

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;

这里创建一个SavePoint名称(必须在事务运行上下文中唯一)并将其加入到ComboBox.SavePoint.创建命令为:

点击回滚按钮ComboBox选中的SavePoint将会回滚:

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 + 1do
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);

其他命令仅仅按名称顺序设置SavePoint.

提交事务后需要将SavePoint列表置为初始化状态:

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

SavePoint的释放命令与回滚命令相同:

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 + 1do
CSavePoints.Items.Delete(i);
CSavePoints.ItemIndex := NewIndex;
if NewIndex = -1 then CSavePoints.Clear;
 SavePoint := CSavePoints.Items.Count;
 DBGrid1.SetFocus;
end;

在ComboBox中SavePoint按创建时间顺序放置,因此如果你创建了一个已存在名称的SavePoint,你需要删除掉相应的项并将新的项放置在列表的最后.

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;

最后需要在事件中写一行代码向状态栏显示国家的数量.

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

好了,执行程序.按需创建一些SavePoint,修改并删除数据,返回一些SavePoint,释放SavePoint,创建同名SavePoint.都正常工作.

由于用户SavePoint使用长更新事务,这些事务只能运行在SNAPSHOT TABLE STABILITY隔离级别.

结论

本文详细阐述了如何使用InterBase/Firebird的事务并写了一个应用程序使用主要的事务特性来使用FIBPlus组件操作数据.

READ COMMITTED在多用户环境下最合适.更新事务必须尽量的短.

SNAPSHOT隔离级别非常不便利,因为很容易发生锁定异常.

如果数据库中表要独占操作,需要使用SNAPSHOT TABLE STABILITY隔离级别.只需要注意如果其他并发事务还有未提交的修改,启动事务并试图打开数据集时会抛出异常.如果使用短更新事务,发生锁定异常概率很低.

如果使用分离事务分别负责读取和写入数据,读取事务必须使用READ COMMITTED.写事务根据任务需要可以选择任意隔离级别及事务特性.

常用的写事务是WAIT+ NO RECORD_VERSION模式,或NO WAIT+ RECORD_VERSION

WAIT+ NO RECORD_VERSION可以避免提交或回滚事务后抛出异常(如果事务试图修改已被其他进程删除的记录时会抛出异常).本例中使用NO WAIT+RECORD_VERSION将会立即得到异常,而后决定如何继续操作.

多用户并发情况下不适合使用NO RECORD_VERSION.如果有人正在修改数据库中的数据将很难启动事务.本例需要获取最新版本的数据变更和数据更新,此模式才比较适合.

如果正确的在FIBPlus中使用两个事务(读和写数据),就可以增加服务器资源效率并减少用户锁定等问题.

使用FIBPlus的保护编辑模式可以防止用户修改或删除正在被其他用户编辑的记录.

InterBase/Firebird服务器(和FIBPlus组件)可以在多个数据库上启动事务.本例中多数据库事务工作原理与单数据库事务相同.为了这个目的FIBPlus提供了一个强大的特性支持: theUpdateObject组件.

嵌套事务帮助分割长更新事务可以分部分进行回滚.

我前面提到锁定是经常发生的.可能这也是为什么你认为这非常糟糕而且需要尽量避免.当然这是不对的,所有的事情都取决于任务需要.经常需要在数据库中锁定其他客户端的修改.实际应用中可以使用两种方法:硬锁定(SNAPSHOT TABLE STABILITY)和保护编辑模式. InterBase/Firebird和FIBPlus组件对CS架构应用程序中的各种任务都提供了支持. 

 

原创粉丝点击