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 doposetAutoCommit(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:
- prelevare fondi per 500 € dal conto n. 1;
- accreditare l’importo di 500 € sul conto n. 2;
- 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):
- La transazione A aggiorna il saldo di un conto da 1000€ a 800€
- La transazione B legge il saldo dello stesso conto (800€) prima che A faccia commit
- La transazione A viene annullata (rollback)
- 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):
- La transazione A legge il saldo di un conto (1000€)
- La transazione B modifica il saldo a 800€ e fa commit
- La transazione A legge di nuovo lo stesso saldo e ottiene 800€
- 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):
- La transazione A seleziona tutti i conti con saldo > 1000€ (trova 5 record)
- La transazione B inserisce un nuovo conto con saldo 1500€ e fa commit
- 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.