【问题标题】:Read-only TClientDataSet Refresh fails with key violation只读 TClientDataSet 刷新因密钥冲突而失败
【发布时间】:2018-05-06 03:50:54
【问题描述】:

在下面的代码中,我在刷新时遇到了密钥冲突。

EmployeeContracts 是一个TClientDataSet,通过TDataSetProvider 耦合到TFDQuery,使用SQL:

select ec.*
from tt_emp e, tt_emp_contract ec
where (coalesce(e.tt_nonactive,0)=0)
and e.tt_emp_id = ec.tt_emp_id

代码片段:

with EmployeeContracts do
begin
  // Retrieve contracts of all active employees
  if (not Active) then
  begin
     Open;
  end;

  // Is record already correctly positioned?
  if (FieldByName(SEmpID).Asinteger=AEmpID) and
     (FieldByName(SFromDate).AsDateTime<=APeilDatum) and
     (FieldByName(SToDate).AsDateTime>=APeilDatum) then
  begin
     Result := True;
     Exit; 
  end;

  if not FindKey([AEmpID]) then  // Make sure the data are up to date. Refresh from the server.
  begin
     Refresh;  // ERROR HERE
  end;

  if FindKey([AEmpID]) then
  begin
     while (FieldByName(SempID).Asinteger=AEmpID) and (not EOF) do
     begin
        if (FieldByName(SFromDate).AsDateTime<=APeilDatum) and
           (FieldByName(SToDate).AsDateTime>=APeilDatum) then
        begin
           Result := True;
           Exit; 
        end;

        Next;
     end;
  end;
end;
  • IndexFieldNames 是 tt_emp_id;tt_fromdate
  • 我们前面已经过套路了,clientdataset是打开的;只要 FindKey 返回 true 就没有错误
  • FetchOnDemand=true,但切换它没有区别
  • Delphi Tokyo Win32、FireBird 2.5.3、Dialect 3 数据库(实际上是一个 GDB 文件)
    2017 年 11 月 30 日添加:我现在也在同一个应用程序中的 MSSQL 数据库上得到了这个。
  • 如果我跟踪 Delphi 代码,在最后调用 FDSBase.AppendData 时会在 TCustomClientDataSet.InternalRefresh 中发生错误。

此代码在我们使用 SQLDirect 作为数据库访问层时有效,但不再适用于 FireBird。

可能是什么原因?


添加 1-12-2017 它与 TFDConnection.UpdateOptions.RequestLive 属性有关如果我将其默认 true 值切换为 false,一切正常。

这一切都很奇怪。为什么 RequestLive 的默认值为 true?
(为什么它的值实际上并没有反映在 DFM 中,而是 EnableDelete、EnableInsert、EnableUpdate 切换了)?。


对于想要复制的人,这是完整的 .pas 源:
(它实际上有 TDataSourceTDBGrid 但这些只是为了显示数据)

unit uClientDatasetRefresh;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf,
  FireDAC.DApt.Intf, FireDAC.Stan.Async, FireDAC.DApt, FireDAC.UI.Intf,
  FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Phys, FireDAC.Phys.FB,
  FireDAC.Phys.FBDef, FireDAC.VCLUI.Wait, Data.DB, Vcl.StdCtrls, Vcl.Grids,
  Vcl.DBGrids, Vcl.ExtCtrls, FireDAC.Comp.Client, FireDAC.Comp.DataSet,
  Datasnap.Provider, Datasnap.DBClient;

type
  TFrmClientDatasetRefresh = class(TForm)
    ClientDataSet1: TClientDataSet;
    DataSetProvider1: TDataSetProvider;
    FDQuery1: TFDQuery;
    FDConnection1: TFDConnection;
    Panel1: TPanel;
    DataSource1: TDataSource;
    DBGrid1: TDBGrid;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    function PositionOnEmployeeContractRecord(AEmpID: integer; ADate: TDateTime = 0): Boolean;
  public
  end;

var
  FrmClientDatasetRefresh: TFrmClientDatasetRefresh;

implementation

{$R *.dfm}

procedure TFrmClientDatasetRefresh.Button1Click(Sender: TObject);
begin
   PositionOnEmployeeContractRecord(20652);   // Has records in tt_emp_contract
   PositionOnEmployeeContractRecord(1024);    // Has no records in tt_emp_contract
end;

const
   SEmpID    = 'tt_emp_id';
   SFromDate = 'tt_fromdate';
   SToDate   = 'tt_todate';

function TFrmClientDatasetRefresh.PositionOnEmployeeContractRecord(AEmpID: integer; ADate: TDateTime = 0): Boolean;
begin
   Result := False;

   if (AEmpID=0) then Exit;
   if ADate=0 then ADate := Date;

   with ClientDataSet1 do
   begin
      if (not Active) then
      begin
         Open;
      end;

      if (FieldByName(SEmpID).Asinteger=AEmpID) and
         (FieldByName(SFromDate).AsDateTime<=ADate) and
         (FieldByName(SToDate).AsDateTime>=ADate) then
      begin
         Result := True;
         Exit;
      end;

      if not FindKey([AEmpID]) then
      begin
         Refresh;
      end;

      if FindKey([AEmpID]) then
      begin
         while (FieldByName(SempID).Asinteger=AEmpID) and (not EOF) do
         begin
            if (FieldByName(SFromDate).AsDateTime<=ADate) and
               (FieldByName(SToDate).AsDateTime>=ADate) then
            begin
               Result := True;
               Exit;
            end;

            Next;
         end;
      end;
   end;
end;

end.

这是完整的 .dfm 源代码:

object FrmClientDatasetRefresh: TFrmClientDatasetRefresh
  Left = 0
  Top = 0
  Caption = 'ClientDataset Refresh'
  ClientHeight = 276
  ClientWidth = 560
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  Position = poScreenCenter
  PixelsPerInch = 96
  TextHeight = 13
  object Panel1: TPanel
    Left = 0
    Top = 0
    Width = 560
    Height = 41
    Align = alTop
    BevelOuter = bvNone
    TabOrder = 0
    ExplicitLeft = 16
    ExplicitTop = 8
    ExplicitWidth = 185
    object Button1: TButton
      Left = 32
      Top = 8
      Width = 75
      Height = 25
      Caption = 'Test'
      TabOrder = 0
      OnClick = Button1Click
    end
  end
  object DBGrid1: TDBGrid
    Left = 0
    Top = 41
    Width = 560
    Height = 235
    Align = alClient
    DataSource = DataSource1
    TabOrder = 1
    TitleFont.Charset = DEFAULT_CHARSET
    TitleFont.Color = clWindowText
    TitleFont.Height = -11
    TitleFont.Name = 'Tahoma'
    TitleFont.Style = []
  end
  object ClientDataSet1: TClientDataSet
    Aggregates = <>
    IndexFieldNames = 'tt_emp_id;tt_fromdate'
    Params = <>
    ProviderName = 'DataSetProvider1'
    Left = 288
    Top = 8
  end
  object DataSetProvider1: TDataSetProvider
    DataSet = FDQuery1
    Left = 376
    Top = 8
  end
  object FDQuery1: TFDQuery
    Connection = FDConnection1
    SQL.Strings = (
      'select ec.*'
      'from tt_emp e, tt_emp_contract ec'
      'where (coalesce(e.tt_nonactive,0)=0)'
      'and e.tt_emp_id = ec.tt_emp_id')
    Left = 448
    Top = 8
  end
  object FDConnection1: TFDConnection
    Params.Strings = (
      'DriverID=FB'
      'Database=*****.GDB'
      'Password=masterkey'
      'User_Name=SYSDBA')
    LoginPrompt = False
    Left = 528
    Top = 8
  end
  object DataSource1: TDataSource
    DataSet = ClientDataSet1
    Left = 216
    Top = 8
  end
end

tt_emp 的表结构很简单,只有两条记录,整数为tt_emp_id,值为20652、1024
tt_emp_contract 有一些记录为不同的tt_emp_id 值,包括20652,不包括1024。结构:

TT_EMP_ID    Integer                  
TT_FROMDATE  DateTime                 
TT_TODATE    DateTime                 
TT_HOURS     Float      
... more

Index TT_I0_EMP_CONTRACT on TT_EMP_ID, TT_FROMDATE   Primary, Unique

【问题讨论】:

  • 如果您不提供IndexFieldNames,您能检查一下会发生什么吗?也许 ClientDataSet 在重新附加之前无法清除?
  • @nil 它需要IndexFieldNames,否则会报错No index current active。根据您的建议,我在 Refresh 之前尝试了 EmptyDataSet 但这没有帮助。将 Refresh 替换为 Close;打开有帮助,但我担心可能带来的开销。
  • 几年前,我在刷新通过 TDataSetProvider 连接到 dbExpress 数据集的 TClientDataSet 时遇到了一些奇怪的问题。其中一些在提供程序选项中将poRetainServerOrder 固定设置为true,尽管我最终放弃并最终做了Close+Open
  • @JRL poRetainServerOrder 对我没有帮助。谢谢。

标签: delphi firedac tclientdataset delphi-10.2-tokyo


【解决方案1】:

这是正在发生的事情:

  1. 打开TClientDataSet 会使用TDataSetProvider 填充它。
  2. 反过来,提供者打开TFDQuery
  3. TFDQueryUpdateOptions.RequestLive 设置为true,这会导致它获取其元数据,尤其是每个TFieldProviderFlags
  4. FireDAC 在select...from... 语句中检索主(first) 表的唯一标识列,因此无法设置tt_fromdate 作为“识别”键的一部分。
  5. 然后,客户端数据集将此元数据(“识别”密钥)传播到其内部 Midas 后端存储。
  6. 稍后,当调用Refresh 时,后端存储使用此错误键重​​新检查其存储记录的唯一性并引发键冲突异常。

引用online help:

TFDQueryTFDTableTFDMemTableTFDCommand 自动检索当 fiMeta 包含在 FetchOptions.Items 中时,SELECT ... FROM ... 语句中的主(第一个)表。
...
当 FireDAC 无法正确确定它们时,应用程序可能需要明确指定唯一标识列。


可能的解决方案:

  • TFDQuery 组件中将RequestLive 设置为false。将其设置为true的主要目的似乎是将FireDAC启用automatically generate updating SQL commands,因此如果这是一个只读数据集,则可以将其禁用(注意is also needed如果您打算调用RefreshRecord)。
  • 更改from 子句中的表顺序,以便tt_emp_contract 是第一个表,因此使用其主键。
  • TFDQuery创建持久字段,并在tt_fromdate对应的TFieldProviderFlags中设置pfInKey
  • TFDQuery UpdateOptions.KeyFields 设置为tt_emp_id;tt_fromdate

他们中的任何一个都必须完成这项工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-08
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-02
    相关资源
    最近更新 更多