再谈 FireBird 的自增字段用 FireDAC 来处理

前言

基于 MIDAS 的多层架构的数据库程序,客户端是 ClientDataSet,数据库服务器是 FireBird,服务器端采用 FireDAC 的数据库控件,如何处理数据库端的自增字段。

之前我有博客文章介绍具体做法。这里更新一下另外一种做法,需要写的代码最少

设计期设置:

FireBird 数据库设置

数据库的自增字段,在设计期,使用工具,直接创建自增字段。其实这里 FireBird 创建了 3 个东西:

  1. 一个整数字段;
  2. 一个生成器;
  3. 一个触发器;

FireBird 本身没有自增字段,使用工具创建自增字段,工具为这个字段创建了一个触发器,当插入新纪录时,由触发器去调用生成器(这个是 FireBird 和 InterBase 独有的东西)去创建最新的自增数字并填入新插入的记录。触发器代码如下:

CREATE TRIGGER BI_TABLE4_ID FOR TABLE4
ACTIVE BEFORE 
  INSERT
POSITION 0
AS
BEGIN
  IF (NEW.ID IS NULL) THEN
      NEW.ID = GEN_ID(TABLE4_ID_GEN, 1);
END;

服务器端 FireDAC 的设置

为 FdQuery1 增加固定字段;假设这个自增字段名字是:ID (我做测试的数据库,对应的表的自增字段,命名为 ID)

在 FdQuery1 的字段编辑器窗口里面,选择 ID 字段,在属性窗口里面找到 ID 字段的属性:AutoGenerateValue

这个属性有3个下拉选项:arNone, arDefault, arAutoInc;

默认值是 arNone.

arNone: 什么都不做。一般的字段用这个。

arDefault:假设数据库的 ID 字段有默认值,对于 FdQuery1 来说,此属性选择这个值,意思是 FdQuery1 会采用数据库默认值;实际测试,发现不管填什么值,提交后会获得这个默认值。

arAutoInc:数据库如果是自增字段,则这里对于 FdQuery1 的对应字段,选择这个值;

arAutoInc 进阶解释:

  1. 新增加记录时,FdQuery1 的对应字段会自动出现 -1, -2 等值,无需我们填写;
  2. 对于 FdQuery1,其 ID 字段是自增字段;对于 FdQuery1 来说,如果属性 CachedUpdates 设置为 True,新增的记录保存后,ApplyUpdates 提交可以成功,并且新增记录的对应字段的 -1, -2 会自动变成数据库提交后的值,但是,FdQuery1 的 ChangeCount 依然大于 0,(可能是个 BUG),而且它没有 MergeChangeLog 方法导致无法清除已经提交的记录,因此,这种模式,无法使用;
  3. FdQuery1 的 CachedUpdates 属性的默认值是 False,在此情况下,FdQuery1 打开后,插入新的记录(因为 ID 的 AutoGenerateValue 属性设置为 arAutoInc 所以插入新记录,这个字段会自动填入 -1,-2),在执行 Post 后,会写入数据库,并且此记录的 ID 字段自动变成数据库自增的值;
  4. 综上,这里仅仅只需要设置 ID 字段的 AutoGenerateValue 属性为 arAutoInc 就可以了!
  5. 在以上设置的情况下,虽然新增记录,ID 字段自动填入了 -1, -2,但是,Post 的时候,FireDAC 并没有向数据库的对应字段插入值。这时候,触发器的代码里面,之前我修改为 IF NEW.ID < 0 THEN 就会导致 FireBird 数据库出现提交异常,说 ID 字段不能为 NULL,这也说明了 FireDAC 提交过去的数据,ID 字段没有值;但如果触发器的代码是以下代码,则此问题消失:
BEGIN
  IF ((NEW.ID < 0) or (NEW.ID IS NULL)) THEN
      NEW.ID = GEN_ID(TABLE3_ID_GEN, 1);
END

服务器端的 DataSetProvider 的设置:

属性 ResolveToDataSet 默认是 False,必须改为 True,让 DataSetProvider 把数据交给 FdQuery1 去提交,而不是由 DataSetProvider 自己去提交。

属性 Options 下拉展开,选择 poPropogateChanges (传播变化) -- 这个值默认没有勾选;这个属性勾选后,DataSetProvider 的数据被修改后,会在提交后,传递回客户端;

服务器端的代码

procedure TtestMasterDetail.DataSetProvider3AfterUpdateRecord(Sender: TObject;
  SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind);
begin
  case UpdateKind of
    TUpdateKind.ukInsert: DeltaDS.FieldByName('ID').NewValue := FdQuery3.FieldByName('ID').Value;

    TUpdateKind.ukModify:;

    TUpdateKind.ukDelete:;
  end;
end;

上述代码,是把 FdQuery3 在 Post 新记录到数据库成功以后自动获得的自增字段 ID 的值,返回给客户端。

客户端要做的事情:

客户端不需要做任何设置。仅仅需要为了操作者方便,自动为新记录加上负数的 ID 值。不写代码,让操作者手动填入负数值也行。自动填写的代码如下:

procedure TForm4.ClientDataSet1AfterInsert(DataSet: TDataSet);
begin
  with ClientDataSet1 do
  begin
    Tag := Tag -1;
    FieldByName('ID').Value := Tag;

  end;
end;

到此,成功搞定。这个是目前最少写代码的办法。

测试结果:

客户端的 DBGrid1 里面,插入3条记录,因为上述代码,ID 字段的值,自动变成 -1,-2,-3;

执行 ClientDataset1.ApplyUpdates(0) 后,提交成功,马上看见 -1, -2, -3 的 ID 值自动变成由 FireBird 数据库自动为这个字段创建的自增的数字。结果很完美。

经过再三测试,另外一种更靠谱的做法如下

上述方法测试通过后,对于这个问题,我再次查阅了之前写的博客,发现 FdQuery 还有另外一种玩法,看起来更靠谱。

首先,上述设置 ID 字段的 AutoGenerateValue 的属性为 arAutoInc 的作法,虽然提交后能获取到数据库真正的自增的字段编号,但它是来自提交后重新从数据库 select 回来最新数据而获得。如果有多个客户端并发提交,可能会出现问题。

更好的办法

另外一种办法是让 FdQuery 直接去向数据库的生成器获得最新编号写入 FdQuery 的记录里面,然后再提交到数据库。这样做在有很多客户端并发提交时,更为靠谱。

类似做法是之前本人博客里面有提到的,使用一个存储过程来从生成器获取最新编号给 FdQuery 或者给客户端提交的 DataSetProvider.OnBeforeUpdateRecord 事件用于在数据真正提交给数据库之前修改字段值。但这个做法,需要:

1. 给数据库做一个存储过程;此存储过程去从生成器获取最新编号;

2. 程序里面需要有一个用于执行该存储过程的控件;

3. 程序里需要有一段代码,用于执行存储过程,并将获得的新编号赋予要提交的字段;

现在仅仅是对 FdQuery 的几个属性设置一下,不需要做存储过程,FdQuery 自己自动向数据库的生成器获取新编号,因此可以写更少的代码

更好的办法的具体做法

更好的办法是仅仅设置几个属性就可以搞定,更少写代码。这是因为 FdQuery 的功能带来的。

  1. 数据库的字段,为它创建一个生成器用于产生编号;但不需要触发器,也就是不需要做成自增字段;
  2. DataSetProvider3BeforeUpdateRecord 事件里面,将来自客户端提交的数据的 ID 字段清空;这里必须是空值,FdQuery 才会去从生成器获取新编号;而在客户端,新记录必须要有编号,才能多条新记录共存(因为编号字段肯定是一个不许重复的字段)。
  3. 设置 FdQuery1.UpdateOptions.FetchGeneratorsPoint 的属性为 gpDeferred
  4. DataSetProvider3AfterUpdateRecord 里面将 FdQuery3 的对应字段(ID 字段)值,赋予 DeltaDS.FieldByName('ID').NewValue 则将新编号返回客户端;
  5. DataSetProvier.Options.poPropogateChanges 设置为 True (设计期在属性列表里面勾选);
  6. DataSetProvider.ResolveToDataSet 属性设置为 True; (设计期在属性列表里面勾选);
  7. FdQuery 其它属性都可以用默认值。不要设置 CachedUpdates 为 True;
  8. FdQuery 的字段 ID 也不需要设置其 AutoGenerateValue 为 arAutoInc,保持默认的 arNone 就可以了。

上述 2 的代码:

procedure TtestMasterDetail.DataSetProvider3BeforeUpdateRecord(Sender: TObject;
  SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
  var Applied: Boolean);
begin
  case UpdateKind of
    TUpdateKind.ukInsert:
    begin
      DeltaDS.Edit;
      DeltaDS.FieldByName('ID').Clear;
    end;
  end;
end;

上述 3 的代码:

procedure TtestMasterDetail.DataSetProvider3AfterUpdateRecord(Sender: TObject;
  SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind);
begin
  case UpdateKind of
    TUpdateKind.ukInsert:
    begin
      DeltaDS.FieldByName('ID').NewValue := FdQuery3.FieldByName('ID').Value;
    end;

    TUpdateKind.ukModify:;

    TUpdateKind.ukDelete:;
  end;
end;

花了几个小时反复测试,终于搞清楚 FireBird 结合 FireDAC 该如何处理自增字段的问题了。

更新:

上述方法在 D10.3. 测试通过;在 D10.4.2 下面,上述方法会出问题,暂时还没找到问题的解决办法。

在 D10.4.2.下面,如果执行了上述 2 的代码,则当执行上述 3 的代码时,Delphi 会弹出异常提示。如果不执行上述 2 的代码,执行上述 3 的代码,则不会有异常提示。

但是,如果不执行上述 2 的代码,也就是客户端提交后,写入 FdQuery 的记录的 ID 字段依然是客户端的负数编号,而不是空值,带来的错误结果有点不符合逻辑。错误现象是:

1. 客户端第一次提交,服务器端的 FdQuery 依然可以向生成器获取新编号值;因此第一次提交会成功;

2. 客户端如果提交后,再次插入新记录,再次提交,则服务器端不会向生成器获取新编号,而是直接将来自客户端的负数编号的记录写入数据库。

3. 奇怪的地方:向数据库的生成器获取新编号是服务器端的代码,但这种情况下,不用重启服务器端,重启客户端,又可以获得第一次的成功提交,第二次,第三次则又是失败的。

因此,上述方法在 D10.4.2 可能不能使用。


版权声明:本文为pcplayer原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。