Delphi - 为什么我会收到此访问冲突? ADO 查询参数有限制吗?

Delphi - Why am I getting this Access Violation? Is there a limit to ADOQuery parameteres?

我的这段代码正在返回访问冲突(“模块 'sqloledb.dll' 中地址 74417E44 处的访问冲突。读取地址 786E3552”),我无法确定问题出在哪里。我唯一的猜测是 ADOQuery 对我们可以传递的参数数量有限制。 代码如下:

With qryInsert do
  begin
    Active := False;
    Close;
    Sql.Clear;
    Sql.Add('Insert Into MyTable(ColumnOne, ');
    Sql.Add('             ColumnTwo,           ');
    Sql.Add('             ColumnThree,         ');
    Sql.Add('             ColumnFour,           ');
    Sql.Add('             ColumnFive,          ');
    Sql.Add('             ColumnSix,        ');
    Sql.Add('             ColumnSeven,        ');
    Sql.Add('             ColumnEight,     ');
    Sql.Add('             ColumnNine,       ');
    Sql.Add('             ColumnTen,       ');
    Sql.Add('             ColumnEleven,     ');
    Sql.Add('             ColumnTwelve,   ');
    if qrySelect.FieldByName('ColumnTwelve').AsSTring = 'Y' then
    begin
      Sql.Add('           ColumnThirteen,   ');
      Sql.Add('           ColumnFourteen,   ');
      Sql.Add('           ColumnFifteen,   ');
    end;
    Sql.Add('             ColumnSixteen,   ');
    if qrySelect.FieldByName('ColumnSixteen').AsSTring = 'Y' then
    begin
      Sql.Add('           ColumnSeventeen,         ');
      Sql.Add('           ColumnEighteen,         ');
      Sql.Add('           ColumnNineteen,         ');
    end;
    if qrySelect.FieldByName('ColumnTwenty').AsSTring = 'Y' then
    begin
      Sql.Add('           ColumnTwenty,  ');
      Sql.Add('           ColumnTwentyOne,        ');
      Sql.Add('           ColumnTwentyTwo,        ');
      Sql.Add('           ColumnTwentyThree,        ');
    end
    else
      Sql.Add('           ColumnTwenty,  ');
    Sql.Add('             ColumnTwentyFour) ');
    Sql.Add('Values(:ColumnOne, :ColumnTwo, :ColumnThree, :ColumnFour, ');
    Sql.Add('       :ColumnFive, ' + dateDB + ', :ColumnSeven,          ');
    Sql.Add('       :ColumnEight, :ColumnNine, :ColumnTen, ');
    Sql.Add('       :ColumnEleven,                                    ');
    Sql.Add('       :ColumnTwelve,                                    ');
    if qrySelect.FieldByName('ColumnTwelve').AsSTring = 'Y' then
      Sql.Add('     :ColumnThirteen, :ColumnFourteen, :ColumnFifteen,              ');
    Sql.Add('       :ColumnSixteen,                                      ');
    if qrySelect.FieldByName('ColumnSixteen').AsSTring = 'Y' then
      Sql.Add('     :ColumnSeventeen, :ColumnEighteen, :ColumnNineteen,                 ');
    if qrySelect.FieldByName('ColumnTwenty').AsSTring = 'S' then
    begin
      Sql.Add('   :ColumnTwenty,                                      ');
      Sql.Add('   :ColumnTwentyOne, :ColumnTwentyTwo, :ColumnTwentyThree,                ');
    end
    else
      Sql.Add('   :ColumnTwenty,                                      ');
    Sql.Add('     :ColumnTwentyFour)                                  ');
    {And then for all the parameteres, pass the value}
    Parameters.ParamByName('ColumnOne').Value := varColumnOne;
    ...
    Parameters.ParamByName('ColumnTwentyFour').Value := varColumnTwentyFour;
    ExecSQL;
  end;

我在这一行收到错误:

Sql.Add('       :ColumnTwelve,                                    ');

这是我插入语句中的第 11 个参数。 如果我评论这一行,我会在下一个参数中得到错误。 如果我这样直接放值:

Sql.Add('     ' + varColumnTwelve + ',                            ');

它工作正常,但我在下一个参数中收到错误。

所以这让我想知道:ADOQuery 是否对它可以处理的参数数量有限制?或者,如果这不是真正的问题,有人知道我该如何解决这个问题吗?


备注:

如果qrySelect没有'ColumnTwelve'那么

if qrySelect.FieldByName('ColumnTwelve').AsSTring = 'Y' then

将引发异常,因为 FieldByName 将 return nil

更改 TADOQuery 的 SQL 属性 会导致 TADOQuery 响应该更改,重新- 将修改后的 SQL 应用于内部 ADO 组件对象并重新解析 SQL 以识别任何参数。

因此,不建议以这种方式增量修改SQL。撇开其他不谈,在 SQL 完全 assembled.

之前一遍又一遍地应用和解析它是非常低效的

在这种情况下,当您添加第 11 个参数时,SQL 已被应用和解析 28 次!

随后产生的 AV 发生在 SQLOLEDB.DLL 中的事实表明,无论发生什么问题,都是 SQL 更改的结果应用于内部 ADO 对象而不是 VCL 处理以识别参数等。因此,您无法解决此问题。你能做的最好的就是避免它。

您可以通过在修改 SQL 的同时设置 ParamCheck := FALSE 来消除某些处理。这将防止 VCL 尝试重新解析修改后的 SQL 来识别参数。但是,它不会阻止 SQL 重新应用于基础 ADO 组件以响应每次更改。

作为诊断练习,您可以在修改 SQL 时尝试设置 ParamCheck := FALSE。完成后,调用 Parameters.Refresh 方法以确保更新参数集合以反映完成的 SQL:

qryInsert.ParamCheck := FALSE;
qryInsert.SQL.Add(..);
qryInsert.SQL.Add(..);
qryInsert.SQL.Add(..);
qryInsert.SQL.Add(..);

qryInsert.Parameters.Refresh;

注意: 将 ParamCheck 设置为 FALSE,您必须在尝试设置之前调用 Parameters.Refresh任何参数值,否则参数将不存在于参数集合中!

如果此更改后 AV 仍然出现,则这更强烈地表明内部 ADO 组件存在某些问题,无法很好地响应对 SQL 的重复更改,可能是由于未能正确处理不完整(语法不正确)SQL.

但是,您可以通过以下两种方式之一完全避免触发更改机制。

也许最简单的方法是在 TADOQuery [=115] 上使用 BeginUpdate/EndUpdate =] 围绕构建 SQL:

的代码的字符串列表
qryInsert.SQL.BeginUpdate;
try
  qryInsert.SQL.Add(..);
  qryInsert.SQL.Add(..);
  qryInsert.SQL.Add(..);

finally
  qryInsert.SQL.EndUpdate;
end;

这会抑制 ADO 查询对象内部的 OnChange 事件,直到调用 EndUpdate,此时SQL 将应用于内部 ADO 对象并更新查询对象的 参数

或者,您可以 assemble 您的 SQL 在一个完全独立的字符串列表中,然后将其应用于 TADOQuery SQL 属性 作为单个直接更改为 SQL.Text 属性:

sql := TStringList.Create;
try
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);

  qryInsert.SQL.Text := sql.Text;

finally
  sql.Free;
end;

无论哪种方式,结果将是 VCL 将解析参数并且内部 ADO 对象将仅更新 一次,具有完整且(希望)语法正确的 SQL声明。

第二种方法可以少一些"boilerplate" - 这里的try..finally纯粹是为了管理临时字符串列表。如果您为此目的在更广泛的范围内重新使用一个对象,或者使用 SQL 生成器助手 class 生成一个简单的字符串(就像我一样),那么就不需要这个特定的 try..finally,使应用更方便、更简洁:

SQLBuilder.Insert('MyTable');
SQLBuilder.AddColumn('ColumnOne');
SQLBuilder.AddColumn('ColumnTwo');

qryInsert.SQL.Text := SQLBuilder.SQL;

// qryInsert.SQL == INSERT INTO MyTable (ColumnOne, ColumnTwo)
//                  VALUES (:ColumnOne, :ColumnTwo)    

例如

字符串与 TStringList

如果您首选的构建 SQL 的技术生成的是字符串列表而不是简单的字符串,您可能会想直接分配字符串列表:

  qryInsert.SQL := sql;

但请注意,这会执行 sql 字符串列表的 Assign(),有效地执行 'deep copy'.您仍然需要确保分配的字符串列表(上面代码中的sql)被适当释放。

另请注意,这也不太有效,因为它还会复制字符串列表的其他属性,包括与列表中每个字符串关联的任何对象。在这种情况下,如果您只对复制字符串列表的 Text 内容感兴趣,则无需招致(轻微的)和不必要的开销。

The AV only (and always) appears when debugging, it does never appear if I execute the application directly through its ".exe".

....

The insert works after all the AVs appeared on the screen. I just want to understand why am I getting this error when everything looks fine.

在外部模块中引发访问冲突,该模块使用 Delphi 以外的语言实现。外部代码很可能按设计正确运行,并且访问冲突是意料之中的。

这听起来可能很奇怪,但外部代码清楚地处理了异常,因为控制不会传递给代码的异常处理程序。如您所见,程序运行正常。

这是所谓的 first chance exception. The debugger is notified and breaks. But then control returns to the program and in this case the program deals with the exception and continues. And it is perfectly normal, albeit perhaps counter-intuitive, for code to raise a first chance access violation exception, but still be functioning correctly. As evidence for that claim, see this from an article 由 VS 开发团队成员编写的:

Why the VS Debugger does not stop on first chance Access Violations (by default)?

....

The reason the default for first-chance AVs does not stop is that sometimes Windows calls will AV, and then catch the exception themselves and carry on happily. If we did default to stopping on first chance AVs we would stop users in some strange place in say kernel32.dll and many would be very confused.

所以就正确性而言,我认为没有什么可担心的。但它确实使调试变得困难。尝试@Deltics 提出的各种建议。如果通过进行这些更改您碰巧避免了异常,那就太好了。否则,您可能需要至少暂时禁止调试器中断异常。