看到一篇介绍ClientDataSet和TDataSetProvider,非常精彩,特此保存。
===========================================================================
TClientDataSet用法
第十一章 TClientDataSet
与TTable、TQuery一样,TClientDataSet也是从TDataSet继承下来的,它通常用于多层体系结构的客户端。TClientDataSet最大的特点是它不依赖于BDE(Borland Database Engine),但它需要一个动态链接库的支持,这个动态链接库叫DBCLIENT.DLL。在客户端,也不需要用TDatabase构件,因为客户端并不直接连接数据库。
由于TClientDataSet是从TDataSet继承下来的,所以,它支持诸如编辑、搜索、浏览、纠错、过滤等功能。由于TClientDataSet在内存中建立了数据的本地副本,上述操作的执行速度很快。也正是由于TClientDataSet并不直接连接数据库,因此,客户程序必须提供获取数据的机制。在Delphi 4中,TClientDataSet有三种途径获取数据:
.从文件中存取数据。
.从本地的另一个数据集中获取数据。
.通过IProvider接口从远程数据库服务器获取数据。
在一个客户程序中,可以同时运用上述三种机制获取数据。
11.1 浏览和编辑数据
和其他数据集构件一样,可以用标准的数据控件显示由TClientDataSet引入的数据集,当然,这需要借助于TDataSource构件。
由于TClientDataSet是从TDataSet继承下来的,所以,凡是其他数据集构件支持的功能,TClientDataSet构件也大致具备。不同的是,TClientDataSet能够在内存中建立数据的副本,因此,TClientDataSet比其他数据集构件增加了一些特殊的功能。
11.1.1 浏览数据
可以用标准的数据控件显示由TClientDataSet引入的数据集。在运行期,可以调用诸如First、GotoKey、Last、Next和Prior等函数来浏览数据。
TClientDataSet也支持书签功能,可以用书签来标记某条记录,以后就可以方便地找到这条记录。
对于TTable、TQuery等数据集构件来说,只能读RecNo属性来判断当前记录的序号。对于TClientDataSet构件来说,还可以写RecNo属性,使某一序号的记录成为当前记录。
11.1.2 CanModify属性
TDataSet的CanModify属性用于判断数据集中的数据是否可以修改。CanModify属性本身是只读的,也就是说,数据是否能够修改不取决于应用程序。
不过,TClientDataSet构件有其特殊性,因为TClientDataSet已经把数据在内存中建立了副本,因此,应用程序可以决定是否允许修改数据。如果不允许用户修改数据,只要把ReadOnly属性设为True,此时,CanModify属性肯定返回False。
与其他数据集构件不同,修改TClientDataSet构件的ReadOnly属性时,不需要事先把Active属性设为True。
11.1.3 取消修改
TClientDataSet传输数据的基本单位称为数据包,当前的数据包可以由Data属性来访问。不过,用户对数据的修改并不直接反映到Data属性中,而是临时写到一个日志即Delta属性中,这样做的好处是以后随时可以取消修改。
不过,这里要说明一点,尽管用户的修改并没有反映到Data,当用户在数据控件中看到的却是最新修改的数据。如果一条记录被反复修改了多次,用户看到的只是最新的数据,但日志中却记载了多次。
要取消上一次的修改,调用UndoLastChange函数。UndoLastChange需要传递一个布尔类型的参数叫FollowChange,如果FollowChange参数设为True,光标就移到被恢复的记录上,如果FollowChange参数设为False,光标仍然在当前记录上。
ChangeCount属性返回日志中记载的修改次数。如果一条记录被反复修改了多次,每调用一次UndoLastChange能够逐级取消上一次的修改。
UndoLastChange只能取消上一次的修改,如果想一下子取消所有的修改,首先要选择一个记录,然后调用RevertRecord。RevertRecord将从日志中取消所有对当前记录的修改。
TClientDataSet还有一个SavePoint属性,它能把当前的编辑状态保存起来,以后随时可以返回当时的状态。例如,可以这样保存当前的状态:
BeforeChanges := ClientDataSet1.SavePoint;
以后,可以这样来恢复当时的状态:
ClientDataSet1.SavePoint := BeforeChanges;
应用程序可以保存多处状态,可以恢复其中一个状态,不过,一旦某个状态被恢复,在其之后的状态就无效。
如果要一下子取消日志中记载的所有修改,可以调用CancelUpdates函数。CancelUpdates将把日志清空,取消所有的修改。
如果LogChanges属性设为False,用户对数据的修改就会直接反映到Data属性中。
11.1.4 合并修改
要把日志中记载的修改合并到Data属性中,有两种方式,具体使用哪一种方式,取决于应用程序获取数据的机制。不过,不管是哪种机制,合并后,日志自动被清空。
对于一个从文件中获取数据的程序来说,只要调用MergeChangeLog函数,就把日志中记载的修改合并到Data属性中。不用担心其他用户同时修改了数据。
对于一个从应用服务器获取数据的程序来说,就不能调用MergeChangeLog来合并数据,而要调用ApplyUpdates函数,ApplyUpdates会把日志中记载的修改传递给应用服务器,待应用服务器成功地把数据更新了数据库服务器后,才会合并到Data属性中。
11.1.5 纠错
TClientDataSet支持纠错功能。一般情况下,需要自己建立纠错规则,以便对用户输入的数据进行纠错。
此外,如果获得了IProvider接口的话,还可以从远程服务器引入纠错规则。
有时候,客户端可能需要暂时禁止纠错,因为客户端从应用服务器检索数据是分阶段进行的,在所有的数据检索完毕之前,有些纠错规则很可能会报错。
要暂时禁止纠错,可以调用DisableConstraints,要重新允许纠错,可以调用EnableConstraints函数。DisableConstraints和EnableConstraints实际上都是作用于一个内部的计数。
11.2 索 引
使用索引有这么几个好处:
.在数据集中定位记录比较快。
.能够在两个数据集之间建立Lookup或Master/Detail关系。
.可以对记录排序。
在多层体系结构中,当客户程序从应用服务器检索数据时,它同时获得了默认的索引。默认的索引叫DEFAULT_ORDER,可以使用这个索引排序,但不能修改或删除这个索引。
除了默认的索引外,TClientDataSet还对日志中记载的记录自动建立了一个副索引叫CHANGEINDEX。与DEFAULT_ORDER一样,不能修改或删除这个副索引。
另外,还可以使用数据集中已建立的其他索引,或者自己建立索引。
11.2.1 创建一个新的索引
要创建一个新的索引,可以调用AddIndex。AddIndex需要传递若干个参数:
一是Name参数,用于指定索引名。在运行期切换索引时需要用到索引的名称。
二是Fields参数,它是一个字符串,用于指定索引中的字段名,彼此之间用分号隔开。
三是Options参数,用于设置索引的选项,包含ixDescending元素表示按降序排列,包含ixCaseInsensitive元素表示大小写不敏感。
四是DescFields参数,它也是一个字符串,用于指定若干个字段名,这些字段将按照降序排列。
五是CaseInsFields参数,它的作用与DescFields参数类似,包含在CaseInsFields参数中的字段将对大小写不敏感。
六是GroupingLevel参数,用于指定分组级别,其值不能超过索引中的字段数。
下面的代码创建了一个索引:
If Edit1.Text <> /‘/‘ and ClientDataSet1.Fields.FindField(Edit1.Text) then
Begin
ClientDataSet1.AddIndex(Edit1.Text+/‘Index/‘,Edit1.Text,
[ixCaseInsensitive],/‘/‘,/‘/‘,0);
ClientDataSet1.IndexName := Edit1.Text + /‘Index/‘;
End;
为了避免创建一个索引,可以临时用IndexFieldNames属性来指定若干个字段,让数据集按这些字段排序。
11.2.2 删除和切换索引
要删除一个先前创建的索引,可以调用DeleteIndex并指定要删除的索引名称。注意:DEFAULT_ORDER和CHANGEINDEX不能删除。
如果建立了多个索引,可以任意选择其中的一个索引,这就要用到IndexName属性。
11.2.3 用索引把数据分组
选择了一个索引后,数据集将自动按其中的字段进行排序。这样,临近的记录往往在关键字段上含有相同的值。例如,假设有一个表是这样的:
SalesRep Customer OrderNo Amount
1 1 5 100
1 1 2 50
1 2 3 200
1 2 6 75
2 1 1 10
2 3 4 200
可以看出,SalesRep字段的值有重复的。对于SalesRep字段的值为1的来说,Customer字段的值也有重复的。这就是说,可以按SalesRep字段分组,进而再按Customer字段分组。显然,这里的分组级别是不同的,按SalesRep字段建立的分组属于第一级,按Customer字段建立的分组属于第二级。实际上,分组级别取决于字段在索引中的顺序。
TClientDataSet可以决定是否按照分组级别来显示记录的值。例如,也许想以下面这种形式显示数据:
SalesRep Customer OrderNo Amount
1 1 5 100
2 50
2 3 200
6 75
2 1 1 10
2 3 4 200
要判断当前记录某一级的什么位置,可以调用GetGroupState函数。GetGroupState函数需要传递一个参数,用于指定分组级别。
11.3 计 算 字 段
与其他数据集一样,也可以在TClientDataSet建立的数据集中增加计算字段。计算字段的值是基于同一个记录中的其他字段计算出来的。
在其他数据集中,只要用户修改了数据或当前记录发生改变,就会触发OnCalcFields事件,换句话说,计算字段的值就被计算一次。
TClientDataSet引入了“内部计算字段”的概念。与一般的计算字段不同的是,内部计算字段的值将随其他字段的值一起存取,这样,只有当用户修改了数据才会触发OnCalcFields事件,如果仅仅改变了当前记录,不会触发OnCalcFields事件。也就是说,内部计算字段的值需要重新计算的机会大大减少。
在处理OnCalcFields事件的句柄中,首先要判断State属性。如果State属性返回dsInternalCalc,此时需要计算内部计算字段的值。如果State属性返回dsCalcFields,此时需要计算一般的计算字段的值。
11.4 统 计 值
TClientDataSet增加了统计的功能,它可以基于分组自动计算总和、平均、计数、最大、最小值。当用户编辑数据时,这些统计值会自动跟着变化。
11.4.1 指定统计方式
要指定怎样进行统计,就要用到Aggregates属性。这个属性是一个TAggregates对象,它用于管理一组TAggregate对象。
在设计期,可以单击Aggregates属性边上的省略号按钮打开如图11.1所示
的编辑器。
图11.1 管理一组TAggregate对象
单击按钮可以增加一个TAggregate对象,单击按钮可以删减一个TAggregate对象,单击按钮可以把TAggregate对象前移,单击按钮可以把TAggregate对象后移。
可以用字段编辑器专门创建一个用于表达统计值的字段,该字段的类型必须是“Aggregate”。Delphi 4会自动创建一个TAggregate对象,并加到Aggregates属性中。选择一个TAggregate对象,Object Inpector将显示该对象的属性。
其中,Expression属性用于指定统计表达式,例如:
Sum(Field1)
也可以是比较复杂的表达式:
Sum(Qty * Price) - Sum(AmountPaid)
在表达式中,可以使用下列统计运算符:
.Sum计算一组数据的总和。
.Avg计算一组数据的平均值。
.Count计算一组数据中的非空值的个数。
.Min计算一组数据的最小值。
.Max计算一组数据的最大值。
除了上述几个统计运算符外,还可以使用过滤条件中所能使用的运算符,但不能嵌套。在一个表达式中,可以混合出现几个统计值或常量,但不能混合出现统计值和字段。
Sum(Qty * Price){合法}
Max(Field1) - Max(Field2){合法}
Avg(DiscountRate) * 100{合法}
Min(Sum(Field1)){非法,不能嵌套}
Count(Field1) - Field2{非法,统计值和字段不能混合出现在一个表达式中}
11.4.2 指定分组
默认情况下,统计值是基于数据集中所有的记录计算出来的。不过,也可以针对一部分记录计算统计值,这就需要事先建立分组。
前面在介绍索引时已经提到分组的概念。可以通过IndexName属性和GroupingLevel属性来选择使用哪个索引以及最大的分组级别。
例如,假设有一个表是这样的:
SalesRep Customer OrderNo Amount
1 1 5 100
1 1 2 50
1 2 3 200
1 2 6 75
2 1 1 10
2 3 4 200
如果要按SalesRep字段分组,并且指定其中的第一级,程序代码应当这样写:
Agg.Expression := /‘Sum(Amount)/‘;
Agg.IndexName := /‘SalesCust/‘;
Agg.GroupingLevel := 1;
Agg.AggregateName := /‘Total for Rep/‘;
11.4.3 怎样获取统计值
要获取统计值,可以调用TAggregate对象的Value函数。如果统计值是基于数据集中所有的记录计算出来的,随时可以调用Value函数。如果统计值是基于分组计算出来的,必须保证当前记录正好位于该分组内。因此,在调用Value之前,最好先调用GetGroupState函数看看当前记录是否位于该分组内。
要在数据控件中显示统计值,必须事先在字段编辑器中创建一个永久字段对象,该字段的类型必须是Aggregate。
11.5 数 据 包
通过Data属性可以访问客户程序从应用服务器检索到的数据。程序示例如下:
Procedure TForm1.Button1Click(Sender: TObject);
Begin
ClientDataSet1.Data := ClientDataSet1.Provider.DataRequest(FilterEdit.Text);
End;
11.5.1 直接对Data属性赋值
前面讲过,客户程序既可以通过IProvider接口获取数据,也可以从另一个数据集获取数据,后者就是通过Data属性赋值的。程序示例如下:
ClientDataSet1.Data := ClientDataSet2.Data;
一旦Data被赋值,就可以用标准的数据控件显示这些数据。
注意:当从另一个数据集获取数据时,另一个数据集的日志也将被复制过来,但不包括原来的范围和过滤条件。
如果要从另一个基于BDE的数据集中获取数据,可以通过数据集构件的Provider属性,程序示例如下:
ClientDataSet1.Data := Table1.Provider.Data;
如果要从一个自定义的数据集获取数据,首先要创建一个临时的TProvider构件,然后设置其DataSet属性指定这个自定义的数据集。程序示例如下:
TempProvider := TDataSetProvider.Create(Form1);
TempProvider.DataSet := SourceDataSet;
ClientDataSet1.Data := TempProvider.Data;
TempProvider.Free;
11.5.2 在数据包中加入自定义的信息
可以把自定义的信息加到数据包中。当把数据保存到文件或流中时,这些自定义的信息也将保存到文件或流中。如果把数据包直接赋值给另一个数据集的话,这些自定义的信息也将被复制。
要把自定义的信息加到数据包中,可以调用SetOptionalParam函数。要从数据包中检索自定义的信息,可以调用GetOptionalParam。程序示例如下:
Procedure TAppServer.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
var
WhenProvided: TDateTime;
Begin
WhenProvided := DataSet.GetOptionalParam(/‘TimeProvided/‘);
...
End;
11.5.3 克隆另一个数据集
调用TClientDataSet的CloneCursor函数可以获得一个数据集的完全相同的副本。它与直接通过Data属性赋值是有区别的。
区别之一:数据在两个数据集之间是共享的,修改其中一个将同时修改另一个。
区别之二:除了数据外,CloneCursor函数还复制了一些属性和事件,这取决于Reset和KeepSettings参数怎样设置。
CloneCursor函数需要传递三个参数,其中,Source参数指定源数据集,Reset参数和KeepSettings参数用于设置除了数据外是否还要复制下列属性和事件:Filter、Filtered、FilterOptions、OnFilterRecord、IndexName、MasterSource、MasterFields、ReadOnly、RemoteServer、ProviderName、Provider。
如果Reset和KeepSettings参数都设为False,源数据集的上述属性和事件都将被复制给目标数据集。如果Reset参数设为True,目标数据集的上述属性和事件都将被清空。如果Reset参数设为False,而KeepSettings参数设为True,目标数据集的上述属性和事件不变,不过,必须保证这些属性和事件与克隆后的数据相容。
11.6 与应用服务器通讯
在多层体系结构中,客户程序通过IProvider接口与应用服务器交换数据。这一章介绍怎样在客户端获得IProvider接口、怎样向应用服务器传递参数、怎样向应用服务器请求数据、怎样把用户对数据的修改写到数据库中。
11.6.1 怎样在客户端获得IProvider接口
在单层应用程序以及工作在“公文包”模式下的多层应用程序中,不需要用到IProvider接口。而在多层体系结构中,客户程序要与应用服务器交换数据,首先必须获得IProvider接口,这就要用到RemoteServer属性和ProviderName属性。
RemoteServer属性用于指定客户端的MIDAS连接构件。MIDAS连接构件又称Data Broker,用于建立和维护与应用服务器的连接。
在设计期,正确设置了RemoteServer属性后,就可以在对象观察器中为ProviderName属性选择一个值,实际上就是选择应用服务器上的一个TProvider构件。
11.6.2 向应用服务器传递参数
客户程序可以向应用服务器传递参数,这些参数实际上是传递给应用服务器上的TQuery构件或TStoredProc构件。既可以在设计期也可以在运行期设置参数。
在设计期,可以单击Params属性边上的省略号按钮,打开一个如图11.2所示的编辑器。
图11.2 设置参数
单击按钮可以增加一个参数,单击按钮可以删减一个参数,单击按钮可以把一个参数前移,单击按钮可以把一个参数后移。
选择一个参数,对象观察器将显示该参数(TParam对象)的属性。
在运行期可以调用TParams的CreateParam函数来创建一个参数。例如,下面的代码创建了一个参数叫CustNo,它的使用类型是ptInput,数据类型是ftInteger,它的值设为605。
With ClientDataSet1.Params.CreateParam(ftInteger, /‘CustNo/‘, ptInput) Do
AsInteger := 605;
设置好参数以后,如果TClientDataset的Active属性是False,只要把Active属性设为True,这些参数将被自动传递给应用服务器。如果Active属性已经为True,就要调用SendParams函数把参数传递给应用服务器。
注意:传递给应用服务器的参数必须与TQuery构件或TStoredProc构件的参数匹配,包括名称、数据类型和参数类型。
11.6.3 怎样向应用服务器请求数据
TClientDataSet提供了两个属性和三个方法,用于怎样向应用服务器请求数据:
一是FetchOnDemand属性。如果这个属性设为True,TClientDataSet会根据需要自动检索附加的数据包,例如BLOB字段的值或者嵌套表的内容。如果这个属性设为False,程序需要显式地调用GetNextPacket才能获得这些附加的数据包。
二是PacketRecords属性,用于设置一个数据包中最多可容纳的记录数,设为-1表示一个数据包可以容纳数据集的所有记录。
三是GetNextPacket函数,用于向应用服务器检索下一个数据包,并把检索到的数据包添加到前一次检索到的数据包的后面。这个函数返回实际检索到的记录数。
四是FetchBlobs过程,用于从应用服务器检索BLOB字段的值。如果FetchOnDemand属性设为True,就没必要调用FetchBlobs函数。
五是FetchDetails过程,用于检索嵌套表中的数据。如果FetchOnDemand属性设为True,就没必要调用FetchDetails函数。
11.6.4 更新数据库
在多层体系结构中,用户在客户端修改了数据后,需要把最新的数据写到数据库中,这就要调用TClientDataSet的ApplyUpdates函数。
ApplyUpdates只需要传递一个参数叫MaxErrors,用于指定一个整数,当遇到无法更新的记录超过这个数时,此次更新就中止。如果MaxErrors参数设为0,表示只要遇到一个错误更新就中止,客户端的日志保持不变。如果MaxErrors参数设为-1,当应用服务器发现有错误的记录,就尝试更新下一个记录,等所有的记录都尝试过以后才返回。
ApplyUpdates会自动调用Reconcile函数,进而调用应用服务器上的TProvider构件的ApplyUpdates函数去更新远程的数据库服务器。没有被DBMS服务器认可的记录通过Reconcile返回给客户端,此时将在客户端触发OnReconcileError事件让您更正错误。最后,ApplyUpdates函数返回仍然没有被认可的记录数。
11.7 在文件中存取数据
要从文件中读取数据,可以调用LoadFromFile函数。LoadFromFile函数需要传递一个参数,用于指定文件名。文件名应包含完整的路径。如果客户程序总是从一个固定的文件中读取数据,可以设置FileName属性指定一个文件名,以后,当TClientDataSet引入的数据集打开时,就自动从这个文件中读取数据,不需要调用LoadFromFile。
要从流中读取数据,可以调用LoadFromStream。LoadFromStream需要传递一个参数,用于指定一个流对象。
注意:LoadFromFile(LoadFromStream)只能从先前用SaveToFile(SaveToStream)保存的文件中读取数据。
要把数据保存到文件中,可以调用SaveToFile函数。SaveToFile需要传递一个参数,用于指定文件名。如果指定的文件已存在,文件中的数据将被覆盖。如果客户程序总是把数据保存到一个固定的文件中,可以设置FileName属性指定一个文件名,当TClientDataSet引入的数据集关闭时,就自动把数据保存到这个文件中,不需要调用SaveToFile。
要把数据保存到流中,可以调用SaveToStream。SaveToStream需要传递一个参数,指定一个流对象。
注意:当把数据保存到文件或流中时,日志中记载的修改仍然保留。这样,当下次调用LoadFromFile或LoadFromStream读取数据时,仍然可以恢复原来的数据
--------------------------------------------------------------------------------
-- 作者:gzkhrh
-- 发布时间:2005-7-29 8:42:46
--
我们也跟着学学
==============
Delphi做为一个快速应用开发工具,深受程序员的喜爱。其强大的组件功能,让程序员能够轻松、高效地完成常见的界面开发、数据库应用等功能。然而,帮助的相对缺乏,使得许多组件的功能并不为人们正确地使用,究其原因,仍然是认识上的问题。对于MIDAS开发中的核心部件,TClientDataSet和TDataSetProvider,由于资料的缺乏,人们在网上大多谈论的是李维的书籍内容。我有幸在BDN上见到了Cary Jensen的Professional Developer系列文章,详细阐述了DELPHI的数据库开发技术。现节选出其中的ClientDataSet部分,与大家共同分享。
ClientDataSet是一个功能强大的类,通过在内存中模拟表格,实现了其它数据集组件所不具备的强大功能。以往只在Delphi和C++ Builder企业版中才提供这个组件,如今,Borland的全部产品(包括最新的Kylix)都集成了TClientDataSet组件。
TClientDataSet从类的继承关系上来看,是TDataSet这个抽象类的子类,所以我们可以在TDataSet这个抽象层次上对其进行我们熟悉的操作,比如导航、排序、过滤、编辑。要注意的是,TClientDataSet使用了一种全新的技术,它将所有的数据均放在内存中,所以TClientDataSet是个只存在内存中的“虚拟表”,因此对数据库的操作是非常快的。在PIII 850,512MB的机器上对十万条记录进行建索引的操作,花费的时间少于半分钟。
与一般的数据集组件不同,TClientDataSet使用的技术比较特别,本着高速度、低存储需求的原则,TClientDataSet的内部使用了两个数据存储源。第一个是其Data属性,这是当前内存数据的视图,反映了所有的数据改变。如果用户从数据中删除一条记录,则此记录将从Data中消失,相应地,加入一条新记录后,此记录便存在Data属性中了。
另一个数据源是Delta属性,故名思义,即增量的意思,这个属性反映了对数据的改变。无论是向Data属性新增还是删除记录,都会在Delta中记录下来,如果是修改了Data中的记录,则会在Delta保存两条相应的记录,一条是原始记录,另一条仅包含修改的字段值。正因为Delta的存在和TClientDataSet在内存中记录数据的特点,所有的改变都没有立即更新加对应的物理存储中,可以根据这些信息在适当的时候恢复,所以TClientDataSet天生具有缓冲更新功能。
为了使数据更新回数据存储源,我们要调用TClientDataSet中对应的方法。如果ClientDataSet与DataSetProvider关联,那么仅需调用TClientDataSet的ApplyUpdates方法即可保存数据的更新,但如果TClientDataSet没有对应的TDataSetProvider存在,而是直接同文件关联,那么,这种方式是非常有趣的,我们在BriefCase模型中会再次讲解这个问题。此时,如果使用TClientDataSet的SaveToFile和LoadFromFile,都会保留着Delta。调用MergeChangeLog和ClearChanges后,Delta的内容才会被
清空。只是前者是将Delta的数据同Data结合起来,将改变存储到物理介质上,而ClearChanges则是一股脑儿全部清空,将数据回复到原始状态。大部分的应用都是将TClientDataSet与TDataSetProvider结合使用的。两者联合使用的行为反映了Borland的设计宗旨,就是要提供一个面向分布式环境的思路。我们下面来慢慢解释。
当我们将TClientDataSet对象的Active属性设为True或者调用其Open方法后,ClientDataSet会向DataSetProvider发送一个取数据包请求。于是DataSetProvider便会打开对应的数据集,将记录指针指向第一条记录,然后从头到尾依次扫描。对于扫描到的每一条记录,都会将其编码成一个variant数组,我们通常将它称之为数据包。完成扫描后,DataSetProvider会关闭指向的数据集,并将所有的这些数据包传递给ClientDataSet。在我提供的演示程序中,你可以清楚地看到这种行为(毕竟眼见为实吗!)。程序主界面右边的DBGrid连接到一个指向数据库表的数据源,DataSetProvider即指向此表。当选择了ClientDataSet | Load菜单项时,你可以看到表格的数据被依次扫描,一旦到达最后一条记录,表格便会被关闭,右边的DBGrid被清空,而左边反映ClientDataSet数据的DBGrid便出显示出内存中的数据来。由于这个过程会在DBGrid上反映出来,所以不到1000条记录的取出时间中,大部分都浪费在屏幕的更新显示上了,你可以选择ClientDataSet | View Table Loading来禁止显示,而达到加速的目的。
在上面的描述中,我们没有提到一个重要的环节,即数据包是如何还原成表格的。那是因为DataSetProvider会将数据包中的元数据解码出来,根据元数据(我们可以理解为数据表的结构)便可以构造出与物理数据表一模一样的内存虚拟表。但要注意的是,尽管DataSetProvider指向的数据表可能有多个索引,但这些信息是不会放在数据包中的,换句话说,ClientDataSet当中的数据默认情况下是无索引的。但因为ClientDataSet具有与TDataSet一致的行为,所以我们可以在此基础上根据需要重建索引。
在ClientDataSet中的数据被修改后,可以提交给物理数据表持久化这此改变。这个工作便是由DataSetProvider完成的。内部工作原理是:DataSetProvider创建一个TSQLResolver的实例,这个实例会生成要在底层数据上执行更改的SQL语句。详细地说,就是对修改日志中的每一条被删除、插入、更改记录生成对应的SQL语句。这个语句的生成也可以由用户控制,DataSetProvider的UpdateMode属性和ClientDataSet中的ProviderFlags属性都对SQL语句的生成有影响。
当然,你也可以换一种方式,即采取同单机或C/S结构一样的数据直接操作机制,绕过SQL语句和缓冲更新机制来修改数据库。只需将ResolveToDataSet属性设为True,那么DataSetProvider在持久化更新时便不会使用TSQLResolve,而是直接修改物理数据源。即定位到要删除的记录,调用删除语句,定位到修改记录,调用修改语句。我们可以对演示程序稍加修改,观察此种行为。请将演示程序中的DataSetProvider的ResolveToDataSet属性由False改为True,运行。在界面中修改数据并且保存,你将会看到右边的导航按钮会在瞬间变得可用。
更绝妙的是,Borland考虑到了应用的多样性,为我们提供了BeforeUpdateRecord事件,这样,当DataSetProvider对每个修改日志的记录进行操作时,都会触发此事件,我们可以在此事件中加入自己的处理,如“加密操作”、“商业敏感数据处理”等应用,从而极大地方便了程序员,让程序员对于数据具有完全的控制能力。分布式环境的复杂性对数据的存取提出了更高的要求,所以使用事务来保证数据的完整性和一致性是非常必要的,Borland考虑到了这一点,当调用ClientDataSet的ApplyUpdates时,你可以传递一个整数值来指明可以容忍的错误数量。如果你的数据非常严格,则可以传递0值,这样,DataSetProvider在应用修改时便会打开一个事务,如果遇到错误,便会回退此事务,修改日志将保持原样,并且将出错的记录标记出来,最后会触发OnReconcileError事件。如果传递了一个大于0的数,则当出现的错误数量小于此指定值时,事务会被提交,发生错误而导致提交失败的记录会保留在Delta中,而提交成功的记录会从修改日志中删除。若错误数量达到指定值,则事务会回退,结果同整数值为0的情况。如果值为负数,则会交所以可提交的数据都提交,不可提交的数据仍然保存在修改日志中,并将出错记录标记出来。
虽然,Borland是为了满足分布式编程的需要而设计了TClientDataSet,但在其它类型的编程环境中使用ClientDataSet也具有积极的意义。首先,我们可以看到,由于数据均在内存中进行操作,而且仅在打开数据库取数据时和将修改持久到回数据库时,才有数据库开销,其它时间数据库为零,这样就极大地增加了数据库的负荷,让数据库服务器能满足更多用户的连接请求。其次,ClientDataSet具有其它数据集所不具备的许多高级功能,这为程序员进行复杂的编程提供了便利,可以不考虑数据库本身是否支持这此功能,而让ClientDataSet去处理这些复杂而繁琐的细节。最后,ClientDataSet在数据存储和应用程序间起到一个抽象层的作用。假如你的程序使用了TClientDataSet,那么如果你以后要更改数据库存储机制。比如说由BDE移植到dbExpress,或者从ADO移植到Interbase Express,你的用户界面和数据控制部分几乎就不用改变,只需要将DataSetProvider指向新的数据存取组件即可。顺便说一句,由于缓冲更新的存在,用户可能非常厌恶调用ApplyUpdates操作,那么你可以将此调用放入AfterPost和AfterDelte中,让用户的操作更方便。
第三章 创建多层应用程序
一个多层的Client/Server应用程序在逻辑上划分为几个部分,分别在不同的机器上运行,这些机器既可以在一个局域网内,也可以在Internet上。多层体系结构最大的优势可以概括为两点,一是集中化的商业逻辑,另一个是客户程序可以做得很“瘦”。
目前较常见的是三层的体系结构,其中,最关键的是应用服务器,它在三层体系结构中起了承上启下的作用,所以,应用服务器又叫Data Broker。Delphi4可以创建应用服务器,也可以创建“瘦”客户。如果不怕麻烦的话,也可以创建数据库后端。
在更复杂的多层体系结构中,“瘦”客户与远程服务器之间可以加入更多的服务中间件,例如,可以加入一个安全服务中间件,或者加入一个转换中间件,专门用来处理不同平台共享数据的问题。一旦您真正理解了三层的体系结构,多层的体系结构就迎刃而解。
3.1 多层体系结构的概述
Delphi 4对多层体系结构的支持主要得益于它的MIDAS技术。MIDAS是Multi-tier Distributed Application Services Suite的简称。MIDAS技术与Delphi 4中的另一个关键技术DAX配合起来使用,可以使多层的体系结构分布在Intrenet/Intranet上。
3.1.1 多层体系结构的优势
在多层体系结构中,由于服务器集中实现了应用逻辑(又称商业规则),客户程序可以把重点放在显示数据和与用户交互上,客户程序甚至都不需要知道数据存储在哪儿。
具体来说,多层的体系结构具有如下优势:
在一个共享的中间层封装了商业规则。不同的客户程序可以共享同一个中间层,而不必由每个客户程序单独实现商业规则。
客户程序可以做得很“瘦”。因为很多复杂的工作由应用服务器代劳了,客户程序只需要关注用户界面本身。“瘦”客户程序更容易发布、安装、配置和维护。
实现了分布式数据处理。把一个应用程序分布在几个机器上运行,可以提高应用程序的性能,通过冗余配置还可以保证不会因为局部故障导致整个应用程序崩溃。
有利于安全。可以把一些敏感的功能放在有严密防护措施的层上,同时又不至于使用户界面变得复杂。Delphi 4中的CORBA或MTS支持较复杂的安全机制。
3.1.2 MIDAS技术
MIDAS技术是多层体系结构的关键。无论是应用服务器端还是客户端,MIDAS技术需要有DBCLIENT.DLL的支持,这个动态链接库用于管理数据包。发布MIDAS应用程序时需要购买服务器许可。
基于MIDAS的多层应用程序需要用到一些特殊的构件,这些构件分为四大种类:对象库中的远程数据模块。远程数据模块与普通的数据模块有些相似,不同的是,远程数据模块可以作为COM服务器或CORBA服务器让客户程序访问它的接口。
TDataSetProvider和TProvider构件。这两个构件用在应用服务器端,主要作用是提供IProvider接口,客户程序通过IProvider接口获得数据和更新数据集。
TClientDataSet构件。这是一个从TDataset继承下来的但不需要BDE的构件。MIDAS连接构件。包括TDCOMConnection、TSocketConnection、TCorbaConnection TOLEnterpriseConnection、TMIDASConnection和TRemoteServer。其中,TMIDASConnection和TRemoteServer是为了兼容Delphi3的代码而保留的。MIDAS连接构件的作用是为客户程序定位服务器和IProvider接口。每个MIDAS连接构件都以一种特定的通讯协议工作。
3.1.3 MIDAS应用程序是怎样工作的
用户首先要启动客户程序,客户程序将试图连接应用服务器,如果应用服务器还没有运行,客户程序将激活应用服务器,并从中获得IProvider接口。
客户程序向应用服务器请求数据。如果TClientDataSet的FetchOnDemand属性设为True,客户程序会根据需要自动检索附加的数据包如BLOB字段的值或嵌套表的内容。否则,客户程序需要显式地调用GetNextPacket才能获得这些附加的数据包。
应用服务器收到客户程序的请求后,就从远程数据库服务器那儿检索数据,并打包返回给客户程序
客户程序收到数据包后把包打开,然后显示或进行处理。
用户对数据进行编辑修改,然后向应用服务器申请更新数据,实际上也要打包。
应用服务器收到客户程序的申请后,就向远程数据库服务器申请更新数据。如果出错,应用服务器就把出错的记录返回给客户程序去核对。
客户程序核对并修改了数据后,既可以放弃此次更新,也可以继续此次更新。
3.1.4 客户程序的结构
对于最终用户来说,多层体系结构中的客户程序与两层体系结构中的应用程序没有什么区别,在结构上,客户程序就好像一个基于文件的单层应用程序一样,仍然通过标准的数据控件与用户交互。但与单层应用程序不同的是,多层体系结构中的客户程序是通过应用服务器提供的IProvider接口获得数据的,也通过IProvider接口申请更新数据。
注意:当使用MTS的时候,可以选择不使用IProvider接口。不使用IProvider接口的好处是,可以充分发挥MTS在处理事务方面的特长。
在客户程序中,MIDAS连接构件扮演着极其重要的角色。不同的MIDAS连接构件使用不同的通讯协议:
. TDCOMConnection DCOM
. TSocketConnection Windows Sockets (TCP/IP)l
. TOLEnterpriseConnection OLEnterprise (RPCs)
. TCorbaConnection CORBA (IIOP)
TRemoteServer和TMIDASConnection是为了兼容Delphi 3的代码而保留的。
3.1.5 应用服务器的结构
应用服务器的关键部件是远程数据模块,它提供了IDataBroker接口。当客户程序与应用服务器建立了连接,就通过IDataBroker接口来获得IProvider接口。
Delphi 4支持三种类型的远程数据模块:
TremoteDataModule。这是一个支持双重接口的自动化服务器,这种类型的远程数据模块适合于使用DCOM、TCP/IP或OLEnterprise方式。
TMTSDataModule。这也是一个支持双重接口的自动化服务器,用这种类型的远程数据模块创建的应用服务器是Active Library即动态链接库,适合于使用DCOM、TCP/IP或OLEnterprise方式。
TcorbaDataModule。这是CORBA服务器,适用于与CORBA客户通讯。
上述三种远程数据模块都可以作为容器,但只能放置非可视的构件。另外,远程数据模块上一般要放一个或几个TDataSetProvider或TProvider构件来提供IProvider接口 。
远程数据模块上也可以放TDatabase构件和TSession构件。
3.1.6 MTS
MTS是Microsoft Transaction Server的简称,是Microsoft为分布式环境下进行事务处理所设计的服务接口。使用TMTSDataModule类型的远程数据模块的优势是:
MTS为应用服务器提供了基于角色的安全机制。每个客户都扮演着一种角色,决定了他们能否访问远程数据模块的接口。TMTSDataModule有一个函数叫IsCallerInRole,可以用来检查客户的角色,然后有条件地开放该角色所允许的功能。
MTS提供了缓冲池的功能,它能把与数据库的连接放到池中,当一个客户不再需要连接时,另一个客户可以继续使用它,这样,应用服务器不必再次登录到远程数据库服务器。可能有的读者会想到,这个功能非常类似于TDatabase构件的KeepConnection属性。不过,要注意的是,如果用了TDatabase构件的话,KeepConnection属性最好设为False。
MTS提供了强大的事务处理能力,它的“两阶段提交”技术使得应用程序能够跨服务器处理事务。
可以用TMTSDataModule类型的远程数据模块实现一个MTS服务器,这个MTS服务器能够根据需要自动地激活或相反,换句话说,只有当远程数据模块接收到客户的连接请求时才创建模块的一个实例,这样能够最大程度地节省资源。
由此可见,MTS服务器可以有两种工作方式,一是单实例方式,一个实例能够处理多个客户的请求,不过,如果客户较多的话,远程数据模块就成了瓶颈,制约着应用服务器的性能。二是多实例方式,每个客户请求连接时都会创建远程数据模块的一个实例,这样,几个客户就可以同时访问数据库而不需要排队。
为了发挥MTS的上述优势,远程数据模块的实例必须做到与状态无关,而IProvider接口又依赖于状态信息,这就造成冲突。因此,TMTSDataModule类型的远程数据模块往往不用IProvider接口,而是自己创建一个接口来传递数据和申请更新。
注意:使用MTS的时候,在远程数据模块的实例激活之前不能连接数据库。
3.1.7 IDataBroker接口和IProvider接口
应用服务器上的远程数据模块支持IDataBroker接口,当客户程序与应用服务器连接以后,客户程序上的MIDAS连接构件就查找IDataBroker接口。
IDataBroker接口只实现了一个方法叫GetProviderNames,调用这个方法可以获得一个列表,这个列表列出了应用服务器上的TDataSetProvider和TProvider构件。
TClientDataSet的ProviderName属性可以指定其中一个TDataSetProvider或TProvider构件。当客户程序通过IDataBroker接口的GetProviderNames以及TClientDataSet的ProviderName属性指定了应用服务器上的一个TDataSetProvider或TProvider构件后,只要客户还在引用IProvider接口,远程数据模块的状态就应该保持,这与MTS的许多特点是有冲突的,也会与单实例的CORBA服务器发生冲突。
客户程序与应用服务器之间通过IProvider接口交换数据,不过,大部分客户程序并不直接使用IProvider接口,而是通过TClientDataSet的属性和方法间接地使用IProvider接口。 不过,也可以通过Provider属性获得IProvider接口,然后直接访问IProvider接口。
下面这个表列出了IProvider接口的属性和方法,同时列出了TProvider构件以及TClientDataSet构件中与之对应的属性和方法。
IProviderTProviderTClientDatasetApplyUpdatesApplyUpdatesApplyUpdatesConstraints属性Constraints客户程序只能通过IProvider接口访问这个属性DataDataDataDataRequestDataRequest客户程序只能通过IProvider接口访问这个方法Get_ConstraintsConstraints客户程序只能通过IProvider接口访问这个方法Get_DataGet_Data用于实现Data属性GetMetaDataGetRecords(Count = 0)内部使用GetRecordsGetRecords用于GetNextPacketResetReset内部使用Set_ConstraintsConstraints客户程序只能通过IProvider接口访问这个属性SetParamsSetParams用于Params属性
注意:IProvider接口的许多属性和方法依赖于远程数据模块的状态信息,正因为如此,在使用CORBA或MTS的应用程序中一般不要用IProvider接口。
3.2 选择连接方式
在客户程序与应用服务器之间,Delphi 4提供了四种不同类型的连接方式或者说通讯协议,包括DCOM、TCP/IP、OLEnterprise和CORBA。这些不同的连接方式都各有利弊,到底选择哪种连接方式,取决于客户的数量、客户的分布情况以及怎样发布应用程序。
DCOM是一种最直接的连接方式,它不需要专门的运行期软件支持。不过,Windows 95 不支持DCOM,除非安装了DCOM95程序。
要使用MTS安全服务,最好使用DCOM连接方式。MTS的安全服务是基于角色的,当一个客户通过DCOM访问MTS时,DCOM会告诉MTS有关客户的信息,MTS据此来决定客户的角色。如果用其他连接方式,需要有专门的运行期软件支持,客户的调用首先被传递给这些运行期软件而不是MTS,MTS就不能尽快指派角色。
TCP/IP连接方式的适合范围非常广泛,例如,如果客户程序要以ActiveForm的形式分布在Web上,最好采用TCP/IP连接方式,因为您无法肯定下载ActiveForm的计算机是否支持DCOM,而支持TCP/IP的环境是很普遍的。
要使用TCP/IP连接方式,应用服务器端必须运行一个专门的运行期软件ScktSrver.exe或ScktSrvc.exe,其中,ScktSrvc.exe只适合于Windows NT,可以作为一个服务在后台运行。与DCOM连接方式不同的是,客户的请求首先传递给ScktSrver.exe或ScktSrvc.exe,然后再创建远程数据模块的实例,而不是由客户的调用直接创建远程数据模块的实例。客户程序上的MIDAS连接构件通过IProvider接口与ScktSrvr.exe or ScktSrvc.exe通讯。
不过,客户程序很有可能在没有正常释放对IProvider 接口的引用之前出现异常,而TCP/IP连接方式无法检测到这种情况,更无法通知应用服务器,因此,有可能造成应用服务器上的资源被占用后得不到释放的后果。
如果要在应用服务器端使用Business Object Broker,就要使用OLEnterprise连接方式。此时,应用服务器端和客户端都要安装OLEnterprise运行期软件。
Delphi 4是目前唯一支持CORBA的开发工具。基于CORBA的客户程序和应用服务器可以与其他基于CORBA的应用程序无缝对接。要使用CORBA连接方式,需要ORB的支持,它提供了类似于Business Object Broker的功能。
3.3 创建应用服务器的一般步骤
要创建一个多层Client/Server应用程序,首先要创建应用服务器,然后注册或安装应用服务器,只有应用服务器已注册并且正在运行的情况下,才能创建客户程序。对于客户程序来说,既可以在设计期连接应用服务器,也可以在运行期连接应用服务器。
注意:如果客户程序与应用服务器不在同一个系统中,必须在客户计算机上注册或安装应用服务器,这样,在设计期就可以连接应用服务器。
创建一个应用服务器与创建一个两层的数据库应用程序有些相似,主要的区别是,应用服务器需要提供IProvider接口,这一般是通过TDataSetProvider或TProvider构件提供的,也可以通过数据集构件如TTable的Provider属性提供。创建应用服务器的一般步骤是:
第一步是使用"File"菜单上的"New Application"命令开始一个新项目,然后使用File菜单上的New命令,选取Multi页,如图3.1所示。
选择一个远程数据模块。如果要创建一个COM自动化服务器,允许客户通过DCOM、TCP/IP、OLEnterprise等方式访问此服务器,选择RemoteMod。如果要创建一个允许客户通过MTS访问的Active Library,选择MTSData Module。如果要创建一个CORBA服务器,选择Corba Data。
第二步是把一个数据集构件如TTable、TQuery或TStoredProc放到远程数据模块上,并进行有关设置,使得它们能访问远程的SQL数据库。尽量不要把TDatabase构件放到远程数据模块上,因为这可能引起名称冲突。如果实在要用TDatabase构件来连接SQL数据库,建议把TDatabase构件放到另一个数据模块上,然后引用这个数据模块的单元文件。
第三步是把TDataSetProvider或TProvider构件放到远程数据模块上,有一个数据集构件,就要有一个TDataSetProvider或TProvider构件与之对应。然后,用鼠标右键单击TDataSetProvider或TProvider构件,在弹出的菜单中选择ExportFrom <Name> in Data Module命令,这是为了引出Provider接口,在类型库中注册。
第四步是设置TDataSetProvider或TProvider构件的DataSet属性指定要访问的数据库,实际上就是第二步所放的数据集构件。
第五步是编写代码,实现商业规则。当然,这一步远远不是几句话所能说清楚的。
第六步是保存、编译、注册或安装应用服务器。
如果使用DCOM、TCP/IP、OLEnterprise作为通讯协议,应用服务器就好像一个自动化服务器一样,必须像ActiveX或COM服务器那样注册。
如果使用MTS,应用服务器是DLL而不是EXE,这时候不需要注册应用服务器,而要把这个DLL作为MTS对象安装到MTS包中。
如果使用CORBA,可以不注册但最好注册。如果要使客户程序对服务器接口的调用在运行期是动态确定的,就要在接口库(Interface Repository)中安装服务器的接口。如果要使客户程序能自动激活应用服务器(如果还没有运行的话),应用服务器就必须用OAD(Object Activation Daemon)注册。
第七步是如果应用服务器没有使用DCOM,您必须安装有关的运行期软件,因为其他连接方式需要这些运行期软件的支持。例如,对于TCP/IP来说,需要安装ScktSrvr.exe或ScktSrvc.exe,后者只能运行在Windows NT环境下。对于OLEnterprise来说,需要安装OLEnterprise运行期版本。对于CORBA来说,需要安装VisiBroker ORB。
3.4 远程数据模块
应用服务器的关键部件是远程数据模块。Delphi 4支持三种类型的远程数据模块,分别是TRemoteDataModule、TMTSDataModule、TCorbaDataModule。
3.4.1 TRemoteDataModule
要加入一个TRemoteDataModule类型的远程数据模块,使用“File”菜单上的“New”命令,选取“Multitier”页,双击“Remote Data Module”图标,弹出“Remote Data Module Wizard”对话框,如图3.2所示。
在“Class Name”框内键入远程数据模块的类名,不必以T打头。Delphi 4将以此名生成一个TRemoteDataModule的派生类,并以此名生成有关接口。例如,假如在“Class Name”框内键入“MyDataServer”, 远程数据模块的类名就是TMyDataServer,它所实现的接口叫IMyDataServer,其祖先接口是IDataBroker。
在“Threading Model”框内选择一种线程模式。可以选“Single-threaded”、“Apartment-threaded”、“Free-threaded”或者“Both”。
在“Instancing”框内选择是否根据客户的请求生成远程数据模块的多个实例,可以选“Single instance”或“Multiple instance”。
3.4.2 TMTSDataModule
要加入一个TMTSDataModule类型的远程数据模块,使用“File”菜单上的“New”命令,选择“Multitier”页,双击“MTS Data Module”图标,弹出“MTSData Module Wizard”对话框,如图3.3所示。
图3.3 MTS Data Module对话框
在“Class Name”框内键入远程数据模块的类名,不必以T打头。Delphi 4将以此名生成一个TMTSDataModule的派生类,并以此名生成有关接口。例如, 假设在“Class Name”框内键入“MyDataServer”, 远程数据模块的类名就是TMyDataServer,它所实现的接口叫IMyDataServer,其祖先接口是IDataBroker。
对于TMTSDataModule类型的远程数据模块来说,必须在“ThreadingModel”框内选择一种线程模式。可以选“Single”、“Apartment”或者“Both”。在“Transaction Attributes”框内选择事务属性:
如选择“Requires a transaction”,每当客户访问远程数据模块的接口时,都与当前的事务是相关的。客户不可能在事务中再申请一个新的事务。
如选择“Requires a new transaction”,每当客户访问远程数据模块的接口时,都自动开始一个新的事务。如选择“Supports transactions”,远程数据模块可以用在事务的环境中,客户访问远程数据模块的接口时必须申请一个新的事务。
如选择“Does not support transactions”,远程数据模块不能用在事务的环境中。
注意:MTS对象只能加入到ActiveX项目中,如果试图在一个EXE项目中加入TMTSDataModule类型的远程数据模块,Delphi 4会显示一个提示框,如图3.4所示。
图3.4 一个提示框
3.4.3 TCORBADataModule
要加入一个TCorbaDataModule类型的远程数据模块,使用“File”菜单上的“New”命令,选取“Multitier”页,双击“CORBA Data Module”图标,弹出“CORBA Data Module Wizard”对话框,如图3.5所示。
图3.5 CORBA Data Module对话框
在“Class Name”框内键入远程数据模块的类名,不必以T打头。Delphi 4将以此名生成一个TCorbaDataModule的派生类,并以此名生成有关接口。例如,假设在“Class Name”框内键入“MyDataServer”, 远程数据模块的类名就是TMyDataServer,它所实现的接口叫IMyDataServer,其祖先接口是IDataBroker。
在“Instancing”框内指定应用服务器怎样创建远程数据模块的实例,可以选“Shared Instance”或者“Instance-Per-Client”。
如果选“Shared Instance”,应用服务器只创建远程数据模块的一个实例来处理所有客户的请求,因此,远程数据模块必须与状态无关,换句话说,就是不能使用IProvider接口。
如果选“Instance-Per-Client”,每当一个客户试图连接时,远程数据模块都会生成一个实例。只要客户与应用服务器的连接没有断开,远程数据模块的实例就一直存在。这种模式下,允许使用IProvider接口。唯一要考虑的问题是,客户程序有可能意外终止,导致没有正常地断开与应用服务器的连接。应用服务器为了避免不必要的资源浪费,可以定期地检查客户是否正在运行,如没有,就手工把远程数据模块的实例删掉。
在“Threading Model”框内选择一种线程模式。可以选“Single-threaded”、“Multi-threaded”。
3.5 Provider
远程数据模块上往往要放一个或几个TDataSetProvider或TProvider构件,用于提供IProvider接口。有时候,也可以不显式地使用TDataSetProvider或TProvider构件,而是由数据集构件如TTable、TQuery或TStoredProc的Provider属性间接地提供IProvider接口。
显式地使用TDataSetProvider或TProvider构件的好处是,可以直接控制数据包中包含哪些信息、应用服务器怎样响应客户的请求。如果显式地使用了TDataSetProvider或TProvider构件,必须设置他们的DataSet属性指定要访问的数据集。
3.5.1 控制数据包中的字段
要控制哪些字段包含到数据包中,首先要创建永久字段。以后,只有永久字段才加入到数据包中。如果不创建永久字段的话,数据集中的所有字段都将加入到数据包中。
如果创建的永久字段中包含计算字段,由于计算字段的值是在运行期计算出来的,这些字段虽然也能加入到数据包中,但这些字段传递到客户端后就变成只读的。
由于客户程序很有可能要编辑修改数据,并且要把编辑修改后的数据申请更新到应用服务器上,因此,您创建的永久字段的数量不能太少,否则,很有可能出现重复的记录。举例来说,假设有一个学生成绩表,由学号、姓名、语文成绩、数学成绩、历史成绩等字段组成,如果创建的永久字段中只包含语文成绩、数学成绩、历史成绩等字段,很有可能出现两名学生的上述成绩完全一样,也就是说有重复的记录,这是不允许的。
如果实在不想使客户程序看到某个字段,而如果没有这个字段的话很有可能出现上述错误,这时候您可以让这个字段(TField对象)的ProviderFlags属性包含pfHidden元素,表示这个字段虽然加入到数据包中,但却是隐含的,客户看不到它。
特别要注意的是,如果使用TQuery作为应用服务器上的数据集构件,SQL语句应当选择足够多的字段,即使客户程序并不需要这么多字段,否则,就有可能出现上述错误。
3.5.2 Options属性
这个属性是一个集合,用于设置有关打包和传递的选项。
如果包含poFetchBlobsOnDemand元素,表示BLOB字段一般不放到包中,除非客户端的TClientDataSet构件的FetchOnDemand属性设为True或者显式地调用FetchBlobs。
如果包含poFetchDetailsOnDemand元素,表示嵌套表中的字段不放到包中,除非客户端的TClientDataSet构件的FetchOnDemand属性设为True或者显式地调用FetchDetails。
如果包含poIncFieldProps元素,表示把字段的属性也放到包中,包括Alignment、MinValue、DisplayLabel、DisplayWidth、Visible、DisplayFormat、MaxValue、EditFormat、Currency、EditMask、DisplayValues等属性。
如果包含poCascadeDeletes元素,当父表中的某条记录被删除时就把子表中的相应记录也删除。
如果包含poCascadeUpdates元素,当父表的关键字段的值变化时自动更新子表的记录。
如果包含poReadOnly元素,表示不允许“瘦”客户向TDataSetProvider申请更新数据。
3.5.3 在数据包中加入自定义的信息
当客户端通过IProvider 接口调用DataRequest函数请求数据时将在应用服务器端触发OnGetDataSetPropertiesevent事件,这样,应用服务器就有机会在数据包中加入一些自定义的信息。客户端可以调用GetOptionalParam来检索这些信息。
OnGetDataSetPropertiesevent事件是这样声明的:
TGetDSProps = Procedure(Sender: TObject; DataSet: TDataSet; out Properties:OleVariant);
其中,Properties参数是一个可变类型的数组,用于指定要加入的信息。Properties参数的每个元素由三部分组成:名称、值和一个布尔数。Delphi 4定义了几个标准的信息名称,它们是UNIQUE_KEY、DEFAULT_ORDER、CHANGE_LOG、SERVER_COL、CONSTRAINTS、DATASET_CONTEXT、DATASET_DELTA、LCID、BDERECORD_X、TABLE_NAME、MD_FIELDLINKS、UPDATEMODE。程序示例如下:
Procedure TAppServer.Provider1GetDataSetProperties(Sender: TObject; DataSet: TDataSet; out Properties:OleVariant);
Begin
Properties := VarArrayCreate([0,1], varVariant);
Properties[0] := VarArrayOf([/‘TimeProvided/‘, Now, True]);
Properties[1] := VarArrayOf([/‘TableSize/‘, DataSet.RecordCount, False]);
End;
上面这个程序中,加入了两个自定义的信息,一个叫TimeProvided,它的值是当前的日期和时间,True表示这个信息可以由客户端返回给应用服务器。另一个信息叫TableSize,它的值是数据集的记录数,False表示这个信息不可以由客户端返回给应用服务器。
以后,当客户端申请更新数据时,TDataSetProvider的OnUpdateData事件可以读出数据包中的信息。程序示例如下:
Procedure TAppServer.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
var WhenProvided: TDateTime;
Begin
WhenProvided := DataSet.GetOptionalParam(/‘TimeProvided/‘);
...
End;
3.5.4 响应客户的数据请求
在大多数的多层应用程序中,客户请求数据是自动进行的,应用服务器对客户请求的响应也是自动的,它自动地检索数据、把数据打包,然后把数据包传递给客户。
应用服务器在把数据包传递给客户之前,还有机会对其中的数据进行编辑 加工,例如,可以对其中敏感的数据加密,或者基于某种条件删掉一些记录。
TDataSetProvider或TProvider构件的OnGetData事件可以让您实现上述功能,程序示例如下:
Procedure TDBClientTest.ProviderGetData(DataSet: TClientDataSet);
Begin
With DataSet Do
Begin
While not EOF Do
Begin
Edit;
SensitiveData.AsString := DoEncrypt(SensitiveData.AsString);
Post;
Next;
End;
End;
End;
3.5.5 响应客户的更新请求
客户程序通过调用ApplyUpdates 向应用服务器申请更新数据。当应用服务
器上的TDataSetProvider或TProvider构件收到客户的更新请求后,就会触发OnUpdateData事件,这样您就有机会编辑数据包(Delta属性)。退出处理OnUpdateData事件的句柄后,TDataSetProvider或TProvider构件就会把数据更新到远程服务器上。
更新是一条记录一条记录进行的。每一条记录被更新前的一瞬间将触发BeforeUpdateRecord事件,这样您还有机会对数据进行检查和修改。如果出现错误,就会触发OnUpdateError事件。发生错误的原因通常是数据违反了服务器的纠错规则,或者另一个客户程序也修改了记录,而且正好在前一个客户已经申请更新的时候。
上述错误既可以由应用服务器来处理,也可以回传给客户处理。有些错误可能需要用户的介入,这就要客户端在处理。
3.5.6 在更新数据库之前编辑Delta数据包
当应用服务器端调用ApplyUpdates向远程服务器申请更新数据时将触发OnUpdateData事件,这样,应用服务器就有机会对将要更新的数据进行检查,也可以对数据进行修改。OnUpdateData事件是这样声明的:
TProviderDataEvent = Procedure(Sender: TObject; DataSet: TClientDataSet) of object;
其中,DataSet参数代表客户程序上的TClientDataSet构件,这样就可以访问Delta属性得到当前要更新的数据包。另外还有一个重要的属性需要访问,这就是UpdateStatus属性,这个属性表示Delta数据包的更新类型。程序示例如下:
Procedure TDataModule1.Provider1UpdateData(Sender:TObject;DataSet: TClientDataSet);
Begin
With DataSet Do
Begin
First;
While not Eof Do
Begin
If UpdateStatus = usInserted then
Begin
Edit;
FieldByName(/‘DateCreated/‘).AsDateTime := Date;
Post;
End;
Next;
End;
End;
End;
3.5.7 怎样定位记录
在处理OnUpdateData事件的句柄中,除了可以检查和修改Delta数据包外,还可以设置怎样定位记录或者说把哪些记录更新到服务器上。
默认情况下,应用服务器用自动生成的SQL UPDATE、INSERT或DELETE语句来把Delta数据包中写到远程服务器中,例如:
UPDATE EMPLOYEES
Set EMPNO = 748, NAME = /‘Smith/‘, TITLE = /‘Programmer 1/‘, DEPT = 52
WHERE
EMPNO = 748 and NAME = /‘Smith/‘ and TITLE = /‘Programmer 1/‘ and DEPT = 47
除非另外指定,否则,数据包中的所有字段都将出现在UPDATE子句和WHERE部分,换句话说,就是用所有的字段去定位一条记录。不过,也可以有选择地排除一些字段,这就要用到UpdateMode属性。这个属性可以设为以下值:
.upWhereAll所有字段都用来定位记录;
.upWhereChanged只有关键字段和变化了的字段用来定位记录;
.upWhereOnly只有关键字段用来定位记录。
不过,UpdateMode属性只能区分关键字段,但实际应用往往要复杂得多。例如,可能不想更新EMPNO字段,而且不想让TITLE和DEPT字段出现在WHERE部分,这时候就要用到字段(TField对象)的ProviderFlags属性,此属性是一个集合,可以包含下列元素:
.pfInWhere该字段将不出现在自动生成的INSERT、DELETE和UPDATE语句的WHERE部分;
.pfInUpdate该字段将不出现在自动生成的UPDATE语句的UPDATE子句;
.pfInKey这个字段将出现在因为更新失败而执行的SELECT语句的WHERE部分,SELECT语句用于选择出错的记录的当前值;
.pfHidden这个字段将用来定位字段,但对客户端是隐藏的。
下面这个程序示例把EMPNO字段排除在UPDATE子句之外,把TITLE字段和DEPT字段排除在WHERE部分之外。
Procedure TDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
Begin
With DataSet Do
Begin
FieldByName(/‘EMPNO/‘).UpdateFlags := [ufInUpdate];
FieldByName(/‘TITLE/‘).UpdateFlags := [ufInWhere];
FieldByName(/‘DEPT/‘).UpdateFlags := [ufInWhere];
End;
End;
3.5.8 在服务器端纠错
大多数数据库管理系统(RDBMS)都实现了纠错,以保证数据的完整和一致性。所谓纠错,实际上就是预先指定一些规则,字段和记录的值必须符合这些规则。
大多数符合SQL-92的RDBMS都支持下列纠错:
.NOT NULL字段必须有值;
.NOT NULL UNIQUE字段必须有值而且不能与其他记录重复;
.CHECK字段的值必须在一个范围内;
.CONSTRAINT在表格级对字段的值进行检查;
.PRIMARY KEY指定一个或几个字段作为关键字段;
.FOREIGN KEY指定一个或几个字段引用其他表格。
当然,不是所有的数据库都支持上述纠错,也有的服务器还支持其他纠错。其实,许多数据库桌面系统也支持纠错,不过,在服务器端纠错的优势是,多个客户程序可以共享服务器端的纠错,而不必在每个客户程序中重复一些代码。
应用服务器可以借用远程数据库服务器的纠错规则,对客户程序传递过来的数据进行纠错,这就要用到Constraints属性,只要把这个属性设为True(默认)。
如果不想借用远程数据库服务器的纠错规则,应当把Constraints属性设为False。
3.6 创建客户程序的一般步骤
在多层体系结构中,一个客户程序至少要有一个TClientDataSet构件,它的作用是引入数据集。TClientDataSet是从TDataSet继承下来的,它不需要依赖BDE。
创建一个客户程序的一般步骤是:
第一步是使用“File”菜单上的“New Application”命令开始一个新的项目,然后使用“File”菜单上的“New”命令,再双击“Data Module”图标加入一个数据模块。
第二步是把一个或几个MIDAS连接构件如TDCOMConnection、TSocketConnection、TOLEnterpriseConnection、TCorbaConnection、TRemoteServer或TMIDASConnection加到数据模块上。至于究竟选择哪一种MIDAS连接构件,这取决于通讯协议。
第三步是设置有关属性指定和连接应用服务器,这与具体的MIDAS连接构件有关。有的MIDAS连接构件还有ObjectBroker属性,可以指定一个TSimpleObjectBroker构件,这样就可以动态地选择应用服务器。
第四步是把一个或几个TClientDataSet构件放到数据模块上,设置RemoteServer属性指定一个MIDAS连接构件,设置ProviderName属性指定应用服务器上的TDataSetResolver 或TProvider构件,这样,客户程序就可以通过IProvider接口与应用服务器通讯。
第五步是把一个TDataSource构件放到数据模块上,设置它的DataSet属性指定TClientDataSet构件,再把一个数据控件如TDBGrid放到窗体上,设置它的DataSource属性指定TDataSource构件。至此,一个简单的客户程序创建完毕。
3.7 与应用服务器连接
要建立与应用服务器的连接,客户程序必须使用一个或几个MIDAS连接构件,这些构件可以在构件选项板的“MIDAS”页上找到。
不同类型的MIDAS连接构件使用不同的通讯协议,定位应用服务器的方式也不同。下面就详细介绍这几种MIDAS连接构件。
3.7.1 用DCOM来连接
要使用DCOM方式来连接应用服务器,就要用到TDCOMConnection构件。
TDCOMConnection构件的ComputerName属性用于指定应用服务器所在的计算机。如果ComputerName属性为空,TDCOMConnection就假设应用服务器与客户程序在同一个计算机上,或者应用服务器在系统注册表中可以找到。反过来说,如果应用服务器没有在客户端注册,而且与客户程序不在同一个计算机上,就须设置ComputerName属性。
即使应用服务器在客户端注册了,仍然可以设置ComputerName属性重新指定一个另一台计算机上的应用服务器。
ComputerName属性一般设为计算机的主机名或IP地址,如果指定的主机名或IP地址是非法的或者没有找到,TDCOMConnection就会触发异常。
如果客户程序需要在运行期间动态地选择应用服务器,最好不要用ComputerName属性来切换,而要加入一个TSimpleObjectBroker构件,再由ObjectBroker属性指定这个TSimpleObjectBroker构件。
3.7.2 用TCP/IP连接
要使用TCP/IP 方式来连接应用服务器,就需用到TSocketConnection构件。
TCP/IP 方式是一种应用广泛的连接方式,因为支持TCP/IP环境是相当普遍的。
TSocketConnection用Address属性或Host属性来定位应用服务器所在的计算机,前者用于指定IP地址,后者用于指定主机名,这两个属性是互斥的,只需要设置其中一个。此外,还要设置Port属性指定端口号,必须与应用服务器上运行的Scktsrver.exe或Scktsrvc.exe所使用的端口号一致,默认值是211。
3.7.3 用OLEnterprise连接
要使用OLEnterprise方式连接应用服务器,就要用到TOLEnterpriseConnection构件。
如果要直接连接应用服务器,不通过Business Object Broker,就要设置ComputerName属性指定应用服务器所在的主机名,就好像DCOM方式一样。
如果要通过Business Object Broker连接应用服务器,就要设置BrokerName属性指定Business Object Broker的名称。
ComputerName属性和BrokerName属性是互斥的,设置了其中一个,另一个就为清空。
3.7.4 用CORBA连接
要使用CORBA方式连接应用服务器,就须用到TCorbaConnection构件。
对于CORBA方式来说,只需要设置RepositoryID属性标识CORBA数据模块的接口,因为局域网中的智能代理(Smart Agent)会自动定位一个可用的应用服务器。
不过,如果不想让Smart Agent自动定位一个应用服务器,而是想指定一个特定的应用服务器,就要设置HostName属性指定应用服务器的主机名或IP地址。
TCorbaConnection构件可以通过两种方式获得应用服务器上CORBA数据模块的接口:
如果要使用静态联编方式,必须把_TLB.pas文件(由类型库编辑器生成)加到客户程序中。静态联编方式不仅能够在编译期进行类型检查,而且运行速度较快。
如果要使用动态联编方式,CORBA数据模块的接口必须用InterfaceRepository注册。
3.7.5 标识服务器
前面讲的是怎样定位应用服务器所在的计算机,现在要讲定位了计算机后,怎样标识应用服务器本身。
如果使用DCOM、TCP/IP或OLEnterprise方式来连接应用服务器,您可以通过ServerName属性或ServerGUID属性来标识应用服务器。其中,ServerName用于指定应用服务器,实际上就是一个已注册的OLE自动化对象名。ServerGUID属性用于指定远程数据模块的全局识别号。如果合法设置了ServerName属性,ServerGUID属性会自动被设置。
如果使用CORBA方式来连接应用服务器,就要用RepositoryID属性来标识应用服务器上CORBA数据模块的接口。
3.7.6 TSimpleObjectBroker
如果客户程序需要在运行期间动态地选择应用服务器,最好用TSimpleObjectBroker构件来切换,而不使用ComputerName属性。TSimpleObjectBroker能够自动维护一个可用的应用服务器的列表。当MIDAS连接构件需要连接一个应用服务器时,它就向TSimpleObjectBroker提出申请,TSimpleObjectBroker一般会随机提供一个应用服务器。如果TSimpleObjectBroker提供的应用服务器没法工作,TSimpleObjectBroker会再换一个,一直到MIDAS连接构件与应用服务器建立了连接为止。
一旦MIDAS连接构件与应用服务器建立了连接,它会自动把应用服务器的有关情况保存到有关属性中,如ComputerName、Address或Host等,因为MIDAS连接构件有可能会断开连接,以后又要再次连接,这时候就不必再向TSimpleObjectBroker提出申请。
在使用OLEnterprise或CORBA方式的情况下,不要用TSimpleObjectBroker构件,因为这两种方式都有自己专门的中介服务。
3.7.7 开始连接
进行了上述有关设置后,现在就可以连接应用服务器了。不过,在连接应用服务器之前,最好还要设置TClientDataSet的RemoteServer属性指定一个MIDAS连接构件,再设置ProviderName属性指定应用服务器中的TDataSetResolver 或TProvider构件。
当客户程序试图访问IProvider接口时,就会自动建立与应用服务器的连接,例如,把TClientDataSet的Active属性设为True。
当然,也可以通过MIDAS连接构件的Connected属性来连接或断开连接。
在将要与应用服务器建立连接之前,会触发BeforeConnect事件。当建立了与应用服务器的连接后,会触发AfterConnect事件。
3.7.8 断开连接
当进行下列操作时会使连接断开:
.把Connected属性设为False。
.关闭客户程序或MIDAS连接构件被删除。
.MIDAS连接构件的ServerName、ServerGUID、ComputerName、Host、Address等属性修改后,也会使原有的连接断开,再与基于新的应用服务器重新建立连接。
注意:尽量不要使用修改ComputerName、Host等属性的方式来切换应用服务器,最好用TSimpleObjectBroker,或者使用几个MIDAS连接构件分别建立连接。
与应用服务器的连接将要断开之前会触发BeforeDisconnect事件,连接真正断开之后,会触发AfterDisconnect事件。
3.8 调用服务器上的接口
通过TClientDataSet的Provider属性可以获得IProvider接口。其实,大部分情况下并不需要直接调用IProvider接口,因为对IProvider接口的调用已经封装在TClientDataSet的属性和方法中,唯一的例外是,DataRequest函数只能通过IProvider接口调用。
通过MIDAS连接构件的AppServer属性可以获得应用服务器上远程数据模块的接口,通过此接口可以调用远程数据模块的方法,例如:
MyConnection.AppServer.SpecialMethod(x,y);
这种调用方式是动态联编的,也就是说,编译器并不检查SpecialMethod的参数,甚至连有没有SpecialMethod它都不管。由于编译器不进行类型检查,在运行期实际调用时有可能失败。而且,动态联编没有静态联编的执行速度快。
如果用DCOM或CORBA作为通讯协议连接应用服务器,最好采用静态联编方式来访问远程数据模块的接口,方法就是用特定的接口类型对AppServer属性进行强制类型转换。假设远程数据模块的接口叫IMyAppServer(它的上级是IDataBroker),程序示例如下:
With MyConnection.AppServer as IMyAppServer Do SpecialMethod(x,y);
上面这行代码适合于DCOM方式,下面这行代码适合于CORBA方式:
With IUnknown(MyConnection.AppServer) as IMyAppServer Do SpecialMethod(x,y);
对于DCOM方式来说,要使用静态联编方式调用远程数据模块的接口,它的类型库必须在客户端注册。要注册类型库,可以调用匼DELPHI4//BIN目录中的TREGSVR.EXE。
对于CORBA方式来说,要使用静态联编方式调用远程数据模块的接口,必须在客户程序中引用类型库编辑器生成的_TLB.pas文件。
对于TCP/IP或OLEnterprise方式来说,没法使用真正的静态联编方式来调用远程数据模块的接口,不过,可以通过远程数据模块的调度接口来改善性能,程序示例如下:
varTempInterface: IMyAppServerDisp;
Begin
TempInterface := MyConnection.AppServer;
...
TempInterface.SpecialMethod(x,y);
...
End;
要通过调用接口访问远程数据模块,必须在客户程序中引用类型库编辑器生成的_TLB.pas文件。
3.9 在客户端纠错
SQL Explorer可以把服务器端的纠错和默认表达式引入到一个数据字典中,这样,通过应用服务器上的基于BDE的数据集,客户端就可以借用服务器端的规则对数据纠错,这就是所谓的客户端纠错。如果用户在客户端输入或修改的数据违反了规则,这些数据就不会传递到应用服务器端,更不会传递到RDBMS端。
由此可见,在客户端纠错可以避免把错误的数据传递给远程数据库服务器,从而节省了网络上不必要的传输,因为这些数据即使传递给远程数据库服务器,也会被退回。
不过,有时候也需要暂时禁止在客户端纠错。例如,假设有一项纠错规则需要基于字段的最大值,而客户端一次只能检索若干条记录,也就是说,客户端统计出来的最大值与服务器端统计出来的最大值有可能不一样。如果客户端使用了过滤,也有可能使客户端统计出来的最大值与服务器端统计出来的最大值不一样。上述情况下,就需要暂时禁止纠错。
要暂时禁止纠错,可以调用TClientDataSet的DisableConstraints,DisableConstraints实际上是使一个内部的引用计数加1,当这个引用计数大于0,就不允许纠错。
要重新允许纠错,可以调用TClientDataSet的EnableConstraints,EnableConstraints实际上是使一个内部的计数减1,当这个计数减到0时,就可以重新允许纠错。
3.10 更 新 数 据
当客户程序从应用服务器检索到数据,就在内存中建立这些数据的副本。用户对数据进行编辑修改后,TClientDataSet专门用一个Delta属性存储变化了记录,包括更新、删除和插入的记录。为了使修改了的数据永久化,就要向数据库申请更新数据。
3.10.1 更新数据的一般步骤
首先,客户程序要调用ApplyUpdates函数向应用服务器提出申请,ApplyUpdates函数将通过IProvider接口把Delta属性传递给应用服务器。
应用服务器收到客户程序的申请后,再向远程数据库服务器提出申请,并且把被远程数据库服务器认为出错的记录暂时缓存起来。应用服务器上的TDataSetProvider或TProvider构件把出错的记录返回给客户程序,其中包括错误信息和错误代码。
客户程序收到这些出错的记录后,可以进行核对和修改,然后继续更新。注意:如果应用服务器端使用MTS类型的远程数据模块,就无法提供IProvider接口,这种情况下,必须通过远程数据模块的接口直接申请更新数据。
3.10.2 ApplyUpdates函数
当用户修改了数据后,应当调用ApplyUpdates函数向应用服务器申请更新数据。
ApplyUpdates函数只有一个MaxErrors参数,用于指定一个最大错误数,如果出错的记录数超过了这个参数的值,此次更新就停止。如果MaxErrors参数设为0,只要应用服务器发现有一个错误的记录,更新操作就停止。如果MaxErrors参数设为-1,当应用服务器发现有错误的记录,就尝试更新下一个记录,等所有的记录都尝试过以后才返回。
ApplyUpdates函数将返回实际遇到的错误数,同时,应用服务器将返回那些有错误的记录。
3.10.3 核对出错的记录
当应用服务器把出错的记录返回给客户程序时,将在客户端触发OnReconcileError事件,这样,您就有机会对记录进行核对并修改。OnReconcileError事件是这样声明的:
TReconcileErrorEvent = Procedure (DataSet: TClientDataSet; E: EReconcileError; UpdateKind:TUpdateKind; var Action: TReconcileAction) of object;
其中,DataSet参数是TClientDataSet构件名,籍此可访问数据集中某字段的NewValue、OldValue、CurValue等属性,从而分析有关出错的原因。通过DataSet参数也能够调用TClientDataSet的方法,但不能进行可能导致数据被修改的操作,否则,可能会引起死循环。
E参数是一个指向EReconcileError对象的指针,从中可以提取出有关错误信息和导致错误的原因。
UpdateKind参数表示是哪种操作导致了错误,可以是以下值:ukModify(修改)、ukInsert(插入)、ukDelete(删除)。
Action参数用于决定在退出处理OnReconcileError事件的句柄后是否继续更新数据集,可以设为下列值:raSkip、raAbort、raMerge、raCorrect、raCancel、raRefresh。下面这个程序示例演示了怎样调用RecError单元中的一个对话框:
Procedure TForm1.ClientDataSetReconcileError(DataSet:TClientDataSet;E:EReconcileError; UpdateKind:TUpdateKind, var Action TReconcileAction);
Begin
Action := HandleReconcileError(DataSet, UpdateKind, E);
End;
3.10.4 刷新记录
客户程序把数据在内存中建立一个副本,并工作于这个副本,而其他用户有可能已经修改了数据,也就是说,内存中的数据已不是数据库中的实际数据。
为了使内存中的数据是当前最新的,可以调用TClientDataSet的Refresh。不过,调用Refresh前要保证客户端没有未确定的修改,换句话说,就是客户端的日志中没有记载任何修改,否则会触发异常。
不过,TClientDataSet的RefreshRecord可以不管当前有没有未决的修改,它可以刷新当前记录,而日志中记载的修改继续保留。
注意:调用RefreshRecord有可能带来冲突。因此,调用RefreshRecord之前,最好还是检查一下当前是否有未决的修改,如果有的话,就触发异常,程序示例如下:
If ClientDataSet1.UpdateStatus <> usUnModified then
Raise Exception.Create(/‘You must apply updates before refreshing the current record./‘);
ClientDataSet1.RefreshRecord;
3.10.5 从应用服务器获取参数
下列两种情况下,客户程序需要从应用服务器获得参数:
.客户程序需要知道存储过程的输出参数。
.客户程序需要初始化TQuery或TStoredProc的输入参数。
要从应用服务器获得参数,调用TClientDataSet的FetchParams函数,这个函数将返回有关参数并作为Params属性的值。在设计期也可以获取参数,办法是:用鼠标右键单击TClientDataSet构件,在弹出的菜单中选择“Fetch Params”命令。
注意:无论是在运行期调用FetchParams函数,还是在设计期使用“FetchParams”命令,客户端必须已经与应用服务器连接并且获得了IProvider接口,因为这些参数是由应用服务器上的TDataSetProvider或TProvider构件提供的,它们的DataSet属性必须指定了TQuery构件或TStoredProc构件。
也可以在客户端设置Params属性,然后把参数传递给应用服务器。
3.11 自定义应用服务器
MIDAS的结构是非常灵活的,可以自定义应用服务器以适应实际的需要。您可以从两个方面来自定义应用服务器,一是扩展远程数据模块的接口,二是使用自定义的数据集。
远程数据模块是应用服务器的核心部件,它提供了应用服务器与客户程序通讯的基本接口,除非客户程序直接调用IProvider接口。
如果应用服务器使用MTS类型的远程数据模块,客户程序只能通过远程数据模块的接口与应用服务器进行通讯。如果试图绕过MTS代理,就会使事务无效,特别是,由于MTS类型的远程数据模块可以与状态无关,这时候如果绕过MTS代理,有可能导致程序崩溃。
同样,单实例模式的CORBA数据模块也是与状态无关的,也存在着上述的问题。
为了使客户程序能够方便地访问MTS或CORBA数据模块(因为这时候没有IProvider接口),您必须对远程数据模块的接口进行扩展,添加一些方法让客户程序调用。
MTS或CORBA数据模块的接口都是从IDataBroker继承下来的。要向接口中加入新的属性或方法,您可以有两种操作方式:
一是使用“Edit”菜单上的“Add to Interface”命令,弹出“Add to Interface”对话框,如图3.6所示。
图3.6 “Add to Interface”对话框
在“Declaration”框内键入属性或方法的声明,单击OK按钮,Delphi 4就会把属性或方法加入到远程数据模块的接口中。
二是使用“View”菜单上的“Type Library”命令打开类型库编辑器,如图3.7所示。
在类型库编辑器中选择接口节点,例如图3.7中的IXXH,然后单击工具栏 上的“New Method”按钮或“New Property”按钮。加入了一个新的成员后,要设置有关属性。要说明的是,对于CORBA类型的远程数据模块的接口来说,许多属性是无效的。
对于基于COM的远程数据模块(TRemoteDataModule或TMTSDataModule)来说,新的成员将出现在接口的实现单元和类型库的描述文件中。
对于基于CORBA的远程数据模块(TCorbaDataModule)来说,新的成员除了加到接口的实现单元中外,还会自动生成一个_TLB单元。如果客户程序需要访问基于CORBA远程数据模块,必须引用这个单元。另外,您可以让类型库编辑器生成IDL脚本,然后用Interface Repository和Object Activation Daemon来注册接口。
客户程序可以通过MIDAS连接构件的AppServer属性获取远程数据模块的接口,然后通过接口调用远程数据模块的方法。
注意:如果使用MTS的话,每一个加入到接口中的方法必须在最后调用SetComplete,告诉MTS事务可以结束了。例如,下面的GetCustomerRecords函数用于获取记录:
Function TMyRemoteDataModule.GetCustomerRecords(MetaData: Boolean; outRecsOut: Integer):OleVariant;
Begin
Try
If MetaData then Result := CustomerProvider.GetRecords(0, RecsOut);
Else Result := CustomerProvider.GetRecords(-1, RecsOut);
SetComplete;
ExceptSetAbort;
End;
End;
在客户端,可以这样调用GetCustomerRecords函数:
ClientDataSet1.Data := CorbaConnection1.AppServer.GetCustomerRecords(False, RecsOut);
再举个例子,下面的ApplyCustomerUpdates函数用于申请更新数据:
Function TMyRemoteDataModule.ApplyCustomerUpdates(Delta: OleVarant; MaxErrors: Integer; outErrorCount: Integer); OleVariant;
Begin
Try
Result := CustomerProvider.ApplyUpdates(Delta, MaxErrors, ErrorCount);
SetComplete;
ExceptSetAbort;
End;
End;
在客户端,可以这样调用ApplyCustomerUpdates函数:
With ClientDataSet1 Do
Begin
CheckBrowseMode;
If ChangeCount > 0 then
Reconcile(MyConnectionComponent.AppServer.ApplyCustomerUpdates(Delta,MaxErrors, ErrCount));
End;
在应用服务器上,一般要使用基于BDE的数据集构件来引入数据,TDataSetProvider或TProvider构件的DataSet属性指定此数据集构件。不过,默认情况下,TDataSetProvider或TProvider构件能够用动态生成的SQL语句直接与远程的数据库服务器通讯,而不需要借助于基于BDE的数据集构件,这样做的好处是减少了一个环节。
不过,TDataSetProvider或TProvider构件有时候也需要直接向应用服务器上的数据集构件申请更新数据,因为应用服务器上使用的有可能不是基于BDE的数据集,而是TClientDataSet或自定义的数据集。这时候需要把ResolveToDataSet属性设为True。
如果能确定应用服务器不需要用到BDE,最好用TDataSetProvider构件而不是TProvider构件,TDataSetProvider不需要依赖BDE, 有利于发布和安装应用服务器。
3.12 多层体系结构下的事务
当客户程序向应用服务器申请更新数据,TDataSetProvider或TProvider构件会自动把更新数据的例程加上一层事务的外套,换句话说,就是处于事务的控制之下。如果出错的记录数没有超过MaxErrors参数,就向远程数据库服务器正式提交此次事务,否则就滚回。
为了更好地支持事务,可以在应用服务器端用TDatabase构件来管理数据库的连接和事务,它的用法与两层体系结构一样。
如果在应用服务器端使用MTS,就可以获得更强大的事务处理能力。MTS事务可以延伸到所有的商业逻辑,而不局限于数据库访问。
另外,MTS的“两阶段提交”技术,使MTS能够跨多个数据库处理事务。要说明的是,“两阶段提交”技术目前只在Oracle和MS-SQL Server中完全实现,如果要跨数据库进行事务,而其中有的数据库不是Oracle和MS-SQL Server,就有可能出错。
正如前面提到的那样,如果要使用MTS类型的远程数据模块,应当扩展它的接口,用自定义的方法来封装MTS的事务功能。
3.13 把客户程序设计为ActiveForm
Delphi 4可以把分布式的数据库结构引申到Internet/Intranet上,把客户程序作为ActiveForm嵌入到网页中让人们下载,然后在当地执行。
Internet/Intranet上的应用服务器必须支持DCOM或TCP/IP连接方式,同样,设计成ActiveForm的客户程序也必须支持DCOM或TCP/IP连接方式,因为下载ActiveForm的计算机上可能没有安装OLEnterprise或CORBA运行期软件。
在设计客户程序的界面时,要用ActiveForm代替一般的窗体。为此,首先要使用“File”菜单上的“New”命令打开“New Items”对话框,选取“ActiveX”页,双击“ActiveForm”图标,打开ActiveForm向导,如图3.8所示。
单击“OK”按钮,Delphi 4就创建一个ActiveX项目,这个项目中只有一个空白的ActiveForm,下面的步骤就象设计一般的“瘦”客户一样。
以ActiveForm的形式设计好一个“瘦”客户程序后,还需要把它发布到Web服务器上,供人们下载。为此,首先要使用“Project”菜单上的“Web DeploymentOptions”命令打开“Web Deployment Options”对话框,然后设置有关Web发布的选项,主要是指定ActiveForm在Web服务器上的URL。最后,使用“Project”菜单上的“WebDeploy”命令把ActiveForm发布到Web服务器上。
为了测试这个Active窗体,可以用一个Web浏览器如IE下载嵌入了ActiveForm的网页。如果客户程序通过DCOM与应用服务器连接,Windows 95中需要安装支持DCOM的程序——DCOM95,而Windows NT 4.0则不需要。
--------------------------------------------------------------------------------
熟悉 MIDAS
你可能已经注意到了DELPHI中的缩写MIDAS,然而你可能和多数DELPHI的开发者一样,并未意识到这种技术带来的巨大进步。Jani Jervinen 描述了MIDAS-Multi-tier Distributed Application Services Suite是如何让你把CLIENTSERVER应用转换成多层应用的。
传统的CLIENTSERVER应用已经延续了很长时间,多数的DELPHI开发者通过编写应用程序来提高水平。CLIENTSERVER技术对需要访问远端数据的简单数据库应用来说是个好的解决方案。
例如,DELPHI可以很方便的访问ORACLE或MICROSOFT SQL SERVER 数据库。然而,随着应用的增大,维持它会消耗越来越多的资源。同时,随着应用程序的用户增加,性能会成为重要的问题。
而且,INTERNET时代需要系统每天24小时不间断运行。在CLIENTSERVER应用中,后端数据库管理系统DBMS扮演着重要角色。如果服务器处理客户端的请求失败,会导致客户端应用的失败。
把多层变为视图
为解决这个问题可以用多种解决方案。分布式就是其中一种,我确信你已经不止一次听到过这个词。然而,分布式应用的一个必须认真考虑的问题就是你必须对前面提到的问题非常熟悉。
虽然有很多方式来建立分布式应用,DELPHI的用户仍然会对MIDAS技术感兴趣。MIDAS已经成为DELPHI3及其后继版本的一部分。如果你用的是DELPHI CLIENTSERVER的企业版,MIDAS的强大优势仍然值得另外的花费。
通过MIDAS,你可以把CLIENTSERVER应用分发到层,每一层实现一种逻辑上独立的功能。多层应用的优点就是你可以很方便的替换应用中的每一层,因为每一层都不知道其他层是如何实现它们的功能的。并且,每一层通过特殊的方式和其他层通讯,并只对其他层提供的服务感兴趣,而不是如何提供这种服务。
最简单的分布式应用是三层式应用。在三层式应用中,客户端的角色就是显示数据。中间层处理所有的交易逻辑,第三层是数据库服务器。如图1所示。
图1
为什么多层?
多层应用的优势在你比较传统应用和多层应用的维护时会变得很明显。当你不得不修改CLIENTSERVER应用时,你需要重建一个新的版本并重新部署它。如果有很多客户,你可能需要整周的时间来做这件事。
例如,假设你在创建一个用于计算销售人员工资的工资表的应用。工资依赖于销售人员的销售情况,突然,老板告诉你计算方法需要修改。
在CLIENTSERVER应用中,你需要修改客户端的程序。然而,如果你已经把交易逻辑(计算方法)从用户接口中分离出来,你就只需要替换中间层的应用。比较修改100个客户端和只修改中间层的花费,不言而喻。
然而,节省开支并不是建立多层应用的唯一理由。当第一层只是用来显示数据,建立INTERNET应用就会变得更经济。例如,我的很多客户问我如何创建他们最好的基于WEB的应用。我通常告诉他们解决这个问题的最好方法就是首先用MIDAS建立一个多层应用的版本,然后为它创建一个WEB用户接口。当然,分布式应用对多层式来说解决起来并非难事,但是一旦你体会到了这项技术的优越性,你就会很快知道它什么时候有用或无用。
MIDAS 很容易创建
现在你已经知道了为什么多层式对传统的CLIENTSERVER应用来说是种很好的选择了,现在是更进一步学习MIDAS的时候了。第一个版本的MIDAS是在DELPHI 3 CLIENTSERVER中发布,所以在DELPHI 5 企业版中MIDAS已经是第三个版本了。虽然MIDAS最初是设计用于DELPHI,它同样可以用于DELPHI的姊妹产品C++BUILDER。和DELPHI的其他应用开发一样,MIDAS的开发就是把控件放到FORM上。所有的MIDAS控件可以在组件模板‘MIDAS’(见图2)中找到。由于MIDAS是用来在层间传送数据库数据,多数控件跟共享和操作数据有关。表1对DELPHI 5 中所见到的控件给出了简单的描述。
图2
表 1. DELPHI 5 中的MIDAS 控件。
控件 描述
TClientDataSet (Enhanced TTable replacement to be used in MIDAS client applications)用于MIDAS的客户端应用的增强的Ttable替代控件
TDCOMConnection (Connection component for using DCOM)用DCOM连接的连接控件
TSocketConnection (Lightweight connection component that uses TCPIP)用TCPIP连接的方便的连接控件。
TDataSetProvider (Component to "export" a dataset from a MIDAS application server)从MIDAS应用服务器‘输出’数据集的控件
SimpleObjectBroker (Simple component to help in load-balancing MIDAS applications)用于MIDAS应用的负载平衡的简单控件
TWebConnection (Component for tunneling database data through HTTP)通过HTTP访问数据库数据的控件
TCorbaConnection (Can be used to connect CORBA and MIDAS applications)可以用来连接CORBA和MIDAS应用
在MIDAS应用中,客户端应用通过应用服务器来取数据,应用服务器依次从数据库取数据。应用服务器把数据库中的数据打包并返回给客户端。在MIDAS术语中,应用服务器成为提供数据给客户端。
MIDAS 提供了三种不同的连接方式(CORBA 连接是特殊的并且通常不包含在内),一个应用服务器可以同时支持所有连接。应用服务器使用常规的数据库控件来访问数据库,然后用一个数据provider控件来允许客户端应用来访问数据。客户端应用使用连接控件来连接应用服务器。连接方式依赖于客户端的应用—例如,简单连接可以用TCPIP,然而复杂的客户端应用就要用DCOM连接。
客户端的应用在使用连接控件的同时使用Client Dataset控件,它的作用就象平常的Ttable一样。例如,Client Dataset控件支持合计字段,过滤器,和主从关系,使你对MIDAS感到适应。
建立一个应用服务器
现在你可以用你的MIDAS知识来建立一个真正意义上的应用了。和所有的MIDAS应用一样,最好从应用服务器端开始开发。首先,启动DELPHI并选择一个New Application。改变自动生成的FORM的大小以使它可以放一个Tlable控件,然后在它上面放一个Tlable控件,改变它的Caption属性,比如改为‘My MIDAS Server’。 Label 和 form 的大小并不重要,因为它们只是用来在屏幕上指示MIDAS应用服务器。通常应用服务器根本不包含任何用户接口,但为了学习,最好有个简单的用户接口。它只是让你来控制服务器。
我在建立MIDAS应用服务器时最喜欢的一步就是设置窗口的背景颜色。改变主窗体的颜色对于确信应用服务器处于运行状态来说是个很好的方法。当然,这只是在你创建服务器的用户接口时才有用。
现在,选择绿颜色,并设置label的字体颜色为白色,以增加对比度。然后从FILE菜单中选择NEW,图3显示了NEW对话框中的选项,从中你要选择的是 位于Multitier 页面中的Remote Data Module图标。我在此不想详细描述,但是必须是一个Remote Data Module,因为MIDAS 是建立在COM基础之上的。因此,你的Remote Data Module 会在后面的例子中自动说明。这里有三个不同的data modules 可用的原因就是MIDAS服务器可以用于不同的环境。例如,你的MIDAS应用可以和CORBA应用通讯。
图3
现在回到代码中,按New Items 对话框的OK按钮后,你就会看到图4所示的向导。因为remote data module是一个COM对象,你需要为它指定一个 CoClass 名称。这个名称在你的系统中必须是唯一的,所以我选择‘MyGreenMIDASServer’为它的名称。其他选项用它的缺省值,你需要参考表2来了解这些选项的用途。
图4
表 2. Remote Data Module向导中的选项
选项 说明
CoClass Name (Specifies the name of the remote data module object.)标识remote data module对象的名字
Instancing (Controls how the remote module is created when the application server runs. For example, Single Instance means that every client connects to its own instance of the application server.)在应用服务器运行时控制remote module如何创建。例如,Single Instance表示每个客户端用自己的实例连接到应用服务器
Threading model (Indicates how client calls are passed to the remote data module. For example, Free means that multiple threads are used.)说明客户端的调用如何传送到remote data module.例如,FREE表示使用多线程。
提供数据集
当remote data module建立好之后,你就可以给它增加普通的database components。我在例子中访问的是DBDEMOS别名数据库中的Biolife表,所以一个Ttable控件就够了。
MIDAS服务器可以支持所有从TdataSet类继承而来的数据库控件。事实上,这意味着你可以选择用ADO,BDE,甚至InterBase Express来连接数据库。你选择的每种数据库连接对客户端应用来说都没什么不同,所以当你决定用ADO替换BDE时客户端不必做任何修改。这是不是很COOL?
为了访问Biolife表,在data module上放一个Ttable控件,并把它和我们熟知的‘fish fact’表连接。为了确保它工作正常,可以activate Ttable 控件来测试数据库的连接。然而,你随后应该马上关闭表,因为让表保持OPEN状态会耗尽宝贵的服务器资源。关闭表是安全的,因为MIDAS在需要时会自动激活控件。
然后,在表单上放一个TdataSetProvider控件(你可以在MIDAS控件页面中找到这个控件)。你要做的就是设置dataset provider的DataSet属性为Ttable 控件。做完这些以后,你就完成了应用服务器的建立,并可以保存整个project。我用RemoteDM,MainForm,和GreenServer来分别为remote data module,the green form,和project来命名。
当你保存了整个project以后,就可以马上编译并运行了。这是个重要的步骤。第一次运行应用服务器时会自动在Windows注册表中注册它。记住:事实上remote data modules是COM对象,所以注册表中必须包含EXE文件的位置(目录)信息,甚至更多。
客户端应用
现在,你的应用服务器已经完成,可以建立客户端的应用了。从View 菜单中选择Project Manager选项,右击project group 并选择Add New Project。当熟悉的New Items对话框打开后,选择Application图标并选择OK。一个新的表单会出现在IDE中。注意服务器端和客户端应用要在同一个项目组中。这使得MIDAS的应用开发更容易,因为你可以很容易的在两个项目中切换。另一种选择就是打开两个DELPHI,但我更喜欢第一种方法。
接下来,改变客户端应用的表单大小使它可以放一个grid。从MIDAS控件页面中选择一个TDCOMConnection控件放到表单上,这个控件是你可以选择的四种连接控件中的一个。如果你用的是Windows 95 或r 98,你需要在继续前先阅读补充说明‘Windows 95, 98 and DCOM’。
回到Object Inspector,设置连接控件的ServerName属性为GreenServer.MyGreenMIDASServer。注意在Object Inspector中的列表中会自动显示在本系统中注册的应用服务器。当你选择了服务器后,DELPHI会自动填写ServerGUID属性。你可以忽略GUID除非你对COM感兴趣。
现在,你可以让ComputerName属性为空。这意味着客户端在本机寻找应用服务器。如果你想在其他机器上运行服务器,可以指定ComputerName属性值为其他机器名称。
为了测试连接,置Connected属性的值为True。然后你就会看到你的应用服务器开始在后台运行(见图5),这就是COM的奇妙之处。事实上,你已经看到了just-in-time activation的所有状况。
图5
接下来,用鼠标从从控件模板中选择一个TclientDataSet控件放到表单上。设置它的RemoteServer属性为connection控件的名字。选择ProviderName属性为应用服务器上的provider控件的名字。注意什么会发生:应用服务器会和你的客户端应用通讯,并告诉它服务器提供什么数据集。
随后的事就很熟悉了:放一个TdataSource和一个TDBGrid到表单上,把data source连接到client dataset,把grid连接到data source。现在,设置client dataset的Active属性为True。如果顺利的话,你就会在grid中看到Biolife表中的数据。这些数据没有任何值得怀疑。
实施MIDADS应用
一旦你完成了MIDAS应用的创建,我确信就你会对实施它感兴趣。虽然实施MIDAS应用会很EASY,你也要按它的基本步骤进行,它们可以一次完成。
在开始服务器应用之前,你需要两个在SERVER上运行的DLL。这两个DLL是STDVCL40.DLL 和 MIDAS.DLL。当安装服务器时,你最好把这两个文件拷贝到Windows系统目录下,这样你就可以很方便的找到它们。拷贝好之后,这两个DLL还需要注册。
注册是个和COM有关的事情,并且是必须的,以使系统可以找到DLL。通常使用REGSVR32.EXE来注册,它和Windows同时被安装。然而,有些系统没有这项功能,你就需要用TregSvr,它是DELPHI中的一个演示程序。你可以在DELPHI的Demos//ActiveX//TregSvr目录下找到。
尽管这些工具用起来很方便,最好的方式还是用安装工具生成一个安装包。例如,我使用InstallShield(DELPHI专业版中提供)来创建一个安装包,并设置MIDAS的DLLs为‘self-registering’。
给代码授权
在你实施MIDAS应用之前,一定要注意MIDAS并非是免费软件。当你实施你的MIDAS应用时,通常需要购买一个实施许可证。开发MIDAS应用程序不需要许可,因为,在DELPHI 5的企业版中已经有了。
在http://shop.borland.com/上的实施许可-$299.95允许你在一台机器上实施你的MIDAS应用。这个许可对客户端的连接数没有限制,所以MIDAS事实上很便宜。
为了使MIDAS让更多人买的起,Inprise公司对许可协议保留了一个例外。在DELPHI的DEPLOY.TXT文件中:"A server deployment license is not required… in an application in which the client and server reside on the same machine." (‘服务器的许可证不需要…当客户端和服务趋端在同一台机器上时’。)
更多MIDAS
MIDAS让你可以用你现在的DELPHI技术方便的开发分布式应用,而不必涉及DCOM 或 CORBA的更深内容。而且,你可以用DELPHI的开发方式建立你的应用程序,同时速度会很快。
尽管在一篇文章中并不能描述MIDAS的所有内容,我希望你可以在此学到最基本的技术。我建议对TclientDataSet控件花点时间,相信你不会对它的一些特性失望。
作为进一步的练习,我建议你测试不同的连接方式。例如,使用TsocketConnection,它可以让你和服务器建立很lightweight的连接。同时,你可能想学习IappServer接口,它是MIDAS技术的基础。
如果你想对MIDAS了解的更多,我建议你访问Borland公司的WEB站点http://community.borland.com/。特别是Dan Miser写的文章更值得一读。同时要记住:将来不管遇到什么问题,也不会阻挡DELPHI开发者在MIDAS领域的旅程。
下载 JARVINEN.ZIP
补充说明: Windows 95, 98 and DCOM
如果你用的是Windows 95或98,你必须确保DCOM已经安装。缺省情况下,Windows 95或98不包含DCOM,但Windows NT 和 Windows 2000已经安装了。要在Windows 95上安装DCOM,从DELHPI的安装CD上运行DCOM95.EXE(这个文件位于安装目录下)。或者,浏览http://www.microsoft.com/com,并下载最新的安装包。要在Windows 98上安装,从相同的网址上下载最新的DCOM安装版本。
--------------------------------------------------------------------------------
其实也就一个主要问题,为什么保存数据时非得要先断开连接,再重新连接后才能正常保存?
百思不得其解!
--------------------------------------------------------------------------------
在非多层的情况下,更新数据往往先判断事务是否正在进行,多层不了解。不知是否有类似情况
procedure TForm1.ADODataSet1BeforePost(DataSet: TDataSet);
begin
if not ADOConnection1.InTransaction then
begin
ADOConnection1.BeginTrans;
end;
procedure TForm1.ADODataSet1AfterPost(DataSet: TDataSet);
begin
if ADOConnection1.InTransaction then
begin
ADOConnection1.CommitTrans;
end;
end;
--------------------------------------------------------------------------------
-- 作者:HUWEIJI
-- 发布时间:2005-8-3 19:38:33
--
出错的提示是:
Cannot create new transaction because capacty was exceeded.
可是除了BeforeUpdateRecord自动开启的事务外,没有任何其他事务也照样出问题。
我用的是DBExpress,以前用ADO时也没有问题。
保存的过程是 SqlConnection.Connection:=False;
SqlConnection.Connction:=True;
保存;
SqlConnection.Connection:= False;
少了一项都可能出问题。
如果只保存一个ClientDataSet中的数据是没有问题的,当不止一个时,比如说父子表,就不知道怎么办了。
去大富翁也查不出所以然来,真是郁闷
出处: http://www.360doc.com/content/12/0817/17/7662927_230712619.shtml