Comprendre les transactions en base de données
Une transaction représente une unité logique d'opération qui regroupe plusieurs instructions DML (Data Manipulation Language) devant être exécutées ensemble. L'objectif est d'assurer l'intégrité des données : soit toutes les opérations réusissent, soit aucune n'est appliquée. Ce principe est fondamental dans les systèmes critiques comme les transferts bancaires.
Par exemple, lorsqu'un compte A transfère 10 000 € à un compte B, deux mises à jour doivent avoir lieu :
- Déduction de 10 000 sur le solde du compte A
- Ajout de 10 000 sur le solde du compte B
Si seule la première opération aboutit, cela entraîne une perte d'argent. Une transaction garantit que ces deux modifications sont atomiques.
Préparation de la table pour le test
Considérons une table t_act avec les colonnes suivantes :
actno(numéro de compte)balance(solde)
Implémentation d'un transfret sans gestion de transaction
Voici un extrait de code illustrant un transfert simple entre deux comptes :
package com.example.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransferExample {
public static void main(String[] args) {
double amount = 10000.0;
Connection connection = null;
PreparedStatement stmt1 = null;
PreparedStatement stmt2 = null;
try {
connection = DatabaseHelper.getConnection();
// Retrait du compte source
String withdrawSql = "UPDATE t_act SET balance = balance - ? WHERE actno = ?";
stmt1 = connection.prepareStatement(withdrawSql);
stmt1.setDouble(1, amount);
stmt1.setString(2, "act-001");
stmt1.executeUpdate();
// Simuler une erreur après la première mise à jour
int error = 1 / 0; // Division par zéro
// Dépôt sur le compte destination
String depositSql = "UPDATE t_act SET balance = balance + ? WHERE actno = ?";
stmt2 = connection.prepareStatement(depositSql);
stmt2.setDouble(1, amount);
stmt2.setString(2, "act-002");
stmt2.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DatabaseHelper.close(stmt1);
DatabaseHelper.close(stmt2);
DatabaseHelper.close(connection);
}
}
}
Ce code présente un risque majeur : chaque instruction DML est automatiquement validée dès son exécution. Si une exception survient entre les deux requêtes, le retrait sera persisté alors que le dépôt ne le sera pas, provoquant une incohérence financière.
Désactivation de la validation automatique
Par défaut, JDBC valide automatiquement chaque modification. Pour contrôler manuellement le moment de la validation, il faut désactiver ce comportement :
connection.setAutoCommit(false);
Implémentation sécurisée avec gestion de transaction
La solution consiste à regrouper les opérations dans une transaction explicite :
public class SecureTransfer {
public static void main(String[] args) {
double amount = 10000.0;
Connection conn = null;
PreparedStatement withdrawal = null;
PreparedStatement deposit = null;
try {
conn = DatabaseHelper.getConnection();
// Désactiver la validation automatique → démarrer une transaction
conn.setAutoCommit(false);
// Opération 1 : retrait
String sql1 = "UPDATE t_act SET balance = balance - ? WHERE actno = ?";
withdrawal = conn.prepareStatement(sql1);
withdrawal.setDouble(1, amount);
withdrawal.setString(2, "act-001");
withdrawal.executeUpdate();
// Opération 2 : dépôt
String sql2 = "UPDATE t_act SET balance = balance + ? WHERE actno = ?";
deposit = conn.prepareStatement(sql2);
deposit.setDouble(1, amount);
deposit.setString(2, "act-002");
deposit.executeUpdate();
// Valider la transaction si tout s'est bien passé
conn.commit();
} catch (Exception e) {
// En cas d'erreur, annuler toutes les modifications
if (conn != null) {
try {
conn.rollback();
} catch (SQLException rollbackEx) {
rollbackEx.printStackTrace();
}
}
e.printStackTrace();
} finally {
// Rétablir le comportement par défaut et libérer les ressources
try {
if (conn != null) conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
DatabaseHelper.close(withdrawal);
DatabaseHelper.close(deposit);
DatabaseHelper.close(conn);
}
}
}
Avec cette approche :
- Les modifications restent temporaires tant que
commit()n'est pas appelé - En cas d'exception,
rollback()annule toutes les opérations effectuées dans la transaction - L'intégrité des données est préservée même en présence d'erreurs
Configuration du niveau d'isolation des transactions
JDBC permet également de définir le niveau d'isolation pour contrôler comment les transactions interagissent entre elles. Cela influence la visibilité des modifications non validées et la protection contre les anomalies concurrentes.
public class IsolationConfig {
public static void main(String[] args) {
Connection connection = null;
try {
connection = DatabaseHelper.getConnection();
// Définir le niveau d'isolation le plus strict
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
} catch (SQLException e) {
e.printStackTrace();
} finally {
DatabaseHelper.close(connection);
}
}
}
Les niveaux disponibles via JDBC incluent :
TRANSACTION_READ_UNCOMMITTEDTRANSACTION_READ_COMMITTEDTRANSACTION_REPEATABLE_READTRANSACTION_SERIALIZABLE
Le choix dépend des besoins en cohérence vs performance dans un environnement concurrent.