I. Introduction▲
Vous avez certainement entendu parler des transactions : il y a les immobilières, les mobilières et … les autres. La plupart des SGBD (systèmes de gestion de base de données) dignes de ce nom mettent en œuvre des transactions. Du sens général du mot, on retiendra qu'il y a un échange entre (au moins) deux acteurs qui se conclut par un accord. L'échange a pour but d'établir l'accord. Si l'accord est conclu, l'objet de l'échange est validé (la base est mise à jour), sinon, chacun revient aux conditions initiales (les données sont rétablies à leur valeur initiale).
Le mécanisme de transaction est d'abord employé par la base elle-même, en interne, pour maintenir sa cohérence : par exemple lorsque vous avez demandé qu'un index soit maintenu sur une colonne d'une table, à chaque mise à jour, un SGBD correctement construit :
- Met à jour la donnée ;
- Met à jour l'index.
Si un problème se pose lors de la mise à jour de l'index, la mise à jour de la donnée est annulée : ou bien toutes les opérations sont effectuées, ou bien aucune.
Ce mécanisme est intéressant bien entendu pour des opérations effectuées au niveau de l'application. Par exemple, dans un schéma volontairement simplifié, on ajoutera une ligne à la facture si l'article est retiré du stock. Supposons que l'article manque en stock, la ligne doit être retirée.
Voyons ceci en détail :
- Réservation du stock ;
- Inscription de la ligne de la facture ;
- Acceptation par le client de la ligne au vu de la quantité et du prix ;
- Sortie effective du stock.
En monoposte, pas de problèmes particuliers. En revanche, si l'on travaille en réseau, un autre opérateur peut être en train de vendre le même article. Ce qui peut donner la succession suivante
1° client |
Étape |
2° client |
---|---|---|
Réservation du stock pour le 1er client |
1 |
|
2 |
Réservation du stock pour le 2e client |
|
Inscription de la ligne de la facture |
3 |
Inscription de la ligne de la facture |
Acceptation par le client de la ligne au vu de la quantité et du prix |
4 |
Acceptation par le client de la ligne au vu de la quantité et du prix |
Sortie effective du stock : le stock est décrémenté |
5 |
|
6 |
Tentative de sortie effective du stock, mais le stock s'avère insuffisant |
Le stock a été définitivement affecté au 1er client à l'étape 5, et, à l'étape 6, le 2e client ne trouve plus ce qui était pourtant disponible à l'étape 2.
Bien sûr on aurait pu procéder à l'affectation dès l'étape 1. Mais, si le 1er client, au vu du prix, refuse la commande, la vente est alors perdue pour le client 2e puisque le stock affecté dès l'étape 1 ne lui est pas disponible à l'étape 2.
Pour éviter cela, on regroupe les séquences d'opération ainsi :
1° client |
Étape |
2° client |
---|---|---|
Début de transaction |
1 |
Début de transaction |
Réservation du stock pour le 1er client |
2 |
|
Inscription de la ligne de la facture |
3 |
|
Acceptation par le client de la ligne au vu de la quantité et du prix |
4 |
|
Sortie effective du stock : le stock est décrémenté |
5 |
|
Fin de transaction |
6 |
Fin de transaction |
Début de transaction |
7 |
Début de transaction |
8 |
Réservation du stock pour le 2e client |
|
9 |
Inscription de la ligne de la facture |
|
10 |
Acceptation par le client de la ligne au vu de la quantité et du prix |
|
11 |
Sortie effective du stock : le stock est décrémenté |
|
Fin de transaction |
12 |
Fin de transaction |
Chacun des deux groupes sera isolé au sein de ce que l'on appellera une transaction.
La transaction est visible pour tous les clients, c'est ce qu'indiquent les marques de début et de fin de transaction dans chaque colonne.
Nous remarquons que c'est au niveau de l'application que doivent être délimitées les transactions : c'est le développeur qui définit les opérations qui constituent une transaction.
Dans la pratique, on évitera de mettre dans la transaction des actions d'attente de la décision des clients.
De plus, les SGBD utilisent des méthodes plus sophistiquées que la sérialisation des transactions telle qu'elle est induite du tableau : attendre d'avoir fini le 1er client pour traiter le 2e client introduit des goulots d'étranglement très pénalisant. Mais pour la compréhension logique de ce qui se passe, la présentation ci-dessus est suffisamment simple et complète.
Dans le langage SQL, on dispose d'un ordre pour démarrer et de deux ordres pour terminer une transaction.
Pour le début de transaction, on utilise SET TRANSACTION, tandis que pour la fin de transaction, on utilise COMMIT (ou ROLLBACK) selon que l'on valide (ou invalide) les modifications de la base effectuées au cours de la transaction.
Ces ordres seront mis en œuvre directement par le programmeur ou par l'intermédiaire de composants qui le feront pour lui.
Nous allons en voir l'application dans trois cas.
II. Les transactions avec InterBaseExpress (IBX)▲
Avant de commencer à parler de nos transactions, il faut souligner que toutes les opérations effectuées avec les IBX sont d'ores et déjà inscrites dans une transaction fondamentale. Cette transaction fondamentale a pour effet de faire apparaître la base de données à chacun, comme s'il en était le seul utilisateur.
Dans ces conditions, on voit mal comment on pourrait mettre en œuvre des transactions fines, puisque tout ce que fait un utilisateur en lecture et en écriture est isolé dans la transaction fondamentale.
Eh bien cela est possible si l'on paramètre correctement la transaction fondamentale en donnant à la propriété Params d'IBTransaction1 les valeurs
IBTransaction1 |
|
Params … |
read_committed |
read_committed permet à la transaction de voir les valeurs validées par les autres transactions (notamment d'autres utilisateurs) et même de mettre à jour ces valeurs à son tour.
TIBTransaction propose des méthodes qui exécutent les ordres SQL vus ci-dessus. Ce sont
StartTransaction
Commit
Rollback
Cependant l'inconvénient de Commit et de Rollback c'est qu'ils obligent à réactiver la transaction. Cela est coûteux, pour une simple mise à jour.
Heureusement, nous avons à disposition une variation de ces commandes qui maintient la transaction ouverte. Ce sont
CommitRetaining
RollbackRetaining
Voyons maintenant comment programmer notre transaction
Plaçons sur un Data Module un composant TIBDatabase et TIBTransaction
Nous relions les deux composants en indiquant les propriétés
IBDatabase1.DefaultTransaction = IBTransaction1
IBTransaction1.DefaultDatabase = IBDatabase1
et mettons IBTransaction1.Params comme indiqué plus haut à read_committed, rec_version et nowait.
Note : nous ne détaillons ici que ce qui est en rapport avec les transactions.
Pour ceux qui veulent une initiation à l'utilisation des composants IBX, nous vous conseillons ce tutoriel.
Voyons la séquence de validation de la ligne de commande (qui peut se trouver dans le gestionnaire d'événement d'un bouton de la Form).
procedure
ReservationDeLigne;
begin
try
IBTransaction1.CommitRetaining; //Début de la transaction
if
StockSuffisant then
InsererLaLigne else
Abort;
if
AcceptationDeLaLigne then
SortieEffectiveDuStock else
Abort;
IBTransaction1.CommitRetaining; //Fin normale de la transaction
except
IBTransaction1.RollbackRetaining; //Fin anormale de la transaction
end
end
;
Cette méthode est en fait une succession de validations de la transaction fondamentale. Le premier CommitRetaining a pour but de marquer le point de départ auquel on reviendrait en cas de RollbackRetaining.
III. Les transactions à plusieurs niveaux▲
Lorsque l'on écrit des transactions, celles-ci peuvent se trouver incluses ultérieurement dans une transaction plus globale.
Par exemple, nous avons une transaction pour une ligne de la facture.
Mais il est tout à fait possible d'avoir également une transaction globale au niveau de la facture au sein de laquelle se produisent les transactions au niveau de chaque ligne.
Nous allons mettre en place une méthode qui prend cela en compte d'une manière très intéressante.
En écrivant différemment la procédure ci-dessus, nous n'aurons même pas besoin de la modifier le jour où elle se trouve prise dans une transaction globale (et ceci, quel que soit le nombre de transactions globales que nous serions amenés à emboîter par la suite).
Tout d'abord nous ajoutons la propriété NiveauDeTransaction en lecture seule dans le DataModule :
private
FNiveauDeTransaction: Integer
;
public
property
NiveauDeTransaction : Integer
read
FNiveauDeTransaction;
Ceci nous permettra de gérer en interne du DataModule le niveau de transaction et de savoir à l'extérieur du DataModule si nous sommes ou non dans une transaction.
Maintenant, pour gérer nos transactions, en tout point de l'application, nous allons créer trois procédures publiques dans le DataModule.
public
procedure
TransactionStart;
procedure
TransactionCommit;
procedure
TransactionRollback;
implementation
procedure
TransactionStart;
begin
if
FNiveauDeTransaction = 0
then
IBTransaction1.CommitRetaining;
Inc(FNiveauDeTransaction);
end
procedure
TransactionCommit;
begin
if
FNiveauDeTransaction > 0
then
Dec(FNiveauDeTransaction);
if
FNiveauDeTransaction = 0
then
IBTransaction1.CommitRetaining;
end
procedure
TransactionRollback;
begin
if
FNiveauDeTransaction > 0
then
IBTransaction1.RollbackRetaining;
FNiveauDeTransaction := 0
;
end
Bien entendu, nous nous interdisons désormais de faire appel à IBTransaction1. Pour gérer nos transactions, nous ferons désormais appel exclusivement aux trois procédures ci-dessus.
Note : comme vous pouvez le constater, la procédure TransactionStart ne met pas en œuvre IBTransaction1.StartTransaction comme on pourrait s'y attendre. En effet, nous ne créons pas de nouvelles transactions. Mais nous nous appuyons sur la transaction fondamentale (voir plus haut). Chaque Commit intervient comme un point de validation de cette transaction. C'est pourquoi on le trouve dans procedure TransactionStart; et dans procedure TransactionCommit;
Par exemple, la réservation de la ligne de facture s'écrit maintenant :
function
ReservationDeLigne : boolean
;
begin
Result := True
;
try
DataModule1.TransactionStart; //Début de la transaction
if
StockSuffisant then
InsererLaLigne else
Abort;
if
AcceptationDeLaLigne then
SortieEffectiveDuStock else
Abort;
DataModule1.TransactionCommit; //Fin normale de la transaction
except
DataModule1.TransactionRollback; //Fin anormale de la transaction
Result := False
;
end
end
;
Supposons maintenant que la ligne de facture ne sera définitivement validée que lorsque la facture sera validée dans son ensemble.
Par exemple, si nous achetons un parquet à poser avec de la colle, ou nous prenons l'ensemble, ou rien.
Le client peut également décider d'annuler, au vu du prix.
Donc nos transactions élémentaires se trouvent maintenant incluses dans une transaction globale au niveau de la facturation. (Nous avons dit au début que nous simplifions en ayant une seule opération pour la commande et la facture).
function
Facturation : boolean
;
begin
Result := True
;
try
DataModule1.TransactionStart; //Début de la transaction
repeat
if
not
ReservationDeLigne then
Abort;
until
DerniereLigne;
DataModule1.TransactionCommit; //Fin normale de la transaction
except
DataModule1.TransactionRollback; //Fin anormale de la transaction
Result := False
;
end
end
;
Nous remarquons que nos transactions au niveau des lignes n'ont pas été modifiées lorsque nous les avons englobées dans la transaction au niveau de la facture.
De même, si nous devons englober un jour la transaction du niveau de la facture dans une transaction d'ensemble, nos procédures ne seront pas à modifier.
Un dernier mot sur l'incidence des commandes Retaining utilisées de manière répétée : elles ne laissent pas la base de données InterBase dans un état optimum. C'est pourquoi il est souhaitable de faire tourner les utilitaires d'InterBase de manière régulière : voyez le conseil donné dans ce tutoriel sur l'utilisation de gfix et de gbak.
IV. Les transactions avec dbExpress▲
(En préparation.)
V. Les transactions avec ADO (dbGo)▲
Avec ADO, nous disposons de la gestion explicite des transactions dans TADOConnection ainsi que nous le voyons dans l'exemple ci-dessous.
procedure
ReservationDeLigne;
begin
try
ADOConnection1.BeginTrans; //Début de la transaction
if
StockSuffisant then
InsererLaLigne else
Abort;
if
AcceptationDeLaLigne then
SortieEffectiveDuStock else
Abort;
ADOConnection1.CommitTrans; //Fin normale de la transaction
except
ADOConnection1.RollbackTrans; //Fin anormale de la transaction
end
end
;
Dans ce tutoriel, nous avons vu comment protéger des ensembles d'opérations par des transactions.