Soutenez-nous

Les transactions

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

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Google Bookmarks ! Facebook Digg del.icio.us Yahoo MyWeb Blinklist Netvouz Reddit Simpy StumbleUpon Bookmarks Share on Google+ 

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 conclue 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 à leurs valeurs initiales).

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 1° client 1  
  2 Réservation du stock pour le 2° 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 1° client à l'étape 5, et, à l'étape 6, le 2° 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 1° client, au vu du prix, refuse la commande, la vente est alors perdue pour le client 2 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 1° 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 2° 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 1° client pour traiter le 2° 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à inscrite 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

 
Sélectionnez
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

 
Sélectionnez
CommitRetaining
RollbackRetaining

Voyons maintenant comment programmer notre transaction

Plaçons sur un Data Module un composant TIBDatabase et TIBTransaction

Image non disponible

Nous relions les deux composants en indiquant les propriétés

 
Sélectionnez
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).

 
Sélectionnez
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 un 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 transaction globale 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 :

 
Sélectionnez
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 3 procédures publiques dans le DataModule.

 
Sélectionnez
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 3 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 :

 
Sélectionnez
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).

 
Sélectionnez
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.

 
Sélectionnez
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.

Interbase
Ma première base InterBase
Delphi 6 et InterBase 6
dbExpress avec Delphi 6
Delphi 6 Relationnel
Le bon PLAN d'InterBase
MDA (Model Driven Architecture)
MDA
Programmez avec les diagrammes de Delphi 7
Les objets métiers avec ModelMaker de Delphi 7
Les Design Patterns avec ModelMaker de Delphi 7
Autres articles
Les transactions
Mise à jour de Delphi 6

  

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.