Exécuter une requête dans un thread

Bloon

Cet article présente l'exécution d'une requête dans un Thread avec Delphi

- Titre : Exécuter une requête dans un thread

- Auteur : Bloon

- Parution : 17 novembre 2004

- Licence : Copyright ® 2004 Nicolas Bruyère. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- 1. Définition

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

- 3. Principes généraux

- 4. Mise en oeuvre

- 4-1. Déclaration de la classe

- 4-2. Les constructeurs

- 4-3. Les méthodes

- 5. Exemple d'utilisation

- 5-1. Exemple simple

- 5-2. Ajout de l'événement OnTerminate

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

- 6-1. Déclaration de la classe

- 6-2. Les constructeurs

- 6-3. La méthode Execute

- 6-4. Intégration dans l'application

- 7. Téléchargement

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

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

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

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

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

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

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

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

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

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

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

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

TForm1 = class(TForm)
...
private
currentThread : TThread; // Thread à terminer si click sur Annuler
procedure ThreadExportOnTerminate(Sender: TObject);
...
end ;

Click sur le bouton Exporter

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

procedure TForm1.Button3Click(Sender: TObject);
begin
// Bouton Annuler
if assigned(currentThread) then
currentThread.Terminate;
end;

Evénement OnTerminate de l'export

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".