Обновление TClientDataSet только для чтения завершается с ошибкой ключа

Я получаю нарушение ключа при обновлении в приведенном ниже коде.

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 is tt_emp_id;tt_fromdate
  • Ранее мы прошли процедуру, набор данных клиента открыт; ошибок нет, пока FindKey возвращает true
  • FetchOnDemand = true, но переключение не имеет значения
  • Delphi Tokyo Win32, FireBird 2.5.3, база данных Dialect 3 (собственно файл GDB)
    ДОБАВЛЕНО 30-11-2017: теперь я тоже получаю это в базе данных MSSQL в том же приложении.
  • Если я отслеживаю код Delphi, ошибка происходит в TCustomClientDataSet.InternalRefresh при звонке FDSBase.AppendData в конце.

Этот код работал, когда мы использовали SQLDirect в качестве слоя доступа к базе данных, но больше не использовали FireBird.

В чем может быть причина?


ДОБАВЛЕНО 1-12-2017 Это связано с UpdateOptions.RequestLive собственность для TFDConnection. Если я переключаю его значение по умолчанию true на false, все работает нормально.

Это все очень странно. Почему по умолчанию true для RequestLive?
(И почему его значение на самом деле не отражено в DFM, но переключены EnableDelete, EnableInsert, EnableUpdate)?


Для тех, кто хочет воспроизвести, это полный источник.pas:
(Это на самом деле имеет TDataSource а также TDBGrid но те были только для того, чтобы показать данные)

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

1 ответ

Вот что происходит:

  1. Открытие TClientDataSet заполняет его с помощью TDataSetProvider,
  2. Провайдер, в свою очередь, открывает TFDQuery,
  3. TFDQuery имеет UpdateOptions.RequestLive установлен в trueчто заставляет его получать свои метаданные, в частности ProviderFlags каждого TField,
  4. FireDAC извлекает уникальные идентифицирующие столбцы для главной (первой) таблицы в select...from... оператор, поэтому не может установить tt_fromdate как часть идентифицирующего ключа.
  5. Затем клиентский набор данных передает эти метаданные ("идентификационный" ключ) во внутреннее внутреннее хранилище Midas.
  6. Позже при звонке Refreshвнутреннее хранилище перепроверяет уникальность своих сохраненных записей, используя этот неправильный ключ, и вызывает исключение нарушения ключа.

Цитата из онлайн-справки:

TFDQuery, TFDTable, TFDMemTable и TFDCommand автоматически извлекают уникальные идентифицирующие столбцы (mkPrimaryKeyFields) для главной (первой) таблицы в операторах SELECT ... FROM ..., когда fiMeta включена в FetchOptions.Items.
...
Приложению может потребоваться явно указать уникальные идентифицирующие столбцы, когда FireDAC не сможет их правильно определить.


Возможные решения:

  • Задавать RequestLive ложь в TFDQuery составная часть. Похоже, что основной целью установки его в значение true является включение FireDAC для автоматической генерации обновляющих команд SQL, поэтому, если это набор данных только для чтения, вы можете отключить его (обратите внимание, что это также необходимо, если вы планируете вызывать RefreshRecord).
  • Изменить порядок столов вfromТаким образом, tt_emp_contract является первой таблицей, поэтому используется ее первичный ключ.
  • Создать постоянные поля для TFDQuery и установить pfInKey в ProviderFlags из TField соответствующий tt_fromdate.
  • Задавать TFDQueryUpdateOptions.KeyFields в tt_emp_id; tt_fromdate.

Любой из них должен сделать эту работу.

Другие вопросы по тегам