如何修复 'Unable to find record. No key specified'?

How to fix 'Unable to find record. No key specified'?

我正在使用 firebird 2.5 服务器写入数据库文件 (BD.fbd)。我的 delphi XE8 项目有一个数据模块 (DMDados):

我的数据库文件有这个 table:

table 有这些字段(全部为整数):

None 这些字段是主键,因为在某些情况下我会有重复的值。与文件的连接是好的。当我运行代码时客户端数据集是打开的。 TSQLQUery2 (QueryConsulta) 在需要时打开。

我的代码,当被按钮触发时,必须删除所有 table 的记录(如果存在)然后用 LOOP 创建的整数填充 table。 在第一次尝试时,代码工作正常,但是当我第二次按下按钮时,我得到错误 'Unable to find record. No key specified' 然后当我检查记录时 table 是空的。

我试图更改查询的 ProviderFlags 但这没有任何区别。我检查了字段名称、table 名称或一些 SQL 文本错误,但什么也没找到。 我怀疑当我的代码删除记录时,旧值保留在内存中,然后当尝试使用新值应用更新时,数据库使用旧值来查找新记录的位置,因此导致此错误。

    procedure monta_portico ();
    var
    I,K,L,M, : integer;
    begin
    with DMDados do
      begin
        QUeryCOnsulta.SQL.Text := 'DELETE FROM PORTICO_INICIAL;';
        QueryConsulta.ExecSQL();
        K := 1;
        for I := 1 to 10 do 
          begin
          L := I*100;
          for M := 1 to 3 do
            begin
              cdsBDPortico_Inicial.Insert;
              cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger := 
                M+L;
              cdsBDPortico_Inicial.FieldbyName('ELEMENTO').AsInteger := M;
              cdsBDPortico_Inicial.ApplyUpdates(0);
              K := K +1;
            end;
          end;
      end;
    end;

我希望每次使用上面的代码时,它首先删除 table 中的所有记录,然后再次用循环填充它。 当我第一次使用代码时,它会做我想做的事,但第二次它只是删除记录,无法用值填充 table。

使用函数和过程将问题分成小部分 创建 TSqlQuery 的实例执行 SQL 语句并在完成后销毁实例...

procedure DeleteAll;
var
  Qry: TSqlQuery;
begin
  Qry := TSqlQuery.Create(nil);
  try
    Qry.SqlConnection := DMDados.conexao;
    Qry.Sql.Text := 'DELETE FROM PORTICO_INICIAL;';
    Qry.ExecSql;
  finally
    Qry.Free;
  end;
end;

您甚至可以直接从 TSQlConnection 一行执行...


DMDados.conexao.ExecuteDirect('DELETE FROM PORTICO_INICIAL;')

procedure monta_portico ();
var
I,K,L,M, : integer;
begin
with DMDados do
  begin

    DeleteAll;

    K := 1;
    for I := 1 to 10 do
      begin
      L := I*100;
      for M := 1 to 3 do
        begin
          cdsBDPortico_Inicial.Insert;
          cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger :=
            M+L;
          cdsBDPortico_Inicial.FieldbyName('ELEMENTO').AsInteger := M;
          cdsBDPortico_Inicial.ApplyUpdates(0);
          K := K +1;
        end;
      end;
  end;
end;

更新 我在下面添加了一些示例代码。另外,当我写这个答案的原始版本时,我忘记了其中一个 TDataSetProvider 选项是 poAllowMultiRecordUpdates,但我不确定你的问题是否与此有关。

错误消息 Unable to find record. No key specified 由 DataSetProvider 生成,因此未直接连接到您的

QUeryCOnsulta.SQL.Text := 'DELETE FROM PORTICO_INICIAL;'

因为它绕过了 DataSetProvider。该错误是由于在 CDS 上尝试 ApplyUpdates 失败所致。尝试将对它的调用更改为

Assert(cdsBDPortico_Inicial.ApplyUpdates(0) = 0);

这将在错误发生时向您显示,因为 ApplyUpdates 的 return 结果给出了调用它时发生的错误数。

你说

will have repeated values in some cases

如果出现问题时确实如此,那是因为您遇到了 DataSetProvider 工作方式的基本限制。要在源数据集上应用更新,它必须生成 SQL 以发送回源数据集 (TSqlQuery1),其中 唯一地 标识要在源数据中更新的行,如果源数据集包含重复的行,这是不可能的。

基本上,您需要重新考虑您的代码,以便源数据集行都是唯一的。一旦你这样做了,将 DSP 的 UpdateMode 设置为 upWhereAll 应该可以避免这个问题。当然,源数据集最好有一个主键。

一个快速的解决方法是在您插入记录的循环中使用 CDS.Locate,看看它是否可以找到一个已经存在的记录,其中包含您要添加的值。

顺便说一句,很抱歉提出有关 ProviderFlags 的问题。如果有重复的行是无关紧要的,因为无论它们被设置成什么,DSP 仍然无法更新单个记录。

如果有帮助,这里有一些代码可能有助于填充您的 table 以一种避免重复的方式。它只填充前两个 列,如您在 q 中显示的代码。

function RowExists(ADataset : TDataSet; FieldNames : String; Values : Variant) : Boolean;
begin
  Result := ADataSet.Locate(FieldNames, Values, []);
end;

procedure TForm1.PopulateTable;
var
  Int1,
  Int2,
  Int3 : Integer;
  i : Integer;
  RowData : Variant;
begin
  CDS1.IndexFieldNames := 'Int1;Int2';
  for i := 1 to 100 do begin
    Int1 := Round(Random(100));
    Int2 := Round(Random(100));
    RowData := VarArrayOf([Int1, Int2]);
    if not RowExists(CDS1, 'Int1;Int2', RowData) then
      CDS1.InsertRecord([Int1, Int2]);
  end;
  CDS1.First;
  Assert(CDS1.ApplyUpdates(0) = 0);
end;

只是一些意见,因为给出了主要答案,但没有解决次要问题。

cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger := 

FieldByName 是一个慢函数——它是对对象数组的线性搜索,对每个对象进行大写字符串比较。你最好只为每个字段调用一次,而不是在循环中再次调用它。

cdsBDPortico_Inicial.ApplyUpdates(0);

同样,应用更新相对较慢 - 它需要通过 DataSnap 库的内部胆量往返服务器,为什么如此频繁?

顺便说一句,您从 SQL table 中删除了行 - 但是您从哪里删除了 cdsBDPortico_Inicial 中的行???我没有看到该代码。

如果我在你的节目中,我会写类似的东西(当然我不是 Datasnap 和 CDS 的忠实粉丝):

procedure monta_portico ();
var
  Qry: TSqlQuery;
  _p_EL, _p_NP: TParam;
  Tra: TDBXTransaction; 
var
I,K,L,M, : integer;
begin
  Tra := nil;
  Qry := TSqlQuery.Create(DMDados.conexao); // this way the query would have owner
  try   // thus even if I screw and forget to free it - someone eventually would

    Qry.SqlConnection := DMDados.conexao;
    Tra := Qry.SqlConnection.BeginTransaction;

    // think about making a special function that would create query
    // and set some its properties - like connection, transaction, preparation, etc
    // so you would not repeat yourself again and again, risking mistyping

    Qry.Sql.Text := 'DELETE FROM PORTICO_INICIAL'; // you do not need ';' for one statement, it is not script, not a PSQL block here
    Qry.ExecSql;

    Qry.Sql.Text := 'INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) '
                  + 'VALUES (:NP,:EL)';
    Qry.Prepared := True;
    _p_EL := Qry.ParamByName('EL'); // cache objects, do not repeat linear searches
    _p_NP := Qry.ParamByName('NP'); // for simple queries you can even do ... := Qry.Params[0]

    K := 1;
    for I := 1 to 10 do
      begin
      L := I*100;
      for M := 1 to 3 do
        begin
          _p_NP.AsInteger := M+L;
          _p_EL.AsInteger := M;
          Qry.ExecSQL;
          Inc(K); // why? you seem to never use it 
        end;
      end;

    Qry.SqlConnection.CommitFreeAndNil(tra);
  finally
    if nil <> tra then Qry.SqlConnection.RollbackFreeAndNil(tra);

    Qry.Destroy;
  end;
end;

此程序不会填充 cdsBDPortico_Inicial - 但您真的需要它吗? 如果你这样做 - 也许你可以从数据库中重新读取它:也可能有其他程序将行添加到 table 中。 或者您可以插入许多行,然后在一个命令中应用它们,然后再提交事务(通常缩写为 tx),但即便如此,也不要多次调用 FieldByName

此外,提前考虑程序工作的逻辑块,那些非常事务,临时 TSQLQuery 对象等。 不管现在多么无聊和乏味,如果你不这样做,你会给自己带来更多意大利面条式的麻烦。在你有许多小函数以不可预测的table顺序相互调用之后追溯添加这个逻辑是非常困难的。

此外,如果您让 Firebird 服务器自动分配 ID 字段(并且您的程序不需要 ID 中的任何特殊值并且可以使用 Firebird 制作的值),那么以下命令可能更适合您:INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) VALUES (:NP,:EL) RETURNING ID