我使用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;
我希望每次我使用上面的代码时,首先删除表中的所有记录,然后用循环再次填充它。当我第一次使用代码时它会按照我想要的方式执行操作,但在第二次它只删除记录并且无法用值填充表格。
更新我在下面添加了一些示例代码。此外,当我写这个答案的原始版本时,我忘记了其中一个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
的返回结果给出了调用它时发生的错误数。
你说
在某些情况下会有重复的值
如果在问题发生时这是真的,那是因为你在DataSetProvider的工作方式上遇到了根本的限制。要在源数据集上应用更新,它必须生成SQL以发送回源数据集(TSqlQuery1),该数据集唯一地标识要在源数据中更新的行,如果源数据集包含重复的行,则这是不可能的。
基本上,您需要重新考虑代码,以便源数据集行都是唯一的。一旦你完成了这一点,将DSP的UpdateMode
设置为upWhereAll
应该可以避免这个问题。当然,源数据集最好有一个主键。
一个快速的解决方法是在插入记录的循环中使用CDS.Locate,以查看它是否可以找到包含您要添加的值的已存在记录。
顺便说一句,对不起提出有关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;
使用函数和过程将问题分成小方使用创建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;
只有少数观察,导致给出主要答案,但他们没有处理次要问题。
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