How to properly start, working and finish a transaction? - delphi

I'm using MySQL, and I know that Nested Connection are not allowed - use "save points" for this - but I would like create a more generic code that could also be used with other DBMS.
So, I would like know how to properly start, working and finish a transaction in the code below?
Once ExampleDAO.Save() function could be used inside other function, like OtherExampleDAO.Save(), I need verify a transaction has been started before I try start a new one.
The lines with the verification if Assigned(dbTransaction) then always returns true, so how to properly verify if dbTransaction was instantiated?
function TExampleDAO.Save(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
Result := False;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;

You need to properly initialize dbxTransaction to nil at the start of your function. Local variables in Delphi (on the Win32 platform, at least) are not initialized until a value is assigned to them, meaning that the content is unknown. Passing any value other than nil to Assigned will result in True. I recommend never testing a local variable's content on any platform until it has had a value assigned in your code.
Here's an example of how to make it work. (I've also removed the unnecessary assignment to Result in the exception block.)
function TExampleDAO.Salve(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
dbxTransaction := nil; // Initialize the transaction variable here
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
As was noted by #SirRufo in the comments to your question, failing to pass Example as a parameter should probably raise an exception as well, which would mean that it could become a procedure instead of a function and Result would no longer apply at all.

Related

How to correctly qualify Result variable inside lambda

function TFlatBlock.SelectRow(const C:TConditionR):TArray<TFlatRow>;
begin
Result := nil;
if not Assigned(C) then Exit;
//
SearchRow(function(const R:TFlatRow):Boolean
begin
// Result from TFlatBlock.SelectRow !!!
if C(R) then Result := Result + [R];
// Result from lambda
Result := false;
end);
end;
How to qualify var Result in this case? Yes I can use local variable instead, but maybe there is a way to do this without it.

How to cache function Boolean result

I have this function:
var
_WordApplicationExistsCache: Integer = -1; // Cache result
function WordApplicationExists: Boolean;
var
WordObj: OleVariant;
begin
if (_WordApplicationExistsCache = -1) then
begin
Result := False;
try
try
WordObj := CreateOleObject('Word.Application');
WordObj.Visible := False;
WordObj.Quit;
WordObj := Unassigned;
Result := True;
except
// error
end;
finally
_WordApplicationExistsCache := Ord(Result); // 0;1
end;
end
else
begin
Result := Boolean(_WordApplicationExistsCache);
end;
end;
I'm trying to call this function only once in the Application lifetime. I might not call this function at all.
Is this the correct pattern? Can this be done better?
EDIT:
Another way I can think of, in this case is to use 2 variables:
var
_WordApplicationExistsInitialized: Boolean = False; // Cache result
_WordApplicationExistsCacheResult: Boolean; // Undefined ?
function WordApplicationExists: Boolean;
var
WordObj: OleVariant;
begin
if not _WordApplicationExistsInitialized then
begin
_WordApplicationExistsInitialized := True;
Result := False;
try
try
WordObj := CreateOleObject('Word.Application');
WordObj.Visible := False;
WordObj.Quit;
WordObj := Unassigned;
Result := True;
except
// error
end;
finally
_WordApplicationExistsCacheResult := Result;
end;
end
else
begin
Result := _WordApplicationExistsCacheResult;
end;
end;
What bugs me a bit about the first version is the type casting Boolean<->Integer. If Boolean could be initialized to nil it would have been perfect (I think).
Use a TriState type for the cached result.
type
TTriState = ( tsUnknown, tsFalse, tsTrue );
var
_WordApplicationExists : TTriState = tsUnknown;
function WordApplicationExists : Boolean;
var
WordObj: OleVariant;
begin
if _WordApplicationExists = tsUnknown
then
try
WordObj := CreateOleObject('Word.Application');
WordObj.Visible := False;
WordObj.Quit;
WordObj := Unassigned;
_WordApplicationExists := tsTrue;
except
_WordApplicationExists := tsFalse;
end;
Result := _WordApplicationExists = tsTrue;
end;
This code will work fine, and is correctly implemented. A nullable boolean or a tristate enum will read better, but fundamentally the logic would be the same.
It's heavy handed and clunky approach though, invoking an instance of Word that is then thrown away. Personally I would read the registry to check whether or not the COM object is registered. I would not attempt to anticipate the case where the object is registered but cannot be created. In my view that is an exceptional case that should be handled when it occurs, but not before.
Another way to go is simply not to attempt to check ahead of time for the Word COM object being available. Just go ahead and attempt to create the object when you need to use it. If this fails, deal with that. If you wish to remember that it failed, do so. But you really should avoid creating the object twice when once will suffice.
This could be done also with a Variant type. Variants are set to Unassigned. (reference)
var
_WordApplicationCanCreate: Variant; // Unassigned (VType = varEmpty)
function WordApplicationCanCreate: Boolean;
var
WordObj: OleVariant;
begin
if VarIsEmpty(_WordApplicationCanCreate) then
try
WordObj := CreateOleObject('Word.Application');
WordObj.Visible := False;
WordObj.Quit;
WordObj := Unassigned;
_WordApplicationCanCreate := True;
except
_WordApplicationCanCreate := False;
end;
Result := _WordApplicationCanCreate = True;
end;

How to implement long running queries on IdhttpServer?

What is good way to implement long running queries on IdHttpServer. I have written simple logic to do so, please advise suggest better way to achive the same as I'm struggling with its performance.
I am using D2010 and Indy 10.5.8 to achieve the goal, also suggest if we retrive values frequently from session will that be a resource intensive ?
procedure TForm1.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
SessionObj : TSessionData;
begin
if ARequestInfo.Document = 'EXECUTEQUERY' then
begin
if not Assigned(ARequestInfo.Session.Content) then
begin
SessionObj := TSessionData.Create;
ARequestInfo.Session.Content.AddObject('U_SESSION', SessionObj);
SessionObj.RunLongQuery;
end;
end;
if ARequestInfo.Document = 'GETDATA' then
begin
SessionObj := TSessionData(ARequestInfo.Session.Content.Objects[ ARequestInfo.Session.Content.IndexOf('U_SESSION')]);
if SessionObj.GetQueryStat = Done then
begin
AResponseInfo.ContentStream.CopyFrom(SessionObj.GetMemStream, SessionObj.GetMemStream.Size);
SessionObj.GetMemStream.Clear;
AResponseInfo.ResponseNo := 200;
end else if SessionObj.GetQueryStat = Error then
AResponseInfo.ResponseNo := 500
else AResponseInfo.ResponseNo := 102;
end;
end;
procedure TForm1.ServerSessionEnd(Sender: TIdHTTPSession);
begin
TSessionData(Sender.Content.Objects[ Sender.Content.IndexOf('U_SESSION')]).Free;
end;
{ TProcessQuery }
constructor TProcessQuery.Create;
begin
myConn := TMyConnection.Create(nil);
myConn.LoginPrompt := False;
myConn.UserName := 'UserName';
myConn.Password := 'Password';
myConn.Server := 'Host';
myConn.Database := 'DBName';
myConn.Connected := True;
myQuery := TMyQuery.Create(nil);
myQuery.Unidirectional := True;
myQuery.Options.CreateConnection := False;
myQuery.Connection := myConn;
Fstat := None;
Fstream := TMemoryStream.Create;
end;
destructor TProcessQuery.Destroy;
begin
if Assigned(myConn) then begin
myConn.Close;
myConn.Disconnect;
FreeAndNil(myConn);
end;
end;
procedure TProcessQuery.ExecuteQuery;
begin
Status := Started;
myQuery.SQL.Text := '<Some Query>';
myQuery.Open;
try
try
while not myQuery.Eof do
begin
Status := Inprogress;
//Add to FStream which would be returned to user.
end;
except
on Exception do
Status := Error;
end;
finally
myQuery.Close;
end;
end;
{ TSessionData }
constructor TSessionData.Create;
begin
FProcessQuery := TProcessQuery.Create;
end;
function TSessionData.GetMemStream: TMemoryStream;
begin
result := FProcessQuery.Fstream;
end;
function TSessionData.GetQueryStat: TStatus;
begin
result := FProcessQuery.Status;
end;
procedure TSessionData.RunLongQuery;
begin
FProcessQuery.ExecuteQuery
end;
You are running the actual query in the context of ServerCommandGet(), so the client will not receive a reply until the query has finished. For what you are attempting, you need to move the query to its own thread and let ServerCommandGet() exit so the client gets a reply and can move on, thus freeing it to send subsequent GETDATA requests. In ServerSessionEnd(), you will have to terminate the query thread if it is still running, and free the TSessionData object.
There are some other problems with your code as well.
ServerCommandGet() is checking for not Assigned(ARequestInfo.Session.Content) and then calling ARequestInfo.Session.Content.AddObject() when ARequestInfo.Session.Contentis nil. I don't see any code that is creating the ARequestInfo.Session.Content object.
If the client issues multiple EXECUTEQUERY requests, you are storing them all in AResponseInfo.Session.Content using the same name, 'U_SESSION'. GETDATA will return the results of only the first query it finds, and ServerSessionEnd() only frees the first query it finds. So either give each query a unique name and send that back to the client so it can include it in GETDATA and make ServerSessionEnd() loop through the entire Sender.Content, or else do not allow multiple queries in the same HTTP session. If the client issues a new EXECUTEQUERY while a previous query is still active, kill the previous query before starting the new one.
When the client issues GETDATA, the code needs to take into account that the requested query may not exist, such as if it previously expired and was freed by ServerSessionEnd(). Also, if a query does exist and has finished, you are calling AResponseInfo.ContentStream.CopyFrom() but AResponseInfo.ContentStream is nil when ServerCommandGet() is called. You are responsible for providing your own ContentStream object. So either take ownership of TSessionData's memory stream and assign it as the AResponseInfo.ContentStream object, or else create a new TMemoryStream to copy into and then assign that as the AResponseInfo.ContentStream object. Either way, TIdHTTPServer will free the AResponseInfo.ContentStream after sending it to the client.

How to terminate a parallel.foreach from OTL in Delphi xe2

I am learning how to use OmniThreadLibrary in Delphi XE2, i wonder if someone can show me how to cancel a parallel.foreach.
I read that I should use a cancellation token, but I cannot find an example of some sort on how to use it.
This is the original for loop inside the function.
function SomeFunction() : string;
begin
for value := 0 to length(listOfThings)-1 do
begin
Chain := Function1( listOfThings[value] );
if Evaluate( Chain , Solution) then
Parameters[value] := Solution
else
begin
Result := 'ERROR';
exit;
end;
end;
end;
And this is how I am using the Parallel.ForEach
function SomeFunction() : string;
begin
Parallel.ForEach(0, length(listOfThings)-1 ).Execute(
procedure (const value: integer)
var Chain : string;
begin
Chain := Function1(listOfThings[value]);
if Evaluate(Chain , Solution) then
Parameters[value] := Solution
else
begin
Result := 'ERROR'; //Here is where it won't work
exit;
end;
end
);
end;
Inside the Parallel.ForEach I can't do Result := 'ERROR' because it is not captured inside the procedure, so I think if I can cancel the Parallel.ForEach and report that cancellation, then I can just assign Result := 'ERROR' outside.
But I am new to OmniThreadLibrary and I don't know how to do such a thing, please help me :)
You need to use a cancellation token:
var
cancelToken: IOmniCancellationToken;
You obtain the cancellation token by calling CreateOmniCancellationToken from the OtlSync unit.
cancelToken := CreateOmniCancellationToken;
You then supply the token to the parallel loop:
Parallel.ForEach(...)
.CancelWith(cancelToken)
.Execute(...);
And you signal the cancellation token by calling its Signal method.
cancelToken.Signal;
From outside the parallel loop you can use
cancelToken.IsSignaled
to detect that you cancelled. Or you can capture a boolean variable from the surrounding scope and pass the information through that variable.
The example here gives an illustration.
The cancellation token is only half of it. If you need it to return a value, you'll need to use Aggregate, because you can have an arbitrary number of elements in the sequence, but only one return value, so you need to collapse (aggregate) an arbitrary number of return values into one final value. So you want something like this:
function SomeFunction() : string;
var
cancelToken: IOmniCancellationToken;
error: TOmniValue;
begin
cancelToken := CreateOmniCancellationToken;
error := Parallel.ForEach(0, length(listOfThings)-1 ).
CancelWith(cancelToken).
Aggregate('',
procedure(var aggregate: TOmniValue; const value: TOmniValue)
var Chain : string;
begin
Chain := Function1(listOfThings[value]);
if Evaluate(Chain , Solution) then
Parameters[value] := Solution
else
begin
aggregate := 'ERROR';
cancelToken.signal;
end;
end).
Execute(
procedure(const value: TOmniValue; var result: TOmniValue)
begin
if value <> '' then
result := value;
end);
if error <> '' then
//something went wrong
end;
This may not be exactly perfect, but it should get you on the right track.

Why do I get an access violation when I access a TValueListEditor found with FindControl?

I have dynamically created TValueListEditor VCL component on a TForm. The code is located in nested procedure of one of the main form's methods. I have set:
ValueListEditor.KeyOptions := [keyEdit, keyAdd, keyUnique];
It looks like this:
TMainForm.Method();
Method has a nested procedure that contains code that creates the components mentioned above.
Then, I have helper function:
function GetMenuListData(XMLNode: TXMLNode; const XNMLDoc: string = '') : string;
In this helper I use this code to load an XML file and then retrieve its nodes and insert them into ValueListEditor.
XMLDoc := TXMLDocument.Create(Self);
XMLDoc.ParseOptions := [poPreserveWhiteSpace];
try
XMLDoc.LoadFromFile(XNMLDoc);
try
Control := FindControl(FindWindow('TForm',PChar('(' + ExtractFileExt(Form1.Edit1.Text) + ')')));
if Control <> nil then
begin
TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1] := XMLDoc.DocumentElement.NodeName;
if XMLDoc.DocumentElement.ChildNodes.First.AttributeNodes.Count > 0 then
TValuelistEditor(Control).Values[TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1]] := String(XMLDoc.DocumentElement.Attributes['id'])
else
TValuelistEditor(Control).Values[TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1]] := '<Empty>';
end else begin
MessageBeep(0);
FlashWindow(Application.Handle, True);
ShowMessagePos('...');
end;
finally
XMLDoc.Active := False; Result := 'Forced ' + Form1.RAWInputBtn.Caption + ' in ' + DateTimeToStr(Now);
end;
except
on E : EXMLDocError do
begin
Result := 'Forced ' + Form1.RAWInputBtn.Caption + ' in ' + DateTimeToStr(Now);
end;
end;
The problem is that I get access violations every time code goes into the line:
TValuelistEditor(Control).Keys[TValuelistEditor(Control).RowCount-1] := XMLDoc.DocumentElement.NodeName;
I have tried various typecasts, values, parameters .. nothing does the trick.
What is my mistake?
I'm using Delphi XE.
As Ken commented your problem is, instead of finding the value list editor, you are finding your form and then typecasting it to a value list editor, hence the AV.
First, you're passing 'TForm' as 'lpClassName' to FindWindow. Assuming 'TForm' is the class name of your form, it will of course find the form - not a child window on it. Second, you cannot use FindWindow to find a child window, see its documentation, it searches top-level windows.
If you had tested the return of FindControl, the code raising the AV would never run:
if (Control <> nil) and (Control is TValueListEditor) then
You can use FindWindowEx to search in child windows, if you don't know the handle of your form find it first as you've done already:
FormHandle := FindWindow('TForm',PChar('(' + ExtractFileExt(Form1.Edit1.Text) + ')'));
if FormHandle <> 0 then
begin
Control := FindControl(FindWindowEx(FormHandle, 0, 'TValueListEditor', nil));
or better yet, test the return of FindWindowEx first to avoid passing '0' to FindControl:
ValueListEditorHandle := FindWindowEx(FormHandle, 0, 'TValueListEditor', nil);
if Win32Check(ValueListEditorHandle <> 0) then
begin
Control := FindControl(ValueListEditorHandle);
if Assigned(Control) then
begin
...
If your dynamically created form is part of the same application, you don't need all the noise of the incorrect FindControl(FindWindow()). Just create your form, giving it a name, and making Application the owner:
MyForm := TMyForm.Create(Application);
MyForm.Name := 'MyDynamicForm';
When you want to get a new reference to it:
var
TheForm: TMyForm;
i: Integer;
begin
TheForm := nil;
for i := 0 to Screen.FormCount - 1 do
if Screen.Forms[i] is TMyForm then
// Could also use Screen.Forms[i].Caption
if Screen.Forms[i].Name = 'MyDynamicForm' then
TheForm := TMyForm(Screen.Forms[i]);
if Assigned(TheForm) then
TheForm.MethodThatLoadsXML(XMLFileName); // or whatever
end;
TheForm.MethodThatLoadsXML can now access the TValueListEditor directly:
procedure TMyForm.MethodThatLoadsXML(const XMLFileName: string);
begin
// Load xml as before, using XMLFileName
with TValueListEditor.Create(Self) do
begin
Options := [Whatever];
Parent := Self;
Left := SomeNumber;
Top := SomeNumber;
// Create items for value list from XML and other stuff
end;
end;

Resources