Обновление 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, 1024tt_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 ответ
Вот что происходит:
- Открытие
TClientDataSet
заполняет его с помощьюTDataSetProvider
, - Провайдер, в свою очередь, открывает
TFDQuery
, TFDQuery
имеетUpdateOptions.RequestLive
установлен вtrue
что заставляет его получать свои метаданные, в частностиProviderFlags
каждогоTField
,- FireDAC извлекает уникальные идентифицирующие столбцы для главной (первой) таблицы в
select
...from
... оператор, поэтому не может установить tt_fromdate как часть идентифицирующего ключа. - Затем клиентский набор данных передает эти метаданные ("идентификационный" ключ) во внутреннее внутреннее хранилище Midas.
- Позже при звонке
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. - Задавать
TFDQuery
UpdateOptions.KeyFields
в tt_emp_id; tt_fromdate.
Любой из них должен сделать эту работу.