Exécuter une requête dans un thread
Date de publication : 17/11/2004
Par
Bloon (Retour Index) Cet article présente l'exécution d'une requête dans un Thread avec Delphi
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;
FMessages := pMessages;
FreeOnTerminate := True;
end;
constructor TDataSetThread.Create(pDataSource: TDataSource; pMessages: TStrings);
begin
inherited Create(true);
initialisation(pDataSource.DataSet, pDataSource, pMessages);
end;
constructor TDataSetThread.Create(pDataSet: TDataSet; pMessages: TStrings);
begin
inherited Create(true);
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
if assigned(FMessages) then
FMessages.Add(FMessage);
end;
procedure TDataSetThread.FinTraitement;
begin
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
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);
with TDataSetThread.Create(DataSource1,Memo2.Lines) do
begin
Resume;
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...';
with TDataSetThread.Create(DataSource1,Memo2.Lines) do
begin
OnTerminate := ThreadOnTerminate;
Resume;
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);
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,'');
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;
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;
procedure ThreadExportOnTerminate(Sender: TObject);
...
end ;
Click sur le bouton Exporter procedure TForm1.Button2Click(Sender: TObject);
begin
Label1.Caption := 'Export en cours...';
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
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;
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".
|