7

I am currently working on a multithreaded server app and I plan to use Firedac for data access. From the docs provided here: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Multithreading_(FireDAC), it seems that the same TFDConnection and/or TFDQuery should not be accessed from multiple threads at the same time (instead these objects should be created on per-thread basis).

Therefore, the example shown in the previous link, centralizes the TFDConnection and TFDQuery in a TThread object. However, in my case I don't have control on thread creation (which is managed by the server environment). I am therefore limiting the lifecycle of my TFDConnection and TFDQuery objects to the lifetime of a procedure, which can be potentially called from multiple threads:

procedure TAPMFiredacTemplate.executeSQL(sql:string);
  var
  oConn: TFDConnection;
  oQuery: TFDQuery;
  begin
  oConn := TFDConnection.Create(nil);
  oQuery := TFDQuery.Create(nil);
  try
    oConn.ConnectionDefName := self.ConnectionDefinitionName;
    oConn.Connected := True;
    oQuery.Connection := oConn;
    oQuery.Open(sql);
    while not oQuery.Eof do
    begin
      // process query
      oQuery.Next;
    end;

  finally
    if assigned(oQuery) then
    begin
      oQuery.Close;
      oQuery.Free;
    end;
    if assigned (oConn) then
    begin
      oConn.Connected := False;
      oConn.Free;
    end;

  end;

Is this approach valid? Is there a performance hit by systematically creating the TFDQuery object?

Note: in order to improve performance I am planning to use a private pooled connection definition (which is used by the TFDConnection). So from my understanding even when I free the TFDConnection, the physical connection is not destroyed but returned to the pool instead:

oParams := TStringList.Create;
oParams.Add('Database=localhost:c:\apm\databases\mydb.FDB');
oParams.Add('User_Name=xxxxx');
oParams.Add('Password=xxxxxx');
oParams.Add('Pooled=True');
FDManager.AddConnectionDef('Firebird_Pooled','FB',oParams);
FDManager.Active := True;
BigONotation
  • 4,406
  • 5
  • 43
  • 72
  • 1
    There is a performance penalty for the first approach. Continue reading to [Connection Pooling](http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Multithreading_(FireDAC)#Connection_Pooling). You can then check [the example](http://docwiki.embarcadero.com/CodeExamples/Tokyo/en/FireDAC.Pooling_Sample). – Victoria May 27 '17 at 16:44
  • I was thinking of creating a pool of connection in advance through the `FDManager` and then use those pooled connection in my procedure (see added note). So even if I create/free the `TFDConnection`, the physical connections or coming from the pool. This should improve performance. However I am not sure what I can do for optimizing `TFDQuery` – BigONotation May 27 '17 at 16:54
  • 1
    Preparing a query can be another expensive operation. If your server supports keep-alive kind of connection, create the query object and prepare the query when the client connects and destroy it when disconnects. Each request you can then process with the prepared query object (referenced by the context class). – Victoria May 27 '17 at 16:59
  • OK many thanks for the clarifications. One more point I don't fully understand: what do you mean by context class? If you put your comments in an answer I will accept it as the valid answer ;) – BigONotation May 27 '17 at 17:17
  • 1
    If you are using Indy server, you can create your own class based on `TIdContext` and store the query object reference there (in the `OnConnect` event you create it and prepare the query, in `OnExecute` you use it and in the `OnDisconnect` you destroy it). All that has meaning just if your server supports keep alive type of connection though (client connects, do more than one request and disconnects). – Victoria May 27 '17 at 17:32

1 Answers1

5

It is a valid approach for thread context execution, but it has a performance penalty in database connection establishment and query preparing with each client request (assuming you are using some Indy server).

To fix the first issue use the connection pooling (you can follow the example).

To fix the latter issue there can be a solution as well. If your server supports keep alive kind of connection, create the query object and prepare the query when the client connects and destroy it when disconnects. This prepared object you can pass to the server request processing method through the extended context class.

For example with TIdTCPServer it could be:

type
  { server context in the meaning of a "task" class }
  TMyContext = class(TIdServerContext)
  private
    FQuery: TFDQuery;
  public
    constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil); override;
    destructor Destroy; override;
    property Query: TFDQuery read FQuery;
  end;

  TForm1 = class(TForm)
    IdTCPServer1: TIdTCPServer;
    FDConnection1: TFDConnection;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure IdTCPServer1Connect(AContext: TIdContext);
    procedure IdTCPServer1Disconnect(AContext: TIdContext);
    procedure IdTCPServer1Execute(AContext: TIdContext);
    procedure IdTCPServer1Exception(AContext: TIdContext; AException: Exception);
  end;

implementation

constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil);
begin
  inherited;
  FQuery := TFDQuery.Create(nil);
end;

destructor TMyContext.Destroy;
begin
  FQuery.Free;
  inherited;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  Params: TStrings;
begin
  Params := TStringList.Create;
  try
    Params.Add('Database=localhost:C:\MyDatabase.fdb');
    Params.Add('User_Name=xxxxx');
    Params.Add('Password=xxxxx');
    Params.Add('Pooled=True');
    { add the definition to the global connection manager singleton }
    FDManager.AddConnectionDef('FirebirdPooled', 'FB', Params);
  finally
    Params.Free;
  end;

  { use the added definition and establish the connection to the DB }
  FDConnection1.ConnectionDefName := 'FirebirdPooled';
  FDConnection1.Connected := True;

  { setup the context class, add a port binding and start the TCP server }
  IdTCPServer1.ContextClass := TMyContext;
  IdTCPServer1.Bindings.Add.Port := 6000;
  IdTCPServer1.Active := True;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  { stop the TCP server and destroy all pooled physical connections }
  IdTCPServer1.Active := False;
  FDManager.CloseConnectionDef('FirebirdPooled');
end;

procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
begin
  { client just connected, assign to the context query object the pooled
    connection and a command text }
  TMyContext(AContext).Query.Connection := FDConnection1;
  TMyContext(AContext).Query.SQL.Text := 'SELECT * FROM MyTable WHERE ID=:ID';
  { preparing the query will acquire one physical connection from the pool
    as this method internally opens the connection }
  TMyContext(AContext).Query.Prepare;
end;

procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
begin
  { client just disconnected, return the physical connection to the pool }
  TMyContext(AContext).Query.Connection.Close;
end;

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  ID: Integer;
  Query: TFDQuery;
begin
  { read an integer from socket }
  ID := AContext.Connection.IOHandler.ReadInteger;
  { just a reference helper }
  Query := TMyContext(AContext).Query;

  { fill the parameter and refresh the prepared query object's dataset }
  Query.Params[0].AsInteger := ID;
  Query.Refresh;

  while not Query.Eof do
  begin
    { process the dataset somehow }
    Query.Next;
  end;

  { do not close the dataset, keep it prepared for the next possible request }
end;

procedure TForm1.IdTCPServer1Exception(AContext: TIdContext; AException: Exception);
begin
  if AException is EFDException then
  begin
    { something bad happened with the DB, this is a base FireDAC exception
      class but you can be more specific of course }
  end;
end;
Victoria
  • 7,822
  • 2
  • 21
  • 44