如何修复 '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):
SQLConnection (conexao)
TSQLQUery1 (QueryBDPortico_Inicial) + TDataSetProvider1 (DSP_BDPortico_Inicial) + TClientDataSet1 (cdsBDPortico_Inicial)
TSQLQUery2 (QueryConsulta)
(仅供使用SQL字符串)
我的数据库文件有这个 table:
PORTICO_INICIAL
table 有这些字段(全部为整数):
NPORTICO
ELEMENTO
ID
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
我正在使用 firebird 2.5 服务器写入数据库文件 (BD.fbd)。我的 delphi XE8 项目有一个数据模块 (DMDados):
SQLConnection (conexao)
TSQLQUery1 (QueryBDPortico_Inicial) + TDataSetProvider1 (DSP_BDPortico_Inicial) + TClientDataSet1 (cdsBDPortico_Inicial)
TSQLQUery2 (QueryConsulta)
(仅供使用SQL字符串)
我的数据库文件有这个 table:
PORTICO_INICIAL
table 有这些字段(全部为整数):
NPORTICO
ELEMENTO
ID
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