Introduction

Cet article utilise les notions de thread et d'accès aux données et suppose que le lecteur les connaît déjà ainsi que leur implémentation dans Delphi : classe TThread, BDE... Si vous avez besoin d'un rappel sur les threads, je vous recommande la lecture de cet article Processus et Thread avec Delphi , quant aux différents accès aux données, ils sont présentés ici : Delphi : Base de données et fichiers . Nous allons voir les principes généraux ainsi que leur application pour le BDE (composant TQuery). Bien qu'étant en fin de vie, le BDE est connu par la plupart des développeurs et disponible sur toutes les installations Delphi, il est donc bien adapté pour illustrer cet article.

1. Définition

Quel que soit le framework utilisé pour exécuter une requête (BDE, ADO...), la requête sera exécutée dans une classe dérivée de TDataSet par la méthode Open (ou la mise à true de la propriété Active, ce qui revient au même). On utilisera parfois ExecSQL, pour les requêtes autres que SELECT (INSERT, UPDATE, CREATE...). Mais que l'on utilise Open ou ExecSQL, dans les deux cas, l'appel à la méthode est bloquant, c'est-à-dire qu'il faut attendre la fin de l'exécution de la requête sur le SGBDR pour continuer l'exécution du programme. Dans certains cas, cela peut être très long. L'objectif quand on exécute une requête dans un thread est donc de récupérer la main immédiatement, sans attendre la fin du Open.

2. Quand exécuter une requête dans un thread ?

On utilisera cette technique pour des traitements :

  • ne nécessitant pas l'intervention de l'utilisateur : il pourrait être surpris de voir surgir tout à coup une boîte de dialogue alors qu'il n'a rien demandé et qu'il est en train de faire autre chose.
  • non critiques pour l'utilisation de l'application : il est inutile de rendre la main à l'utilisateur si l'exécution de la requête conditionne l'utilisation de l'application.

Par exemple, si votre application propose une extraction de données et sa sauvegarde dans un fichier, l'utilisation d'un thread est appropriée : l'utilisateur saisit éventuellement quelques informations (critères de recherche, nom du fichier...), clique sur un bouton pour lancer l'extraction (dans un thread) et peut continuer à utiliser son application. Dans ce cas, le thread ne fera pas que le open, il s'occupera également de parcourir tout le TDataSet et de créer le fichier (voir l'exemple en fin d'article). On utilisera également un thread pour exécuter des requêtes à intervalle régulier, déclenchées par un timer : par exemple si l'application implémente un mécanisme de messagerie, on peut aller vérifier régulièrement si de nouveaux messages sont arrivés. Comme il ne faut pas que cette vérification bloque l'application, on exécute la requête dans un thread.

3. Principes généraux

Il faut créer une classe héritant de TThread et respecter les contraintes de programmation inhérentes aux threads, notamment :

  • L'action à threader (Open) doit se trouver dans la méthode Execute. Il ne faut pas que le Open engendre des appels à la VCL, il faut donc :
    • Détacher le TDataSet du TDataSource (sinon les contrôles attachés seront mis à jour)
    • Renseigner les paramètres de connexion (login/mot de passe) avant l'exécution de la requête et empêcher l'affichage de la boite de dialogue qui les demande (LoginPrompt à false)
  • Les éventuels appels à la VCL (écriture dans un contrôle par exemple) doivent être faits dans des méthodes appelées par Synchronize

Il faut également respecter les contraintes liées au framework d'accès aux données. Dans le cas du BDE, la requête threadée doit utiliser une session (TSession) qui lui est réservée.

4. Mise en oeuvre

Nous allons créer une classe qui ouvre un TDataSet dans un thread. Elle sera utilisable avec n'importe quel descendant de TDataSet, sous réserve qu'il soit utilisé correctement (dans le cas d'un TQuery, il devra avoir son propre TSession). La classe s'appelle TDataSetThread et comporte les attributs suivants :

Nom Type Description
FDataSet TDataSet DataSet représentant la requête à exécuter dans le thread.
FDataSource TDataSource DataSource auquel était attaché FDataSet avant l'exécution de la requête. La liaison entre les deux est rétablie en fin de traitement.
FMessages TStrings En cas d'erreur, on écrit le message d'erreur dans ce TStrings.
FMessage String Propriété interne à la classe utilisée pour écrire dans FMessages.

Constructeurs :

Nom Description
Create (1) Créé à partir d'un TDataSet, dans ce cas la propriété FDataSource est ignorée.
Create (2) Créé à partir d'un TDataSource. Dans ce cas, c'est le TDataSet attaché au TDataSource qui est utilisé par le thread. L'intérêt de cette solution est que le thread détache puis rattache le TDataSet au TDataSource.

Si le TDataSet est attaché à des contrôles, on utilisera le second constructeur.

Méthodes :

Nom Description
initialisation Méthode utilisée en interne pour initialiser les propriétés de l'objet.
EcrireMessage Méthode qui ajoute FMessage à FMessages. On utilise une méthode afin de synchroniser cette opération (EcrireMessage sera donc appelée par Synchronize).
FinTraitement Méthode appelée lorsque le traitement est terminé afin de rétablir la liaison entre FDataSource et FDataSet. Là aussi l'appel est synchronisé.
Execute Override de la méthode de TThread. Execute effectue FDataSet.Open

4.1. Déclaration de la classe

Déclaration de la classe TDataSetThread
Sélectionnez

Uses Classes, DB;

TDataSetThread = class(TThread)
protected
  FDataSet : TDataSet;
  FDataSource : TDataSource;
  FMessages : TStrings;
  FMessage : string;
  procedure initialisation(pDataSet : TDataSet; pDataSource : TDataSource; pMessages : TStrings);
  procedure EcrireMessage;
  procedure FinTraitement;
  procedure Execute; override;
public
  constructor Create(pDataSet : TDataSet; pMessages : TStrings = nil); overload;
  constructor Create(pDataSource : TDataSource; pMessages : TStrings = nil); overload;
end;

4.2. Les constructeurs

Les constructeurs appellent la méthode d'initialisation et démarrent le thread.

Constructeurs et initialisation
Sélectionnez

Uses SysUtils;

procedure TDataSetThread.initialisation(pDataSet: TDataSet; pDataSource: TDataSource; pMessages: TStrings);
begin
  FDataSource := pDataSource;
  FDataSet := pDataSet;
  if assigned(FDataSource) then
    FDataSource.DataSet := nil; // On débranche le dataset du TDataSource
  FMessages := pMessages;
  FreeOnTerminate := True; // Le thread s'autodétruit en fin de traitement
end;
  
constructor TDataSetThread.Create(pDataSource: TDataSource; pMessages: TStrings);
begin
  inherited Create(true); // Le thread est créé suspendu
  initialisation(pDataSource.DataSet, pDataSource, pMessages);
end;
  
constructor TDataSetThread.Create(pDataSet: TDataSet; pMessages: TStrings);
begin
  inherited Create(true); // Le thread est créé suspendu
  initialisation(pDataSet, nil, pMessages);
end;

4.3. Les méthodes

Execute lance la requête. En cas d'erreur, on écrit le message dans FMessages. Remarquez l'utilisation de Synchronize dès qu'on accède à des objets susceptibles d'être accédés par le thread principal de l'application.

Méthode Execute
Sélectionnez

procedure TDataSetThread.Execute;
begin
  inherited;
  try
    FDataSet.Open;
  except
    on e: Exception do
    begin
      FMessage := e.Message;
      Synchronize(ecrireMessage);
    end;
  end;
  Synchronize(FinTraitement);
end;

Méthodes internes à la classe

Autres méthodes
Sélectionnez

procedure TDataSetThread.EcrireMessage;
begin
  // Ecriture du message dans la liste des messages
  if assigned(FMessages) then
    FMessages.Add(FMessage);
end;

procedure TDataSetThread.FinTraitement;
begin
  // On rebranche le DataSet sur le DataSource
  if assigned(FDataSource) then
    FDataSource.DataSet := FDataSet;
end;

5. Exemple d'utilisation

5.1. Exemple simple

Posez simplement les composants suivants sur une fenêtre TForm1, sans toucher à leurs propriétés, à l'exception du click sur Button1 :

Composant Rôle Evénement
Memo1 Texte de la requête  
Memo2 Messages d'erreur  
Combobox1 Liste des alias du BDE  
Edit1 Login de connexion à la base  
Edit2 Mot de passe de connexion à la base  
Query1 Requête à exécuter dans un thread  
Session1 Session de la requête  
Database1 Database de la requête  
DBGrid1 Grille pour afficher le résultat de la requête  
Datasource1 DataSource de Grid1, relié à Query1  
Button1 Bouton qui lance la requête OnClick
Initialisation des propriétés dans le create de la fenêtre
Sélectionnez

procedure TForm1.FormCreate(Sender: TObject);
begin
  // Remplissage de la liste des alias du BDE
  with TSession.Create(nil) do
  try
    SessionName := 'recupAlias';
    GetAliasNames(ComboBox1.Items);
    ComboBox1.ItemIndex := 0;
  finally
    Free;
  end;
  DataSource1.DataSet := Query1;
  DBGrid1.DataSource := DataSource1;
  Session1.SessionName := 'sessionQuery1';
  Database1.SessionName := Session1.SessionName;
  Database1.DatabaseName := 'databaseQuery1';
  Database1.LoginPrompt := false;
  Query1.SessionName := Session1.SessionName;
  Query1.DatabaseName := Database1.DatabaseName;
end;
Exécution de la requête lors du click sur le bouton
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
begin
  Database1.Close;
  Database1.AliasName := ComboBox1.Text;
  Database1.Params.Values['USER NAME'] := Edit1.Text;
  Database1.Params.Values['PASSWORD'] := Edit2.Text;
  Query1.SQL.Assign(Memo1.Lines);
  // Lancement de la requête dans un Thread :
  with TDataSetThread.Create(DataSource1,Memo2.Lines) do
  begin
    Resume; // Démarre le thread
  end;
end;

5.2. Ajout de l'événement OnTerminate

Cet événement de la classe TThread est déclenché lorsque le thread est terminé, après Execute mais avant la destruction du thread. OnTerminate est appelé dans un Synchronize, ce qui permet d'utiliser la VCL sans contrainte. Ajoutons à la fenêtre de l'exemple précédent un TLabel nommé Label1 et une procédure ThreadOnTerminate :

Modification de TForm1
Sélectionnez

TForm1 = class(TForm)
...
private
  procedure ThreadOnTerminate(Sender: TObject);
...
end;

procedure TForm1.ThreadOnTerminate(Sender: TObject);
begin
  Label1.Caption := 'Requête terminée';
end;

Il faut également brancher le thread sur le OnTerminate, ce qui se fait au moment de la création, avant de lancer la requête :

Modification de Button1.Click
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
begin
  Database1.Close;
  Database1.AliasName := ComboBox1.Text;
  Database1.Params.Values['USER NAME'] := Edit1.Text;
  Database1.Params.Values['PASSWORD'] := Edit2.Text;
  Query1.SQL.Assign(Memo1.Lines);
  Label1.Caption := 'Requête en cours...';
  // Lancement de la requête dans un Thread :
  with TDataSetThread.Create(DataSource1,Memo2.Lines) do
  begin
    OnTerminate := ThreadOnTerminate;
    Resume; // Démarre le thread
  end;
end;

6. Autre exemple : l'export vers un fichier texte

L'export d'un DataSet vers un fichier correspond tout à fait à une action threadable, car elle peut prendre beaucoup de temps et l'utilisateur n'a pas à intervenir. Nous pouvons réutiliser la classe TDataSetThread en créant une sous-classe dont nous allons redéfinir les constructeurs (afin de prendre en compte le nom du fichier à créer) et Execute.

6.1. Déclaration de la classe

Déclaration de la classe TDataSetExportThread
Sélectionnez

TDataSetExportThread = class(TDataSetThread)
private
  FNomFichier : string;
protected
  procedure Execute; override;
public
  constructor Create(pDataSet : TDataSet; pNomFichier : string; pMessages : TStrings = nil); overload;
  constructor Create(pDataSource : TDataSource; pNomFichier : string; pMessages : TStrings = nil); overload;
  property NomFichier : string read FNomFichier write FNomFichier;
end;

6.2. Les constructeurs

Les constructeurs
Sélectionnez

constructor TDataSetExportThread.Create(pDataSet: TDataSet; pNomFichier: string; pMessages: TStrings);
begin
  inherited Create(pDataSet, pMessages);
  FNomFichier := pNomFichier;
end;

constructor TDataSetExportThread.Create(pDataSource: TDataSource; pNomFichier: string; pMessages: TStrings);
begin
  inherited Create(pDataSource, pMessages);
  FNomFichier := pNomFichier;
end;

6.3. La méthode Execute

La méthode Execute
Sélectionnez

procedure TDataSetExportThread.Execute;
var
  f : TextFile;
  i : integer;
begin
  try
    if not FDataSet.Active then
      FDataSet.Open
    else
      FDataSet.First;
    Assign(f, NomFichier);
    rewrite(f);
    // Noms des zones
    for i := 0 to FDataSet.FieldCount - 1 do
      if (i = 0) then
        write(f, FDataSet.Fields[i].FieldName)
      else
        write(f, ';' + FDataSet.Fields[i].FieldName);
    writeln(f,'');
    // Le fait de tester Terminated permet d'arrêter l'export
    // en appelant Terminate sur le thread (via un bouton Annuler par exemple)
    while not (FDataSet.Eof) and (not Terminated) do
    begin
      for i := 0 to FDataSet.FieldCount - 1 do
        if (i = 0) then
          write(f, FDataSet.Fields[i].Text)
        else
          write(f, ';' + FDataSet.Fields[i].Text);
      writeln(f,'');
      FDataSet.Next; // (*) voir astuce ci-dessous
    end;
    CloseFile(f);
  except
    on e: Exception do
    begin
      FMessage := e.Message;
      Synchronize(ecrireMessage);
    end;
  end;
  Synchronize(FinTraitement);
end;

(*) Astuce : Le bouton Annuler est utile lorsque l'export est long. Si vous n'avez pas d'export suffisamment long pour tester la classe, il suffit de mettre en commentaire la ligne FDataSet.Next dans la méthode Execute de TDataSetExportThread. La boucle sera alors sans fin, à moins de provoquer le Terminate du thread (dans notre exemple, en cliquant sur Annuler). Attention, si vous ne cliquez pas sur Annuler, le thread va créer un fichier énorme composé uniquement du premier enregistrement, avec les conséquences qui en découlent !

6.4. Intégration dans l'application

L'intégration dans notre application exemple est très simple : on ajoute un bouton pour exporter (Button2), un bouton pour annuler l'export (Button3) si celui-ci est trop long et un TEdit (Edit3) pour avoir le nom du fichier à créer. On ajoute également un événement OnTerminate afin d'ouvrir automatiquement le fichier créé :

Composant Rôle Evénement
Button2 Bouton qui lance l'export OnClick
Button3 Bouton qui annule l'export OnClick
Edit3 Nom du fichier à exporter  
Déclaration dans TForm1
Sélectionnez

TForm1 = class(TForm)
...
private
  currentThread : TThread; // Thread à terminer si click sur Annuler
  procedure ThreadExportOnTerminate(Sender: TObject);
...
end ;
Click sur le bouton Exporter
Sélectionnez

procedure TForm1.Button2Click(Sender: TObject);
begin
  // Bouton Exporter
  Label1.Caption := 'Export en cours...';
  // on garde un trace du thread afin de pouvoir l'annuler si besoin
  currentThread := TDataSetExportThread.Create(DataSource1,Edit3.Text,Memo2.Lines);
  with TDataSetExportThread(currentThread) do
  begin
    OnTerminate := ThreadExportOnTerminate;
    Resume;
  end;
end;
Click sur le bouton Annuler
Sélectionnez

procedure TForm1.Button3Click(Sender: TObject);
begin
  // Bouton Annuler
  if assigned(currentThread) then
    currentThread.Terminate;
end;
Evénement OnTerminate de l'export
Sélectionnez

procedure TForm1.ThreadExportOnTerminate(Sender: TObject);
begin
  Label1.Caption := 'Traitement terminé';
  currentThread := nil;
  // Ouverture du fichier créé
  ShellExecute(0,'open',PChar((sender as TDataSetExportThread).NomFichier),nil,nil,0);
end;

7. Téléchargement

Télécharger l'exemple complet réalisé avec Delphi 6 (7 Ko)
L'exemple reprend le code vu dans l'article dans une Frame elle-même insérée dans un onglet de PageControl ce qui permet d'exécuter plusieurs requêtes en parallèle. Les onglets sont créés dynamiquement en cliquant sur "Nouveau".