关于 Delphi:Delphi – 为什么我会遇到访问冲突? ADOQuery 参数有限制吗?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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;

我在这一行得到错误:

1
Sql.Add('       :ColumnTwelve,                                    ');

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

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

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

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

注释:

  • 我正在使用 Delphi 7 和 Windows 8.1。

  • AV 只(并且总是)在调试时出现,如果我直接通过它的 ".exe" 执行应用程序,它就永远不会出现。

  • 如果我在出现错误后一直按"运行",它会显示越来越多的AV(我认为AV的数量与10号之后添加的参数数量相同),直到应用程序继续正常运行。

  • 在所有 AV 出现在屏幕上后,插入才起作用。我只是想了解为什么在一切正常时会出现此错误。


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

因此,不建议以这种方式增量修改 SQL。除了其他任何事情之外,在完全组装之前一遍又一遍地应用和解析 SQL 是非常低效的。

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

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

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

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

1
2
3
4
5
6
7
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.

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

也许最简单的方法是在构建 SQL 的代码周围的 TADOQuery SQL 字符串列表上使用 BeginUpdate/EndUpdate:

1
2
3
4
5
6
7
8
9
qryInsert.SQL.BeginUpdate;
try
  qryInsert.SQL.Add(..);
  qryInsert.SQL.Add(..);
  qryInsert.SQL.Add(..);

finally
  qryInsert.SQL.EndUpdate;
end;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 语句。

第二种方法可以少一些"样板"——try..finally 这里纯粹是为了管理临时字符串列表。如果您为此目的在更广泛的范围内重新使用一个对象,或者使用产生一个简单字符串的 SQL 构建器帮助程序类(就像我一样),那么就不需要这个特定的 try..finally,让它多一点使用方便、清洁:

1
2
3
4
5
6
7
8
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 的首选技术产生一个字符串列表而不是一个简单的字符串,您可能会想直接分配字符串列表:

1
  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 以外的语言实现。很可能外部代码的行为正确且符合设计,并且访问冲突是预期的。

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

这就是所谓的第一次机会例外。调试器收到通知并中断。但随后控制权返回给程序,在这种情况下,程序处理异常并继续。代码引发第一次机会访问冲突异常,但仍然正常运行,这是完全正常的,尽管可能违反直觉。作为该声明的证据,请参阅 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 提出的各种建议。如果通过进行这些更改,您碰巧避免了异常,那就太好了。否则,您可能需要至少暂时禁止调试器中断异常。


如果 qrySelect 没有 \\'ColumnTwelve\\' 那么

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

将引发异常,因为 FieldByName 将返回 nil