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▲
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.
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.
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
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 |
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
;
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 :
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 :
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▲
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▲
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▲
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 |
TForm1 = class
(TForm)
...
private
currentThread : TThread; // Thread à terminer si click sur Annuler
procedure
ThreadExportOnTerminate(Sender: TObject);
...
end
;
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
;
procedure
TForm1.Button3Click(Sender: TObject);
begin
// Bouton Annuler
if
assigned(currentThread) then
currentThread.Terminate;
end
;
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".