Using TIdHTTPServer thread safe in D2005 - indy

This question has been asked very often and I've spent hours reading, trying, testing with no result.
I guess it has to do with my older 2005 version.
Below is the code I tried after reading a post in the Embarcadero forum answered by Remy Lebeau:
Thread: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer
procedure TMainForm.IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
Msg : String;
begin
if ARequestInfo.QueryParams <> '' then
begin
Msg := DateTimeToStr(Now) + ': ReqParam "' + ARequestInfo.QueryParams + '"';
TThread.Queue(nil,
procedure
begin
Memo1.Lines.Add(Msg);
end
);
AResponseInfo.ContentText := '<HTML><BODY>Query Params found.</BODY></HTML>';
end
else
begin
AResponseInfo.ContentText := '<HTML><BODY>Error: No Query Params.</BODY></HTML>';
Msg := DateTimeToStr(Now) + ': Error: No Query Params';
TThread.Queue(nil,
procedure
begin
Memo1.Lines.Add(Msg);
end
);
end;
end;
What I'm aiming for is accessing a memo or log file entry in a thread safe manner. Somehow using TThread.Synchronize() or TThread.Queue() doesn’t compile.
When adding the TThread.Queue() line as suggested by Remy, the error I get is:
E2029 Expression expected but procedure found
Does somebody have an alternative that I can use in Delphi 2005?
Edit: this is what I see from code completion:

Related

Error: "Cannot open file "20210609.log". The process cannot access the file because it is being used by another process"

I've got some third party code that writes to a log file one line at a time using the code below:
procedure TLog.WriteToLog(Entry: ansistring);
var
strFile: string;
fStream: TFileStream;
strDT: ansistring;
begin
if ((strLogDirectory<>'') and (strFileRoot<>'')) then
begin
if not(DirectoryExists(strLogDirectory)) then
ForceDirectories(strLogDirectory);
strFile:=strLogDirectory + '\' + strFileRoot + '-' + strFilename;
if FileExists(strFile) then
fStream:=TFileStream.Create(strFile, fmOpenReadWrite)
else
fStream:=TFileStream.Create(strFile, fmCreate);
fStream.Seek(0, soEnd);
if blnUseTimeStamp then
strDT:=formatdatetime(strDateFmt + ' hh:mm:ss', Now) + ' ' + Entry + chr(13) + chr(10)
else
strDT:=Entry + chr(13) + chr(10);
fStream.WriteBuffer(strDT[1], length(strDT));
FreeandNil(fStream);
end;
end;
This has previously been working fine at a client site but in the last few weeks it is now getting the error in the title.
There is no other process that should have the file open. I suspect it is Anti-Virus but the client claims they have disabled the AntiV and they still get the error.
The error ONLY seems to occur when the code is in a loop and writing lines fast.
WHAT I WANT TO KNOW:
Assuming it is not the Anti-Virus (or similar) causing the problem, could it be due to the operating system not clearing a flag (or something similar) before the next time it tries to write to the file?
WHAT I WANT TO KNOW: Assuming it is not the Anti-Virus (or similar) causing the problem, could it be due to the operating system not clearing a flag (or something similar) before the next time it tries to write to the file?
No, this is not a speed issue, or a caching issue. It is a sharing violation, which means there MUST be another open handle to the same file, where that handle has sharing rights assigned (or lack of) which are incompatible with the rights being requested by this code.
For example, if that other handle is not sharing read+write access, then this code will fail to open the file when creating the TFileStream with fmOpenReadWrite. If any handle is open to the file, this code will fail when creating the TFileStream with fmCreate, as that requests Exclusive access to the file by default.
I would suggest something more like this instead:
procedure TLog.WriteToLog(Entry: AnsiString);
var
strFile: string;
fStream: TFileStream;
strDT: AnsiString;
fMode: Word;
begin
if (strLogDirectory <> '') and (strFileRoot <> '') then
begin
ForceDirectories(strLogDirectory);
strFile := IncludeTrailingPathDelimiter(strLogDirectory) + strFileRoot + '-' + strFilename;
fMode := fmOpenReadWrite or fmShareDenyWrite;
if not FileExists(strFile) then fMode := fMode or fmCreate;
fStream := TFileStream.Create(strFile, fMode);
try
fStream.Seek(0, soEnd);
if blnUseTimeStamp then
strDT := FormatDateTime(strDateFmt + ' hh:mm:ss', Now) + ' ' + Entry + sLineBreak
else
strDT := Entry + sLineBreak;
fStream.WriteBuffer(strDT[1], Length(strDT));
finally
fStream.Free;
end;
end;
end;
However, do note that using FileExists() introduces a TOCTOU race condition. The file might be deleted/created by someone else after the existence is checked and before the file is opened/created. Best to let the OS handle this for you.
At least on Windows, you can use CreateFile() directly with the OPEN_ALWAYS flag (TFileStream only ever uses CREATE_ALWAYS, CREATE_NEW, or OPEN_EXISTING), and then assign the resulting THandle to a THandleStream, eg:
procedure TLog.WriteToLog(Entry: AnsiString);
var
strFile: string;
hFile: THandle;
fStream: THandleStream;
strDT: AnsiString;
begin
if (strLogDirectory <> '') and (strFileRoot <> '') then
begin
ForceDirectories(strLogDirectory);
strFile := IncludeTrailingPathDelimiter(strLogDirectory) + strFileRoot + '-' + strFilename;
hFile := CreateFile(PChar(strFile), GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_ALWAYS, 0, 0);
if hFile = INVALID_HANDLE_VALUE then RaiseLastOSError;
try
fStream := THandleStream.Create(hFile);
try
fStream.Seek(0, soEnd);
if blnUseTimeStamp then
strDT := FormatDateTime(strDateFmt + ' hh:mm:ss', Now) + ' ' + Entry + sLineBreak
else
strDT := Entry + sLineBreak;
fStream.WriteBuffer(strDT[1], Length(strDT));
finally
fStream.Free;
end;
finally
CloseHandle(hFile);
end;
end;
end;
In any case, you can use a tool like SysInternals Process Explorer to verify if there is another handle open to the file, and which process it belongs to. If the offending handle in question is being closed before you can see it in PE, then use a tool like SysInternals Process Monitor to log access to the file in real-time and check for overlapping attempts to open the file.

Runtime error 104 Pascal

Im very new to using files and im really struggling to fix this any help would be great.
It seems that the error is coming from my read array function but not entirely
sure i am also not to sure what the 104 error really means
thanks in advance
program ReadFromFile;
type
lineArray = array [0..19] of String;
procedure PrintArray(lines: lineArray);
var
i: Integer;
begin
for i:=0 to High(lines) do
begin
WriteLn('Text is: ', lines[i], ' Line number is: ', i);
end;
end;
function ReadArray(var myFile: TextFile):lineArray;
var
count : Integer;
lines : lineArray;
i: Integer;
begin
ReadLn(myFile, count);
for i := 0 to count do
begin
ReadLn(myFile, lines[i]);
end;
result := lines;
end;
procedure Main();
var
myFile: TextFile;
line: lineArray;
begin
AssignFile(myFile, 'mytestfile.dat');
ReWrite(myFile);
line:=ReadArray(myFile);
Close(myFile);
AssignFile(myFile, 'mytestfile.dat');
Reset(myFile);
PrintArray(line);
Close(myFile);
end;
begin
Main();
end.
You don't know what that error means. Neither do I off the top of my head. So, let's look it up in the documentation and find out. Websearch takes us here: https://www.freepascal.org/docs-html/user/userap4.html
File not open for input
Reported by Read, BlockRead, Eof, Eoln, SeekEof or SeekEoln if the file is not opened with Reset.
You have your calls to open the file the wrong way round. Call Reset to open for reading, Rewrite to open for writing.
Notes:
Looping from 0 to count will perform count + 1 iterations. I'd expect to see you looping from 0 to count - 1.
You don't check whether your array is long enough. You therefore run the risk of a buffer overrun. A dynamic array would avoid this.
It's not clear why you open the file for a second time when you print the contents to the console.
You could have looked up the error code yourself. Please take the hint to do web search the next time you encounter an error like this.

Strange behavior with TThread.CreateAnonymousThread

I was unable to follow how it is working.
A very simple example first, to try explain my situation better.
This code is inside a new Form Form1 create in a new project. Where mmo1 is a Memo component.
TOb = class
Name : String;
constructor Create(Name : String);
procedure Go();
end;
procedure TOb.Go;
begin
Form1.mmo1.Lines.Add(Name);
end;
Then I have a button with this event:
procedure TForm1.btn4Click(Sender: TObject);
var
Index : Integer;
begin
mmo1.Lines.Clear;
for Index := 1 to 3 do
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(Index)).Go).Start;
end;
And my output on the memo is:
Thread 4
Thread 4
Thread 4
I really don't got it.
First question: Why the "Name" output is: Thread 4? Is a For loop from 1 to 3. At least should be 1 or 3
Second: Why it only execute the last thread "Thread 4", instead of 3 times in sequence "Thread 1", "Thread 2", "Thread 3"?
Why I'm asking this? I have an object that has already a process working fine. But now I found me in a situation that I need a List of this object to be processed. Sure work fine process one by one, but in my case they are independent one of other so I thought "hm, lets put them in threads, so it will run faster".
To avoid modifying the object to extend TThread and overriding Execute I look up on how to execute a thread with a procedure instead of an object that inherits from TThread and found the Anonymous Thread. Works really great with one object, but when I tried loop through my object list, strange behaviors happens.
This has the same effect.
for Index := 1 to 3 do
TThread.CreateAnonymousThread(
procedure
var
Ob : TOb;
begin
OB := TOb.Create('Thread ' + IntToStr(Index));
OB.Go;
end
).Start;
Sure I'm not clean the object, this was just some tests that I was running.
Any Ideas? Or in this case I will need to inherits from TThread and override the Execute methode?
The funny thing is that THIS runs just fine.
mmo1.Lines.Clear;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(1)).Go).Start;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(2)).Go).Start;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(3)).Go).Start;
Output:
Thread 1
Thread 2
Thread 3
Works really great with one object, but when I tried loop through my object list, strange behaviors happens.
You are likely not taking into account how anonymous procedures bind to variables. In particular:
Note that variable capture captures variables--not values. If a variable's value changes after being captured by constructing an anonymous method, the value of the variable the anonymous method captured changes too, because they are the same variable with the same storage. Captured variables are stored on the heap, not the stack.
For example, if you do something like this:
var
Index: Integer;
begin
for Index := 0 to ObjList.Count-1 do
TThread.CreateAnonymousThread(TOb(ObjList[Index]).Go).Start;
end;
You will actually cause an EListError exception in the threads (I least when I tested it - I don't know why it happens. Verified by assigning an OnTerminate handler to the threads before calling Start(), and then having that handler check the TThread(Sender).FatalException property).
If you do this instead:
var
Index: Integer;
Ob: TOb;
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
TThread.CreateAnonymousThread(Ob.Go).Start;
end;
end;
The threads won't crash anymore, but they are likely to operate on the same TOb object, because CreateAnonymousThread() is taking a reference to the TOb.Go() method itself, and then your loop is modifying that reference's Self pointer on each iteration. I suspect the compiler is likely generating code similar to this:
var
Index: Integer;
Ob: TOb;
Proc: TProc; // <-- silently added
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
Proc := Ob.Go; // <-- silently added
TThread.CreateAnonymousThread(Proc).Start;
end;
end;
If you do this instead, it will have a similar issue:
procedure StartThread(Proc: TProc);
begin
TThread.CreateAnonymousThread(Proc).Start;
end;
...
var
Index: Integer;
Ob: TOb;
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
StartThread(Ob.Go);
end;
end;
Probably because the compiler generates code similar to this:
procedure StartThread(Proc: TProc);
begin
TThread.CreateAnonymousThread(Proc).Start;
end;
...
var
Index: Integer;
Ob: TOb;
Proc: TProc; // <--
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
Proc := Ob.Go; // <--
StartThread(Proc);
end;
end;
This will work fine, though:
procedure StartThread(Ob: TOb);
begin
TThread.CreateAnonymousThread(Ob.Go).Start;
end;
...
var
Index: Integer;
Ob: TOb;
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
StartThread(Ob);
// or just: StartThread(TOb(ObjList[Index]));
end;
end;
By moving the call to CreateAnonymousThread() into a separate procedure that isolates the actual reference to TOb.Go() into a local variable, you remove any chance of conflict in capturing the reference for multiple objects.
Anonymous procedures are funny that way. You have to be careful with how they capture variables.
After reading a the article that Remy Lebeau post on the comments, I found this solution.
changing the main object by add one more procedure that make the call.
Change the loop instead of creating the anonymous thread at the main loop, it is created inside the object.
TOb = class
Name : String;
constructor Create(Name : String);
procedure Process();
procedure DoWork();
end;
procedure TOb.Process;
begin
TThread.CreateAnonymousThread(DoWork).Start;
end;
procedure TOb.DoWork;
var
List : TStringList;
begin
List := TStringList.Create;
List.Add('I am ' + Name);
List.Add(DateTimeToStr(Now));
List.SaveToFile('D:\file_' + Name + '.txt');
List.Free;
end;
And the loop:
List := TObjectList<TOb>.Create();
List.Add(TOb.Create('Thread_A'));
List.Add(TOb.Create('Thread_B'));
List.Add(TOb.Create('Thread_C'));
List.Add(TOb.Create('Thread_D'));
for Obj in List do
//TThread.CreateAnonymousThread(Obj.Go).Start;
Obj.Process;
Thats resolves the problem with just a minimum change on the Main Object.
This about race condition. When you increased to max value to 100, you will see different values. Threading not guarantee when Thread starts or ends.
You can try this code block.
for I := 1 to 100 do
begin
TThread.CreateAnonymousThread(
procedure
var
Msg : string;
begin
try
Msg := 'This' + I.ToString;
MessageDlg(Msg,mtCustom,
[mbYes,mbAll,mbCancel], 0);
Except
on E: Exception do
End;
end
).Start;
end;
If you want a guarantee to write 1 to 4, you should instantiate every value before send to Thread.
for I := 1 to 100 do
begin
TThread.CreateAnonymousThread(
procedure
var
Msg : string;
begin
var instanceValue := I;
try
Msg := 'This' + instanceValue.ToString;
MessageDlg(Msg,mtCustom,
[mbYes,mbAll,mbCancel], 0);
Except
on E: Exception do
End;
end
).Start;
end;

Capture all commands output

I want to capture cmd.exe output and show it in a diffrent gui I am making. I want to make a command interpreter with extended functionality. The dir command works flawless, the problem ocurrs when I try to execute another process like ipconfig.
I don't see ipconfig output. Is there a workarround on that ?!
I use TProcess component from Lazarus (FreePascal)
proc := TProcess.Create(nil);
proc.Options:= [poUsePipes, poNoConsole];
proc.ShowWindow:= swoHIDE;
proc.Executable:= 'cmd';
Reading output thread:
if (Length(cmd) > 0) then
begin
cmd := cmd + #13#10;
proc.Input.Write(cmd[1], Length(cmd)); // here I write command from user
strikes := 0;
end
else
if proc.Output.NumBytesAvailable > 0 then
begin
while proc.Output.NumBytesAvailable > 0 do
begin
FillChar(buf, sizeof(buf), #0);
proc.Output.Read(buf, sizeof(buf) - 1);
data := data + buf;
end;
// data gets echoed to user
It works fine for me (I use FPC 3.1.1 & Lazarus 1.5 but I hope it does not matter):
proc.Options:= [poUsePipes];
proc.ShowWindow:= swoHIDE;
proc.Executable:= 'cmd';
...
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
cmd: String;
begin
if Key = #13 then
begin
Key := #0;
if not proc.Active then
proc.Active := True;
cmd := Edit1.Text + LineEnding;
proc.Input.Write(cmd[1], Length(cmd));
end;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
buf: array[0..65535] of Char;
begin
if proc.Output.NumBytesAvailable > 0 then
begin
while proc.Output.NumBytesAvailable > 0 do
begin
FillChar(buf, sizeof(buf), #0);
proc.Output.Read(buf, sizeof(buf) - 1);
Memo1.Lines.Add(buf);
end;
end;
end;
I guess that you just does not catch process output properly.
Good luck.
PS: If you need to create some windows console-like application, the best way, I think, is using Windows console API instead of cross-platform Lazarus components.
PPS: To emulate console look and behavior with Lazarus use CmdLine component.
In general it is smart to first check if the short examples don't solve the problem:
e.g.
uses process;
var s : ansistring;
begin
runcommand('ipconfig',['/all'],s);
writeln(s);
end.
works fine, and saves a whole lot of trouble. (FPC 2.6.2+ though)

BDE vs ADO in Delphi

Please note the Edit below for a lot more information, and a possible solution
We recently modified a large Delphi application to use ADO connections and queries instead of BDE connections and queries. Since that change, performance has become terrible.
I've profiled the application and the bottleneck seems to be at the actual call to TADOQuery.Open. In other words, there isn't much I can do from a code standpoint to improve this, other than restructuring the application to actually use the database less.
Does anyone have suggestions about how to improve the performance of an ADO-connected Delphi application? I've tried both of the suggestions given here, with virtually no impact.
To give an idea of the performance difference, I benchmarked the same large operation:
Under BDE: 11 seconds
Under ADO: 73 seconds
Under ADO after the changes referenced by that article: 72 seconds
We are using an Oracle back-end in a client-server environment. Local machines each maintain a separate connection to the database.
For the record, the connection string looks like this:
const
c_ADOConnString = 'Provider=OraOLEDB.Oracle.1;Persist Security Info=True;' +
'Extended Properties="plsqlrset=1";' +
'Data Source=DATABASE.DOMAIN.COM;OPTION=35;' +
'User ID=******;Password=*******';
To answer the questions posed by zendar:
I'm using Delphi 2007 on Windows Vista and XP.
The back end is an Oracle 10g database.
As indicated by the connection string, we are using the OraOLEDB driver.
The MDAC version on my benchmark machine is 6.0.
Edit:
Under the BDE, we had a lot of code that looked like this:
procedure MyBDEProc;
var
qry: TQuery;
begin
//fast under BDE, but slow under ADO!!
qry := TQuery.Create(Self);
try
with qry do begin
Database := g_Database;
Sql.Clear;
Sql.Add('SELECT');
Sql.Add(' FIELD1');
Sql.Add(' ,FIELD2');
Sql.Add(' ,FIELD3');
Sql.Add('FROM');
Sql.Add(' TABLE1');
Sql.Add('WHERE SOME_FIELD = SOME_CONDITION');
Open;
//do something
Close;
end; //with
finally
FreeAndNil(qry);
end; //try-finally
end; //proc
But we found that the call to Sql.Add is actually very expensive under ADO, because the QueryChanged event is fired every time you change the CommandText. So replacing the above with this was MUCH faster:
procedure MyADOProc;
var
qry: TADOQuery;
begin
//fast(er) under ADO
qry := TADOQuery.Create(Self);
try
with qry do begin
Connection := g_Connection;
Sql.Text := ' SELECT ';
+ ' FIELD1 '
+ ' ,FIELD2 '
+ ' ,FIELD3 '
+ ' FROM '
+ ' TABLE1 '
+ ' WHERE SOME_FIELD = SOME_CONDITION ';
Open;
//do something
Close;
end; //with
finally
FreeAndNil(qry);
end; //try-finally
end; //proc
Better yet, you can copy TADOQuery out of ADODB.pas, rename it under a new name, and rip out the QueryChanged event, which as far as I can tell, is not doing anything useful at all. Then use your new, modified version of TADOQuery, instead of the native one.
type
TADOQueryTurbo = class(TCustomADODataSet)
private
//
protected
procedure QueryChanged(Sender: TObject);
public
FSQL: TWideStrings;
FRowsAffected: Integer;
function GetSQL: TWideStrings;
procedure SetSQL(const Value: TWideStrings);
procedure Open;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function ExecSQL: Integer; {for TQuery compatibility}
property RowsAffected: Integer read FRowsAffected;
published
property CommandTimeout;
property DataSource;
property EnableBCD;
property ParamCheck;
property Parameters;
property Prepared;
property SQL: TWideStrings read FSQL write SetSQL;
end;
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
constructor TADOQueryTurbo.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FSQL := TWideStringList.Create;
TWideStringList(FSQL).OnChange := QueryChanged;
Command.CommandText := 'SQL'; { Do not localize }
end;
destructor TADOQueryTurbo.Destroy;
begin
inherited;
inherited Destroy;
FreeAndNil(FSQL);
end;
function TADOQueryTurbo.ExecSQL: Integer;
begin
CommandText := FSQL.Text;
inherited;
end;
function TADOQueryTurbo.GetSQL: TWideStrings;
begin
Result := FSQL;
end;
procedure TADOQueryTurbo.Open;
begin
CommandText := FSQL.Text;
inherited Open;
end;
procedure TADOQueryTurbo.QueryChanged(Sender: TObject);
begin
// if not (csLoading in ComponentState) then
// Close;
// CommandText := FSQL.Text;
end;
procedure TADOQueryTurbo.SetSQL(const Value: TWideStrings);
begin
FSQL.Assign(Value);
CommandText := FSQL.Text;
end;
I don't know about Delphi 2007, but I did same thing with Delphi 7 and Oracle 8.
Here are things I did:
Set TAdoDataSet.CursorLocation according to query:
clUseClient if query fetches records for GUI and query is relatively "simple" - no grouping or sum
clUseServer if query have some sort of aggregation (sum, grouping, counting)
Set TAdoDataSet.CursorType according to query:
ctForwardOnly for reports where you don't need scroll back through dataset - works only with clUseServer
ctStatic for GUI. This is only mode that works with clUseClient
Set TAdoDataSet.LockType according to query:
ltReadOnly for every dataset that is not used for editing (grids, reports)
ltOptimistic when records are posted to database immediately after change (e.g. user editing data on form)
ltBatchOptimistic when you change large number of records. This is for situations where you fetch number of records, then do some processing on them and then send updates to database in batch. This works best combined with clUseClient and ctStatic.
In my experience, Microsoft OLEDB provider for Oracle worked better than Oracle OleDb provider. You should test that. Edit: Check Fabricio's comment about possible blob problems.
Replace TAdoQUery with TAdoDataSet. TAdoQuery was created for conversion of apps from BDE to ADO, but Borland/Codegear recomendation was to use TAdoDataSet
Recheck Oracle connection string to be sure that you do not have network latency. How long it lasts to connect to Oracle? How long is TnsPing?
i found the performance problems with ADOExpress years ago:
ADO vs ADOExpress time trials. Not good for ADOExpress (6/7/2005)
ADO vs ADO Express Time Trials (redux) (12/30/2007)
Note: Before ADO became a standard part of Delphi, Borland was selling it as an addon called ADOExpress. It was simply object wrappers around Microsoft's ActiveX Data Objects (ADO) COM objects.
i had tested three scenarios
using ADO directly (i.e. Microsoft's COM objects directly)
using ADOExpress (Borland's object wrappers around ADO)
specifying .DisableControls on the TADOQuery before calling Open
i discovered
use Query.DisableControls to make each call .Next 50x faster
use Query.Recordset.Fields.Items['columnName'].Value rather than Query.FieldByName('columnName') to make each value lookup 2.7x faster
using TADODataSet (verses TADOQuery) makes no difference
Loop Results Get Values
ADOExpress: 28.0s 46.6s
ADOExpress w/DisableControls: 0.5s 17.0s
ADO (direct use of interfaces): 0.2s 4.7s
Note: These values are for looping 20,881 rows, and looking up the values of 21 columns.
Baseline Bad Code:
var
qry: TADOQuery;
begin
qry := TADOQuery.Create(nil);
try
qry.SQL.Add(CommandText);
qry.Open;
while not qry.EOF do
begin
...
qry.Next;
end;
Use DisableControls to make looping 5000% faster:
var
qry: TADOQuery;
begin
qry := TADOQuery.Create(nil);
try
qry.DisableControls;
qry.SQL.Add(CommandText);
qry.Open;
while not qry.EOF do
begin
...
qry.Next;
end;
Use Fields collection to make value lookups 270% faster:
var
qry: TADOQuery;
begin
qry := TADOQuery.Create(nil);
try
qry.DisableControls;
qry.SQL.Add(CommandText);
qry.Open;
while not qry.EOF do
begin
value1 := VarAsString(qry.Recordset.Fields['FieldOne'].Value);
value2 := VarAsInt(qry.Recordset.Fields['FieldTwo'].Value);
value3 := VarAsInt64(qry.Recordset.Fields['FieldTwo'].Value);
value4 := VarAsFloat(qry.Recordset.Fields['FieldThree'].Value);
value5 := VarAsWideString(qry.Recordset.Fields['FieldFour'].Value);
...
value56 := VarAsMoney(qry.Recordset.Fields['FieldFive'].Value);
qry.Next;
end;
Since it is a common enough problem, we created a helper method to solve the issue:
class function TADOHelper.Execute(const Connection: TADOConnection;
const CommandText: WideString): TADOQuery;
var
rs: _Recordset;
query: TADOQuery;
nRecords: OleVariant;
begin
Query := TADOQuery.Create(nil);
Query.DisableControls; //speeds up Query.Next by a magnitude
Query.Connection := Connection;
Query.SQL.Text := CommandText;
try
Query.Open();
except
on E:Exception do
begin
Query.Free;
raise;
end;
end;
Result := Query;
end;
For best performance, should get a look at our Open Source direct access to Oracle.
If you are processing a lot of TQuery, without using the DB components, we have a dedicated pseudo-class to use direct OCI connection, as such:
Q := TQuery.Create(aSQLDBConnection);
try
Q.SQL.Clear; // optional
Q.SQL.Add('select * from DOMAIN.TABLE');
Q.SQL.Add(' WHERE ID_DETAIL=:detail;');
Q.ParamByName('DETAIL').AsString := '123420020100000430015';
Q.Open;
Q.First; // optional
while not Q.Eof do begin
assert(Q.FieldByName('id_detail').AsString='123420020100000430015');
Q.Next;
end;
Q.Close; // optional
finally
Q.Free;
end;
And I've added some unique access via a late-binding Variant, to write direct code as such:
procedure Test(Props: TOleDBConnectionProperties; const aName: RawUTF8);
var I: ISQLDBRows;
Customer: Variant;
begin
I := Props.Execute('select * from Domain.Customers where Name=?',[aName],#Customer);
while I.Step do
writeln(Customer.Name,' ',Customer.FirstName,' ',Customer.Address);
end;
var Props: TOleDBConnectionProperties;
begin
Props := TSQLDBOracleConnectionProperties.Create(
'TnsName','UserName','Password',CODEPAGE_US);
try
Test(Props,'Smith');
finally
Props.Free;
end;
end;
Note that all OleDB providers are buggy for handling BLOBs: Microsoft's version just do not handle them, and Oracle's version will randomly return null for 1/4 of rows...
On real database, I found out our direct OCI classes to be 2 to 5 times faster than the OleDB provider, without the need to install this provider. You can even use the Oracle Instant Client provided by Oracle which allows you to run your applications without installing the standard (huge) Oracle client or having an ORACLE_HOME. Just deliver the dll files in the same directory than your application, and it will work.

Resources