Les transactions

Henry Cesbron Lavau

Comment protéger des ensembles d'opérations par des transactions.

- Titre : Les transactions

- Auteur : Henry Cesbron Lavau

- Parution : 9 octobre 2002

- Mise à jour : 27 avril 2020

- Licence : Copyright ® 2002 Henry Cesbron Lavau. 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

- I. Introduction

- II. Les transactions avec InterBaseExpress (IBX)

- III. Les transactions à plusieurs niveaux

- IV. Les transactions avec dbExpress

- V. Les transactions avec ADO (dbGo)

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 :

1. Met à jour la donnée ;

2. 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 :

1. Réservation du stock ;

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.

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
rec_version
nowait

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.