我正在使用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;
我希望每次使用上面的代码时,首先删除表中的所有记录,然后再次用循环填充它。 当我第一次使用该代码时,它会做我想做的事情,但是第二次,它只是删除记录,而不能用值填充表。
答案 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