断开连接后恢复状态 dsInsert 中的 TADOQuery

Recover TADOQuery in State dsInsert after disconnect

我们使用 Delphi TADOQuery 与插入的显式连接。

总结: 当查询处于状态 dsInsert 时连接丢失,查询似乎进入了与基础 ADO 记录集相关的不一致状态。因此,即使已重新建立连接,也无法再使用查询。

详情:

假设以下简化步骤:

  quTest.Connection:= ADOConnection1;
  quTest.Open;
  quTest.Insert;
  //Simulate lost connection
  ADOConnection1.Close;

  try
    //quTest.State is still dsInsert
    quTest.Post; //Throws 'Operation is not allowed when the object is closed'. This is the expected beavior.
  except
    //Reconnect (simplified algorithm)
    ADOConnection1.Connected:= true;
  end;

  //quTest.State is still dsInsert
  //So far, so good.
  //Now let's close or abort or somehow reset quTest so that we can use it again. How? 
  quTest.Close //throws 'Operation is not allowed when the object is closed'

问题是在上述代码示例的末尾,quTest 仍处于 dsInsert 状态,但基础 ADO 记录集已断开连接。 任何关闭或以某种方式重置 quTest 的尝试都会失败,并出现异常 'Operation is not allowed when the object is closed'.

请注意,我们的目标不是继续初始插入操作。我们只想将查询恢复到可以再次打开和使用它的状态。

这可能吗?

由于 quTest 是具有设计时字段绑定的数据模块的一部分,我们无法轻松释放损坏的查询并创建新的查询实例。

编辑: 当然,断开连接的模拟不太现实。 然而,比较生产错误和测试样本的堆栈跟踪,我们发现测试足够好。

Production stack trace:

================================================================================
Exception class  : EOleException
Exception message: Operation is not allowed when the object is closed
EOleException.ErrorCode : -2146824584
================================================================================
[008B9BD7] Data.Win.ADODB.TCustomADODataSet.InternalGotoBookmark + 
(0000E290) [0040F290]
[008B9BD7] Data.Win.ADODB.TCustomADODataSet.InternalGotoBookmark + 
[008B9BF4] Data.Win.ADODB.TCustomADODataSet.InternalSetToRecord + 
[0081EEBE] Data.DB.TDataSet.InternalSetToRecord + 
[0081D576] Data.DB.TDataSet.SetCurrentRecord + 
[0081D9A4] Data.DB.TDataSet.UpdateCursorPos + 
[0081E378] Data.DB.TDataSet.Cancel + 
[0081AA49] Data.DB.TDataSet.SetActive + $AD
[0081A841] Data.DB.TDataSet.Close + 


Test case stack trace:

Data.Win.ADODB.TCustomADODataSet.InternalFirst
Data.DB.TDataSet.SetCurrentRecord(0)
Data.DB.TDataSet.UpdateCursorPos
Data.DB.TDataSet.Cancel
Data.DB.TDataSet.SetActive(???)
Data.DB.TDataSet.Close

事实上,由于查询状态仍然是 dsInsert,因此尝试 .Cancel,导致后续对 ADO 记录集的调用失败。

procedure TDataSet.SetActive(Value: Boolean);
begin
...
        if State in dsEditModes then Cancel;
...
end;

编辑 2: 该问题不容易重现,因为它似乎与数据有关。 这就是我创建控制台测试程序的原因。 请运行两次测试程序,并在主程序块中更改测试用例。 我机器上的测试输出如下所示。

控制台测试程序:

program Project2;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Data.DB,
  Data.Win.ADODB,
  ActiveX;

procedure Setup(aConnection: TADOConnection; aEmpty: Boolean);
var
  query: TADOQuery;
begin
  query:= TADOQuery.Create(nil);
  try
    query.Connection:= aConnection;

    //Create test table
    try
      query.SQL.Add('create table test3 (a int)');
      query.ExecSQL;
      WriteLn('Table created.');
    except
      on e: Exception do
        Writeln(e.Message);
    end;

    //Clear test table
    query.SQL.Clear;
    query.SQL.Add('delete test3');
    query.ExecSQL;

    if not aEmpty then begin
      //Create a row
      query.SQL.Clear;
      query.SQL.Add('insert into test3 values (0)');
      query.ExecSQL;
    end;
  finally
    query.Free;
  end;
end;

var
  con: TADOConnection;
  query: TADOQuery;
begin
  CoInitialize(nil);
  try
    con:= TADOConnection.Create(nil);
    query:= TADOQuery.Create(nil);
    try
      con.ConnectionString:= 'Provider=SQLOLEDB.1;Persist Security Info=False;Integrated Security=SSPI;Data Source=10.0.0.11,1433;Initial Catalog=TestDB';
      con.Connected:= true;

      //Test case 1: With data
      Setup(con, false);

      //Test case 2: No data
      //Setup(con, true);

      query.Connection:= con;
      query.SQL.Add('select * from test3');
      query.Open;
      query.Insert;
      con.Close;
      WriteLn('query.Active: ' + BoolToStr(query.Active));
      WriteLn('query.State: ' + IntToStr(Ord(query.State)));
      query.Close;
      WriteLn('Test ran without exception.');
    except
      on E: Exception do
        Writeln('Exception: ' + E.ClassName, ': ', E.Message);
    end;
  finally
    ReadLn;

    query.Free;
    con.Free;
  end;
end.

测试环境:

测试于:

测试用例 1 的输出:

There is already an object named 'test3' in the database
query.Active: 0
query.State: 0
Test ran without exception.

测试用例 2 的输出:

There is already an object named 'test3' in the database
query.Active: -1
query.State: 3
Exception: EOleException: Operation is not allowed when the object is closed

我不喜欢发布实际上没有回答问题的答案,但是 在这种情况下,我认为我应该这样做,因为我根本无法重现您在 您对 quTest 状态的评论。或许是分歧 我的结果和你的结果之间是由于你的代码或对象属性的某些部分 您的问题中不包含这些内容。

请试试这个(我已经在 D7 和西雅图测试过):

开始一个新项目并在您的表单上放置一个 TAdoConnection 和 TAdoQuery。 仅进行下面 DFM 摘录中显示的 属性 更改;

设置下面代码摘录中显示的事件处理程序。

BeforeCloseBeforeCancel 处理程序中放置断点,并在

中放置一个断点
quTest.Post

然后编译,运行点击Button1。

得到的结果如下:

  1. BeforeClose 行程的 BP。

  2. BeforeCancel 行程的 BP。

  3. quTest.Post 次行程的 BP。

在第3步,quTest的状态是dsInactive,它的Active 属性是False。 这些值和预先调用 Before ... 事件的事实是 正是我所期望的,因为调用 AdoConnection.Close 关闭 使用它作为 Connection.

的数据集

所以,我认为如果你的应用程序得到不同的结果,你需要解释 为什么因为我认为我已经表明一个测试项目没有展示 您报告的行为。

更新 2

  1. 应OP的要求,我在table中添加了int列'a',并在[=中添加了相应的Parameter 25=] 并添加了

    quTest.Parameters.ParamByName('a').值:= 0;

在我两次调用 quTest.Open 之前。这使得 no 在 BP on quTest.Post trips 时与 quTest 的 State 和 Active 属性不同:它们仍然分别为 dsInactive 和 False。

  1. 正如 OP 所说,他只想在中止插入后继续使用 quTest,我将 except 块中的 quTest.Post; 替换为 quTest.Open;。在那之后,一旦发生 Inssert 异常,我可以继续使用 quTest 而不会出现任何明显的问题——我可以手动执行删除、插入和编辑,这些都会正确地传回服务器,这样当应用程序是重新运行,这些变化一直存在。

更新 3。 OP 似乎怀疑调用 AdoConnection1.Close 会导致 quTest 被关闭。它 确实 。要验证这一点,请在 Form1.quTest.RecordSetState 和 运行 上监视应用程序,直至 AdoConnection1.Close。然后,追踪到那个电话。你会发现 TCustomConnection.SetConnected 调用 DoDisconnect 调用 ConnectionObject.Close。将 quTest.RecordSetState 设置为 stClosed 以便当 TAdoConnection.Disconnect 执行

for I := 0 to DataSetCount - 1 do
    with DataSets[I] do
      if stClosed in RecordsetState then Close;

quTest 已关闭。

示例代码

  TForm1 = class(TForm)
    ADOConnection1: TADOConnection;
    quTest: TADOQuery;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure quTestBeforeCancel(DataSet: TDataSet);
    procedure quTestBeforeClose(DataSet: TDataSet);
  public
    { Public declarations }
    procedure TestReconnect;
  end;

[...]

procedure TForm1.FormCreate(Sender: TObject);
begin
  quTest.Open;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  TestReconnect;
end;

procedure TForm1.quTestBeforeCancel(DataSet: TDataSet);
begin
  Caption := 'Before Cancel';
end;

procedure TForm1.quTestBeforeClose(DataSet: TDataSet);
begin
  Caption := 'Before close';
end;

procedure TForm1.TestReconnect;
begin
  quTest.Connection:= ADOConnection1;
  quTest.Open;
  quTest.Insert;
  //quTest.FieldByName('Name').AsString := 'yyyy'; added by MA

  //Simulate lost connection
  ADOConnection1.Close;

  try
    quTest.Post; //Throws 'Operation is not allowed when the object is closed'
  except
    //Reconnect (simplified algorithm)
    ADOConnection1.Connected:= true;
    quTest.Post;
  end;
end;

end.

部分 DFM

object ADOConnection1: TADOConnection
  Connected = True
  ConnectionString =
    'Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Initia' +
    'l Catalog=MATest;Data Source=MAI7'
  Provider = 'SQLOLEDB.1'
  Left = 24
  Top = 24
end
object quTest: TADOQuery
  Connection = ADOConnection1
  CursorType = ctStatic
  BeforeClose = quTestBeforeClose
  BeforeCancel = quTestBeforeCancel
  Parameters = <>
  SQL.Strings = (
    'Select * from TestTable')
  Left = 64
  Top = 24
end

Update 1下面的代码允许完成pending insert 在 except 块中。请注意在 except 块中 缺少 quTest.Post 的调用。

procedure TForm1.TestReconnect;
const
  SaveFileName = 'C:\Temp\testdata.xml';
begin
  quTest.Connection:= ADOConnection1;
  quTest.Open;
  quTest.Insert;
  quTest.FieldByName('Name').AsString := 'yyyy';
  quTest.SaveToFile(SaveFileName, pfXML);
  //Simulate lost connection
  ADOConnection1.Close;

  try
    quTest.Post; //Throws 'Operation is not allowed when the object is closed'
  except
    //Reconnect (simplified algorithm)
    ADOConnection1.Connected:= true;
    quTest.LoadFromFile(SaveFileName);
  end;
end;

观察到的行为的原因是 Delphi XE6 或之前的更改,我认为这是一个错误。

https://quality.embarcadero.com/browse/RSP-15545

总结:

  • 此问题在 Delphi 2007 和 Delphi XE 中没有出现。
  • 问题出现在Delphi10.1
  • 在 TDataSet.SetActive 中的 XE6 中或之前引入了有问题的代码更改,其中添加了对 Cancel 的新调用。
  • 此调用在描述的场景中失败,导致描述的效果。