Reputation: 431
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
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:
Upvotes: 0
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:
The BP on BeforeClose trips.
The BP on BeforeCancel trip.
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
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.
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