如何解决“无法找到记录。未指定密钥?

时间:2019-04-02 16:50:38

标签: delphi firebird firebird2.5 delphi-xe8 dbexpress

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

  • SQLConnection (conexao)

  • TSQLQUery1 (QueryBDPortico_Inicial) + TDataSetProvider1 (DSP_BDPortico_Inicial) + TClientDataSet1 (cdsBDPortico_Inicial)

  • TSQLQUery2 (QueryConsulta)(仅用于SQL字符串)

我的数据库文件具有此表:

  • PORTICO_INICIAL

该表具有以下字段(均为整数):

  • NPORTICO

  • ELEMENTO

  • ID

这些字段都不是主键,因为在某些情况下我会有重复的值。与文件的连接正常。运行代码时,客户端数据集已打开。 TSQLQUery2 (QueryConsulta)在需要时处于打开状态。

我的代码在被按钮触发时必须删除所有表的记录(如果存在),然后用LOOP创建的整数填充整个表。 在第一次尝试中,代码运行良好,但是第二次按下按钮时,出现错误“无法找到记录。未指定键”,那么当我检查记录时,表为空。

我尝试更改查询的ProviderFlags,但这没什么区别。我检查了字段名,表名或某些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;

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

3 个答案:

答案 0 :(得分:3)

更新,我在下面添加了一些示例代码。另外,当我写这个答案的原始版本时,我忘记了TDataSetProvider Options之一是 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的返回结果给出了调用它时发生的错误数。

你说

  

在某些情况下将具有重复的值

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

基本上,您需要重新考虑代码,以便源数据集的行都是唯一的。完成此操作后,将DSP的UpdateMode设置为upWhereAll应该可以避免此问题。当然,最好使源数据集具有主键。

一种快速的解决方法是使用CDS。在插入记录的循环中定位,以查看它是否可以找到将要添加的值的现有记录。

顺便说一句,很抱歉提出关于ProviderFlags的观点。是否存在重复行无关紧要,因为无论将行设置为什么,DSP仍将无法更新单个记录。

如果有帮助,下面的一些代码可能有助于填充表格 以一种避免重复的方式。它仅填充前两个 列,如您在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;

答案 1 :(得分:1)

使用功能和过程将问题分为多个小组 创建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;

答案 2 :(得分:0)

只有很少的观察,导致给出了主要的答案,但它们没有解决次要的问题。

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

FieldByName是一种慢速功能-它是对对象数组进行线性搜索,并且每个对象之间均使用大写字符串进行比较。最好只为每个字段调用一次,而不要在循环中再次调用它。

cdsBDPortico_Inicial.ApplyUpdates(0);

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

顺便说一句,您从SQL表中删除行-但是您在哪里从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-但是您真的需要吗? 如果这样做-也许您可以从数据库中重新读取它:可能还有其他程序在表中添加了行。 或者,您可以在提交事务之前插入很多行,然后在一个命令中全部应用它们(通常缩写为tx),但是即使如此,也不要多次调用FieldByName

此外,请事先考虑程序的逻辑块,那些非常重要的事务,临时TSQLQuery对象等。 无论现在多么无聊又乏味,如果不这样做,您将给自己带来更多的意大利面条麻烦。在您有许多小的函数以无法预测的顺序互相调用之后,追溯地添加此逻辑非常困难。

此外,如果您使Firebird服务器自动分配ID字段(并且您的程序不需要ID中的任何特殊值,并且可以使用Firebird的值确定),则可以使用以下命令可能会为您提供更好的服务:INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) VALUES (:NP,:EL) RETURNING ID