架构:
1. 数据库服务器:FireBird;
2. WebService 服务器端,采用 FireDAC 控件 + DataSetProvider;
3. WebService 客户端,采用 ClientDataSet 绑定到服务器端的 DataSetProvider;
4. 数据库表名:TEST,有2个字段:XUHAO, DESC;其中 XUHAO 是整数,我希望它是个顺序自增的字段,是主键,也是本文关注处理的对象。
代码需求
上述架构下,连接数据库的控件,采用 FireDAC 还是其它控件,和本主题无关。虽然 FireDAC 控件也能处理 FireBird 的自增字段,我之前有文章写这个 -- 请见:再谈 Firebird / Interbase 自增字段和 FireDAC 以及 ClientDataSet
但在三层 WebService 架构下,上述方法比较麻烦,用不上。
在上述架构下,有几个前提是必须的:
1. XUHAO 字段是主键,因此在 ClientDataSet1 里面,新增记录,不能空,必须填值。又因为它的值是数据库服务器赋予的,是自增的,因此在客户端的 ClientDataSet1 里面,新增记录,填入负数值作为填充。
2. 在 WebService 架构下,ClientDataSet 和服务器端的 DataSetProvider1 是绑定的。
3. 尽量少写代码;
4. 尽量减少网络来回和网络数据量。
解决方案
方案一:
1. FireBird 数据库里面,为自增值,创建一个【生成器】。
2. FireBird 数据库里面,创建一个存储过程,这个存储过程调用生成器,获得一个唯一的增加值。存储过程代码如下:
BEGIN
/* Procedure body */
NEW_XUHAO = GEN_ID(TEST_XUHAO, 1);
SUSPEND;
END
3. 在 WebService 服务器端的 SoapDataModule 里面,放一个 FdStoreProc 这个是 FireDAC 的用于存储过程的控件。将这个控件指向数据库的上述存储过程。写一个函数名为 GetNewXuHao,函数里面执行这个存储过程,获得最新的自增值;
4. 服务器端的 DataSetProvider 的 BeforeUpdateRecord 事件写入代码:
procedure TtestAutoInc.DataSetProvider1BeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
if UpdateKind = ukInsert then
DeltaDS.FieldByName('XuHao').NewValue := GetNewXuHao;
end;
上述代码,利用数据库的存储过程,从数据库获得唯一的自增值,将来自客户端提交的新增字段的 XUHAO 字段值,修改为数据库产生的值。
4.1. 同时,设置 DataSetProvider1.Options 的 poPropogateChanges 设置为 True (设计期勾选上);
4.2. 因为在服务器端修改了 DataSetProvider1 里面的 DeltaDS 的 XUHAO 字段值,又设置了上述属性,则客户端 ClientDataSet1.ApplyUpdates(0) 提交后,会自动获得服务器端修改的值,自动将新增记录的负数值变为正确的值。
到此问题搞定。
方案二:
第一个方案,服务器端需要做不少事,写不少代码,去调用数据库的存储过程等。如果绕开服务器端直接操作数据库,则自增不存在。严重依赖服务器端代码。
对于 FireBird 数据库来说,可以做一个真正的自增字段。实现方法是:
A. 仍然需要一个生成器,用于产生顺序自增的数字;
B. 一个绑定到 Insert 新增记录的触发器,当数据库插入新纪录时,为记录产生一个自增值。触发器代码如下:
CREATE TRIGGER BI_LOG_XUHAO FOR "LOG"
ACTIVE BEFORE
INSERT
POSITION 0
AS
BEGIN
NEW.XUHAO = GEN_ID(LOG_XUHAO_GEN, 1);
END;
实现了上述触发器,对于这个字段来说,就是个自增字段。只要增加一条记录,数据库就自动为这个新纪录自动插入一个最新的由生成器产生的唯一数字。和数据库之外的代码无关。
问题来了,客户端 ClientDataset1 插入一条记录并提交后,如何更新,获得数据库自己产生的自增值?
此时,方案一的办法不灵了。除非想办法获得数据库对应的新纪录的值。网上有一些方法,但毕竟麻烦也不易懂,而且代码和具体的数据库有关。
对于客户端来说:
1. 首先要做点事情,就是每增加一条新纪录,把 XUHAO 字段的值,自动赋值为负数,避免因为它是主键导致没有值带来的键值冲突异常。当然,要求用户手动输入也是可以的。自动的代码如下,在 ClientDataSet1 的 AfterInsert 事件里面写代码:
procedure TForm2.ClientDataSet1AfterInsert(DataSet: TDataSet);
var
i: Integer;
begin
i := ClientDataSet1.Tag - 1;
ClientDataSet1.FieldByName('XUHAO').AsInteger := i;
ClientDataSet1.Tag := i;
end;
2. 提交成功后,最简单的做法:ClientDataSet1.Refresh;就可以刷新。马上,就可以看到 XUHAO 自动新插入的记录的负数,变成了数据库对应该记录产生的新的自增数字。
但是,简单的 Refresh 的问题是网络数据流量大,尤其是当数据库里面记录特别多的时候。如果肯定该表不会有太多数据,可以采用这个简单办法,少写代码。
2.1. ClientDataSet1.Refresh 会清空当前记录;
2.2. 然后,从服务器端的 DataSetProvider 请求全部数据。这样带来网络流量大的问题。
解决网络流量的问题:
既然 XUHAO 是一个整数主键,那么,SQL 语句可以写成:
select * from TEST where XUHAO>:XUHAO
然后,运行服务器,在设计期 IDE 里面,右键点 ClientDataSet1,下拉菜单选择 Fetch Params,它自动获取到参数。参数存在 ClientDataSet1 的 Params 属性里面,如果获取成功,看属性面板对应项目就能看到。
然后在 ClientDataSet1 的 BeforeRefresh 事件里面,对参数赋值,服务器端的 SQL 语句自动获得该参数。只要赋值正确,抓取的数据就只是最新的数据,降低了网络数据流量。代码如下:
procedure TForm2.ClientDataSet1BeforeOpen(DataSet: TDataSet);
begin
ClientDataSet1.Params[0].Value := -1
end;
procedure TForm2.ClientDataSet1BeforeRefresh(DataSet: TDataSet);
var
i: Integer;
begin
FData := ClientDataset1.Data; //这个 FData: Variant; 全局的。
CLientDataSet1.Last;
i := ClientDataSet1.FieldByName('XuHao').Value;
ClientDataSet1.Params[0].Value := i;
end;
procedure TForm2.ClientDataSet1AfterRefresh(DataSet: TDataSet);
begin
ClientDataset1.AppendData(FData, True);
with ClientDataSet1 do
begin
//刷新后,负数记录还在,对于负数记录新增的记录也拉回来了。因此要清除掉负数记录。
First;
while not Eof do
begin
if FieldByName('XuHao').AsInteger < 0 then
begin
Delete;
end
else Next;
end;
MergeChangeLog;
end;
end;
上述代码解释:
1. OnOpen 事件,第一次打开,参数赋值 -1,获得全部记录;
2. BeforeRefresh 事件,获取当前最大编号,赋值给参数。这里使用了 LAST,所以必须要先把 XUHAO 作为索引,让它自动排序。这里我是设计期完成,因此没有设置索引的代码。
3. AfterRefresh 事件,因为只抓取了最新插入的记录,因此:A. 把原来的记录加回来;B. 把负数的记录删除掉。
到此完成。这个方法的好处是,所有代码是在客户端完成,服务器端的代码是正常的代码,可以应对任何需求。数据库完成自增,也无需服务器端的 Delphi 代码干预,耦合度更低。