Le développeur que vous êtes, doit comprendre et appréhender les problèmes liés à la gestion de transactions lors du développement de ses applications. Vous avez à votre disposition une multitude d'API qui vous permettent de mener à bien cette tache. Personnellement, j'ai opté le plus souvent pour Spring pour la gestion de transactions.
Dans Spring, on peut gérer les transactions soit par programmation soit par déclaration. Les fonctions de prise en charge des transactions dans Spring offrent une alternative aux transactions des EJB en apportant des possibilité transactionnelles aux POJO (Plain Old Java Object).
Dans la gestion des transactions par programmation, le code de gestion des transactions est incorporé dans les méthodes métier de manière à contrôler la validation (commit) et l'annulation (rollback) des transactions. En général, une transaction est validé lorsqu'une méthode se termine de manière normale, elle est annulée lorsqu'une méthode lance certain types d'exceptions. Avec cette gestion des transactions, nous pouvons définir nos propres règles de validation et d'annulation.
Toutefois, cette approche nous oblige à écrire du code de gestion supplémentaire pour chaque opération transactionnelle. Un code transactionnel standard est ainsi reproduit pour chacune des opérations. Par ailleurs, il nous est difficile d'activer et de désactiver la gestion des transaction pour différentes applications. Avec nos connaissance en POA (Programmation Orientée Aspect), nous comprenons sans peine que la gestion des transactions est une forme des préoccupations transversale.
Vous comprenez donc que sans la gestion des transactions, les données et les ressources pourraient être endommagées et laissées dans un état incohérent. Cette gestion est extrêmement importante dans les environnements concurrents et répartis lorsque le traitement doit se poursuivre après des erreurs inattendues.
En première définition, une transaction est une suite d'actions qui forment une seule unité de travail. Ces actions doivent toutes réussir ou n'avoir aucun effet. Si toutes les actions réussissent, la transaction est validée de manière permanente. En revanche, si l'une des actions se passe mal, la transaction est annulée de manière à revenir dans l'état initial, comme si rien ne s'était passé.
Le concept de transactions peut être décrit par les propriétés ACID :
- Atomicité : une transaction est une opération atomique constituée d'une suite d'opérations. L'atomicité de celle-ci garantit que toutes les actions sont entièrement exécutées ou qu'elles n'ont aucun effet.
- Cohérence : dès lors que toutes les actions d'une transaction se sont exécutées, la transaction est validée. Les données et les ressources sont alors dans un état cohérent qui respecte les règles métier.
- Isolation : puisque plusieurs transactions peuvent manipuler le même jeu de données au même moment, chaque transaction doit être isolée des autres afin d'éviter la corruption des données.
- Durabilité : dès lors qu'une transaction est terminée, les résultats doivent survivre à toute panne du système. En général, les résultat d'une transaction sont écrit dans une zone de stockage persistant.
Pour mieux illustrer ça, quoi de mieux qu'un exemple :
Supposons, qu'on dispose d'une application pour gérer une librairie. Dans une libraire, il faut répertorier tous les livres (nous allons donc créer une table SQL BOOK), mettre à jour au quotidien le stock pour chaque livre (donc d'une table SQL BOOK_STOCK) puis, pour chaque client, enregistrer son nom (ou son USERNAME) et son solde, et veiller à ce que ce solde ne soit jamais négatif ni nul, (table ACCOUNT)
Voici le script de création de chaque table :
CREATE TABLE BOOK(
ISBN VARCHAR(50) NOT NULL,
BOOK_NAME VARCHAR(100) NOT NULL,
PRICE INT,
PRIMARY KEY (ISBN)
);
CREATE TABLE BOOK_STOCK(
ISBN VARCHAR(50) NOT NULL,
STOCK INT NOT NULL,
PRIMARY KEY(ISBN),
CHECK (STOCK>=0)
);
La quantité en stock est affectée d'une contrainte CHECK de manière à rester positive. Cette contrainte, bien qu'elle soit définie dans le SQL-99, elle n'est pas reconnu par tous les moteurs de base de donnée, je l'ai utilisée pour sa simplicité et pour les besoins de l'exemple
CREATE TABLE ACCOUNT(
USERNAME VARCHAR(50) NO NULL,
BALANCE INT NOT NULL,
PRIMARY KEY(USERNAME),
CHECK (BALANCE>=0)
);
De la même manière, la contrainte CHECK garantie que le solde (balance) reste positif.
Définissons, en java, une interface qui définit les opérations de notre librairie. Pour faire simple, l'interface ne définie qu'une opération PURCHASE() pour acheter.
public interface BookShop{
public void purchase(String isbn, String username);
}Puis nous implémentons cette interface avec JDBC, nous crions la classe JdbcBookShop suivant, pour mieux comprendre les transactions, procédant sans l'aide de Spring.
public class JdbcBookShop implements BookShop{
private DataSource dataSource;
public void setDataSource(DataSource dataSource){
this.dataSource = dataSource;
}
public void purchase(String isbn, string username){
connection conn=null;
try{
conn = dataSource.getConnection();
preparedStatement stm1 = conn.prepareStatment("Select PRICE FROM BOOK WHER ISBN=?");
stm1.setString(1,isbn);
ResultSet rs = stm1.executeQuery();
rs.next();
int price = rs.getInt("PRICE");
stm1.close();
PreparedStatment stm2 = conn.prepareStatment("UPDATE BOOK_STOCK SET STOCK=STOCK-1 WHERE ISBN=?");
stm2.setString(1,isbn);
stm2.executeUpdate();
stm2.close();
PreparedStatement stm3 = conn.prepareStament("UPDATE ACCOUNT SET BLANCE=BALANCE -? WHERE USERNAME=?");
stm3.setINT(1,price);
stm3.set(2,username);
stm3.executeUpdate();
stm3.close();
}catch(SQLException e){
throw new runtimeException(e);
}finally{
if(conn!=null) try{conn.close()}catch(SQLException e){}
}
}}
Pour l'opération purchase(), nous exécutons trois requêtes SQL. La première obtient le prix du livre, tandis que la seconde et la troisième mettent à jour le stock du livre et ajustent le solde du compte en conséquence.
Si le client "utilisateur1" dont le solde =20 DHM, vient acheter le livre "Le premier livre" dont la quantité en stock=10 livres et le prix unitaire est 30 DHM et le ISBN='0001'. A l'exécution du programme au-dessus, nous allons lever une exception SQLException car la contrainte CHECK sur la table ACCOUNT n'est pas respectée. Ce résultat était attendu, puisque nous avons tenté un débit supérieur au solde du compte. Toutefois, si nous examinons le stock de ce livre dans la table BOOK_STOCK, nous constant qu'il a été diminué par cette opération ratée. En effet, nous avons exécuté la deuxième requête SQL pour diminuer le stock avant de recevoir l'exception générer par la troisième.
L'absence de la gestion des transaction a donc placé nos données dans un état incohérent. La solution est donc de placer nos trois requêtes de l'opération purchase() dans une même transaction. Si l'une des requête échoue, l'intégralité de la transaction est annulée pour défaire les changements apportés par les actions exécutées.
Ceci n'est qu'un simple exemple pour illustrer l'intérêt de la gestion des transactions.
Bonne lecture.