nang
nang

Reputation: 431

Recover TADOQuery in State dsInsert after disconnect

We use a Delphi TADOQuery with an explicit connection for inserts.

Summary: When the connection is lost while the query is in State dsInsert, the query seems to enter an inconsistent state with respect to the underlying ADO recordset. As a result, the query cannot be used anymore, even if the connection has been reestablished.

Details:

Assume the following simplified steps:

  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'

The problem is that at the end of the code sample above quTest is still in State dsInsert, but the underlying ADO recordset is disconnected. Any attempt to close or somehow reset quTest fails with an exception 'Operation is not allowed when the object is closed'.

Please note that our goal is not to continue the initial insert operation. We just want to bring the query back to a state where we can open and use it again.

Is this possible?

Since quTest is part of a datamodule with design-time field bindings, we cannot easily free the broken query and create a new query instance.

Edit: Of course, the simulation of the disconnect is not too realistic. However, comparing the stack traces of the production error and the test sample, we see that the test is good enough.

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 + $17
(0000E290) [0040F290]
[008B9BD7] Data.Win.ADODB.TCustomADODataSet.InternalGotoBookmark + $17
[008B9BF4] Data.Win.ADODB.TCustomADODataSet.InternalSetToRecord + $14
[0081EEBE] Data.DB.TDataSet.InternalSetToRecord + $2
[0081D576] Data.DB.TDataSet.SetCurrentRecord + $62
[0081D9A4] Data.DB.TDataSet.UpdateCursorPos + $10
[0081E378] Data.DB.TDataSet.Cancel + $68
[0081AA49] Data.DB.TDataSet.SetActive + $AD
[0081A841] Data.DB.TDataSet.Close + $9


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

In fact, since the query state is still dsInsert, an attempt to .Cancel is made, resulting in subsequent calls to the ADO recordset which fail.

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

Edit 2: The problem is not easy to reproduce, since it seems to be data dependent. That's why I created a console test program. Please run the test program twice and change the test case in the main block. The output of the tests at my machine is shown below.

Console test program:

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.

Test-Environment:

Tested on:

Output of test case 1:

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

Output of test case 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

Upvotes: 1

Views: 1964

Answers (2)

nang
nang

Reputation: 431

The reason for the observed behavior is a change in or before Delphi XE6, which I think is a bug.

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

Summary:

  • The problem does not occur in Delphi 2007 and Delphi XE.
  • The problem occurs in Delphi 10.1
  • The problematic code change has been introduced in or before XE6 in TDataSet.SetActive, where a new call to Cancel has been added.
  • This call fails in the described scenario leading to the described effects.

Upvotes: 0

MartynA
MartynA

Reputation: 30715

I don't like posting an answer which doesn't actually answer the question, but in this case I think I should because I simply cannot reproduce what you've said in your comments regarding the state of your quTest. Perhaps the divergence between my results and yours is due to some part of your code or object properties that are not included in your question.

Please try this (I've tested it in D7 and Seattle):

Start a new project and drop a TAdoConnection and TAdoQuery on your form. Make only the property changes shown in the DFM extract below;

Set up the event handlers shown in the code extract shown below.

Put breakpoints inside the BeforeClose and BeforeCancel handlers, and one on

quTest.Post

then compile, run and click Button1.

What I get is as follows:

  1. The BP on BeforeClose trips.

  2. The BP on BeforeCancel trip.

  3. The BP on quTest.Post trips.

At step 3, the state of quTest is dsInactive and its Active property is False. Those values and the fact that the Before ... events are called beforehand are precisely what I would expect, given that calling AdoConnection.Close closes the dataset(s) using it as their Connection.

So, I think that if you get different results with your app, you need to explain why because I think I've shown that a test project does not exhibit the behaviour you've reported.

Update 2

  1. At the request of the OP, I added an int column 'a' to the table, and a corresponding Parameter to quTest and added

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

before both my calls to quTest.Open. This makes no difference to the State and Active properties of quTest when the BP on quTest.Post trips: they are still dsInactive and False, respectively.

  1. As the OP has said he just want to be able to carry on using quTest after the aborted Insert, I replaced quTest.Post; in the except block by quTest.Open;. After that, once the Inssert exception has occurred, I can carry on using quTest without any apparent problem - I can do Deletes, Inserts and Edits manually and these are correctly passed back to the server, so that when the app is re-run, these changes have persisted.

Update 3. The OP seems to be in some doubt that calling AdoConnection1.Close results in quTest being closed. It does. To verify this put a watch on Form1.quTest.RecordSetState and run the appl as far as AdoConnection1.Close. Then, trace into that call. You will find that TCustomConnection.SetConnected calls DoDisconnect which calls ConnectionObject.Close. That sets quTest.RecordSetState to stClosed so that when TAdoConnection.Disconnect executes

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

quTest is closed.

Sample code

  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.

Partial 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 The following code allows the completion of the pending insert in the except block. Note the absence of a call to quTest.Post in the except block.

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;

Upvotes: 3

Related Questions