Vai al contenuto

Gestione delle transazioni in JDBC

2025 – prof. Roberto Fuligni

Quando si interagisce con un database in un’applicazione Java, è fondamentale garantire l’integrità dei dati, specialmente quando si eseguono più operazioni che devono essere completate come un’unica unità logica. Le transazioni in JDBC permettono di eseguire più operazioni SQL in modo atomico, evitando situazioni in cui solo alcune di esse vengono eseguite con successo mentre altre falliscono.

Una transazione è una sequenza di operazioni SQL che devono essere eseguite come un blocco unitario e segue le proprietà ACID:

  • Atomicità (Atomicity): o tutte le operazioni vengono eseguite o nessuna viene applicata.
  • Coerenza (Consistency): il database rimane in uno stato valido prima e dopo la transazione.
  • Isolamento (Isolation): le transazioni concorrenti non devono interferire tra loro.
  • Durabilità (Durability): una volta confermata, una transazione non può essere annullata da un crash del sistema.

Per gestire una transazione in JDBC, si utilizzano tre metodi principali dell’oggetto Connection:

  • setAutoCommit(false): Disabilita l’autocommit per controllare manualmente la transazione;
  • commit(): Conferma tutte le operazioni eseguite dopo setAutoCommit(false);
  • rollback(): Annulla tutte le operazioni fatte dall’ultimo commit.

Di default, JDBC lavora in modalità autocommit, ovvero ogni istruzione SQL viene eseguita e confermata automaticamente. Per gestire manualmente una transazione, bisogna disabilitare l’autocommit.

Commit e Rollback

Supponiamo di voler trasferire fondi tra due conti bancari. Dobbiamo:

  1. prelevare fondi per 500 € dal conto n. 1;
  2. accreditare l’importo di 500 € sul conto n. 2;
  3. confermare le operazioni solo se entrambe si concludono con successo.

Se una delle due operazioni fallisce (ed esempio, per mancanza di fondi), si annulla l’intera transazione.

final int idContoDa = 1;
final int idContoA = 2;
final double importo = 10.00;

final String sqlPrelievo = "UPDATE conti SET saldo = saldo - ? WHERE id = ?";
final String sqlVersamento = "UPDATE conti SET saldo = saldo + ? WHERE id = ?";

try (Connection conn = DriverManager.getConnection(url, user, pwd)) {
    conn.setAutoCommit(false);

    try {
        try (PreparedStatement psPrelievo = conn.prepareStatement(sqlPrelievo)) {
            psPrelievo.setDouble(1, importo);
            psPrelievo.setInt(2, idContoDa);

            int righeModificate = psPrelievo.executeUpdate();
            if (righeModificate != 1) {
                throw new SQLException("Errore: conto da cui prelevare non trovato");
            }
        }

        try (PreparedStatement psVersamento = conn.prepareStatement(sqlVersamento)) {
            psVersamento.setDouble(1, importo);
            psVersamento.setInt(2, idContoA);

            int righeModificate = psVersamento.executeUpdate();
            if (righeModificate != 1) {
                throw new SQLException("Errore: conto su cui versare non trovato");
            }
        }

        conn.commit();

    } catch (SQLException e) {
        conn.rollback();
        System.err.println("Errore durante l'esecuzione della transazione: " +
                e.getMessage());
    }
} catch (SQLException e) {
    System.err.println("Errore di connessione al database: " +
            e.getMessage());
}
CREATE TABLE conti (
    id INT PRIMARY KEY AUTO_INCREMENT,
    nome VARCHAR(50) NOT NULL,
    saldo DECIMAL(10,2) NOT NULL DEFAULT 0
);

INSERT INTO conti (id, nome, saldo) VALUES
(1, 'Mario Rossi', 1000.00),
(2, 'Luigi Bianchi', 500.00);

Nell’esempio precedente, se si verifica un errore durante il trasferimento (conto inesistente), il blocco catch chiama rollback(), ripristinando lo stato del database prima dell’inizio della transazione.

Livelli di isolamento delle transazioni

Quando si lavora con database relazionali come MariaDB, la gestione della concorrenza è fondamentale per evitare anomalie nei dati durante l’esecuzione di transazioni, le quali possono interferire tra loro se eseguite simultaneamente.

JDBC supporta quattro livelli di isolamento standard, definiti nella specifica SQL:

Costante JDBC Livello Descrizione
TRANSACTION_READ_UNCOMMITTED Basso Una transazione può leggere dati non ancora confermati da altre transazioni
TRANSACTION_READ_COMMITTED Medio Legge solo dati confermati da altre transazioni (Default in molti DBMS)
TRANSACTION_REPEATABLE_READ Alto Previene letture inconsistenti all’interno della stessa transazione
TRANSACTION_SERIALIZABLE Massimo Le transazioni vengono eseguite una alla volta, evitando ogni possibile interferenza

JDBC permette di controllare il livello di isolamento delle transazioni tramite conn.setTransactionIsolation(int livello). Per esempio, per impostare il livello su TRANSACTION_REPEATABLE_READ:

conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);

READ UNCOMMITTED

Il livello più basso di isolamento. Una transazione può leggere dati che un’altra transazione ha modificato ma non ha ancora confermato, e questi dati potrebbero essere annullati (problema delle letture sporche o dirty reads):

  1. La transazione A aggiorna il saldo di un conto da 1000€ a 800€
  2. La transazione B legge il saldo dello stesso conto (800€) prima che A faccia commit
  3. La transazione A viene annullata (rollback)
  4. La transazione B ora ha un valore non valido (il saldo reale è 1000€)

READ COMMITTED

Una transazione può vedere solo dati che sono stati confermati da altre transazioni, prevenendo le letture sporche. Tuttavia, se una transazione legge lo stesso record più volte, potrebbe ottenere valori diversi se un’altra transazione modifica e conferma i dati tra le letture (problema delle letture non ripetibili o non-repeatable reads):

  1. La transazione A legge il saldo di un conto (1000€)
  2. La transazione B modifica il saldo a 800€ e fa commit
  3. La transazione A legge di nuovo lo stesso saldo e ottiene 800€
  4. La transazione A ha ora due valori diversi per lo stesso dato

REPEATABLE READ

Previene sia le letture sporche che le letture non ripetibili, garantendo che, se una transazione legge un record, continuerà a vedere gli stessi dati per quel record fino alla fine della transazione.

Se però una transazione esegue una query che restituisce un insieme di righe più volte, potrebbe vedere righe aggiuntive (fantasma) se un’altra transazione inserisce nuove righe che corrispondono alla condizione della query (problema delle letture fantasma o phantom reads):

  1. La transazione A seleziona tutti i conti con saldo > 1000€ (trova 5 record)
  2. La transazione B inserisce un nuovo conto con saldo 1500€ e fa commit
  3. La transazione A seleziona di nuovo tutti i conti con saldo > 1000€ e trova 6 record

SERIALIZABLE

È il livello più alto di isolamento. Garantisce la massima consistenza dei dati, prevenendo tutti i problemi sopra menzionati, comprese le letture fantasma. Le performance risultano tuttavia ridotte a causa dei blocchi imposti da questo livello di isolamento.