Gestion des transactions JDBC et contrôle d'isolation

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_UNCOMMITTED
  • TRANSACTION_READ_COMMITTED
  • TRANSACTION_REPEATABLE_READ
  • TRANSACTION_SERIALIZABLE

Le choix dépend des besoins en cohérence vs performance dans un environnement concurrent.

Étiquettes: JDBC transaction rollback commit isolation-level

Publié le 7 septembre à 14h22