Transaction Management in JDBC
A transaction is a group of SQL statements that must be treated as one unit of work: either all of them take effect, or none do. The classic example is moving money between two accounts. Taking the money from one account and adding it to the other are two UPDATE statements; if the first succeeds and the second fails, money has vanished. A transaction prevents that: on any failure, everything done so far is rolled back. In JDBC, transactions are controlled through the Connection with three methods: setAutoCommit(false), commit() and rollback(). In this lesson, you will learn how they work, what ACID means, how savepoints and isolation levels fit in, and build a safe money transfer.
ACID: What a Transaction Guarantees
| Property | Meaning | In the transfer example |
|---|---|---|
| Atomicity | All statements happen, or none | The debit and the credit succeed or fail together |
| Consistency | Rules of the database always hold | A balance never goes below zero |
| Isolation | Concurrent transactions don't see each other's unfinished work | Nobody sees the money "in flight" |
| Durability | Once committed, changes survive a crash | A completed transfer is not lost |
Auto-Commit
A new JDBC connection is in auto-commit mode: every statement is committed as soon as it runs, as its own tiny transaction. To group statements, turn it off:
| Method | Description |
|---|---|
con.setAutoCommit(false) | It is used to start grouping statements into a transaction. |
con.commit() | It makes every change since the last commit permanent. |
con.rollback() | It undoes every change since the last commit. |
con.setSavepoint(name) | It marks a point inside the transaction. |
con.rollback(savepoint) | It undoes changes made after that point only. |
con.setTransactionIsolation(level) | It sets the isolation level. |
Checkpoint: A Safe Money Transfer
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
public class Transfer {
static void show(Connection con, String when) throws SQLException {
try (Statement st = con.createStatement();
ResultSet rs = st.executeQuery(
"SELECT owner, balance FROM account ORDER BY owner")) {
StringBuilder line = new StringBuilder(when + ":");
while (rs.next()) {
line.append(" ").append(rs.getString(1)).append("=")
.append(rs.getInt(2));
}
System.out.println(line);
}
}
static void transfer(Connection con, String from, String to, int amount)
throws SQLException {
String take =
"UPDATE account SET balance = balance - ? WHERE owner = ?";
String give =
"UPDATE account SET balance = balance + ? WHERE owner = ?";
con.setAutoCommit(false);
try (PreparedStatement debit = con.prepareStatement(take);
PreparedStatement credit = con.prepareStatement(give)) {
debit.setInt(1, amount);
debit.setString(2, from);
debit.executeUpdate();
credit.setInt(1, amount);
credit.setString(2, to);
if (credit.executeUpdate() != 1) {
throw new SQLException("No account for " + to);
}
con.commit();
System.out.println("Committed: " + amount + " from " + from
+ " to " + to);
} catch (SQLException e) {
con.rollback();
String state = e.getSQLState();
String why = state != null && state.startsWith("23")
? "a constraint was violated (SQLState " + state + ")"
: e.getMessage();
System.out.println("Rolled back: " + why);
} finally {
con.setAutoCommit(true);
}
}
public static void main(String[] args) throws Exception {
try (Connection con =
DriverManager.getConnection("jdbc:h2:mem:bank")) {
try (Statement st = con.createStatement()) {
st.execute("CREATE TABLE account (owner VARCHAR(30) "
+ "PRIMARY KEY, balance INT CHECK (balance >= 0))");
st.executeUpdate("INSERT INTO account VALUES "
+ "('Navika', 5000), ('Srihitha', 1000)");
}
show(con, "Start");
transfer(con, "Navika", "Srihitha", 2000);
show(con, "After 1");
// The receiver doesn't exist: the debit must be undone.
transfer(con, "Navika", "Advitha", 500);
show(con, "After 2");
// More than the balance: the CHECK constraint refuses it.
transfer(con, "Srihitha", "Navika", 9000);
show(con, "After 3");
// Savepoint: undo part of a transaction, keep the rest.
con.setAutoCommit(false);
try (Statement st = con.createStatement()) {
st.executeUpdate("UPDATE account SET balance = balance + 100 "
+ "WHERE owner = 'Navika'");
Savepoint bonus = con.setSavepoint("bonus");
st.executeUpdate("UPDATE account SET balance = balance + 999 "
+ "WHERE owner = 'Srihitha'");
con.rollback(bonus);
con.commit();
}
con.setAutoCommit(true);
show(con, "After savepoint");
}
}
}Start: Navika=5000 Srihitha=1000
Committed: 2000 from Navika to Srihitha
After 1: Navika=3000 Srihitha=3000
Rolled back: No account for Advitha
After 2: Navika=3000 Srihitha=3000
Rolled back: a constraint was violated (SQLState 23513)
After 3: Navika=3000 Srihitha=3000
After savepoint: Navika=3100 Srihitha=3000Explanation:
transferturns auto-commit off, runs the debit and the credit, and commits only if both worked. AnySQLExceptionleads torollback(). Thefinallyblock restores auto-commit so the connection is left as it was found.- Transfer 1 succeeds: 2,000 moves from Navika to Srihitha, and both changes are committed together.
- Transfer 2 names a receiver who has no account. The debit already ran, but the credit changed 0 rows, so the method throws.
rollback()undoes the debit; the balances are unchanged. - Transfer 3 tries to take 9,000 from an account holding 3,000. The table's
CHECK (balance >= 0)rule rejects the debit, the database reports a constraint violation (SQLState class 23), and nothing changes. - The savepoint: inside one transaction, a 100 bonus is applied, a savepoint is set, and a mistaken 999 is applied.
rollback(bonus)undoes only what came after the savepoint;commit()then keeps the 100.
Isolation Levels
When transactions run at the same time, three problems can appear:
| Problem | What happens |
|---|---|
| Dirty read | You read a change another transaction hasn't committed, and it is later rolled back |
| Non-repeatable read | You read a row twice and get different values, because another transaction committed a change in between |
| Phantom read | You run a query twice and get different rows, because another transaction inserted or deleted some |
The isolation level decides which of them are prevented:
Level (Connection.TRANSACTION_...) | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
READ_UNCOMMITTED | Possible | Possible | Possible |
READ_COMMITTED | Prevented | Possible | Possible |
REPEATABLE_READ | Prevented | Prevented | Possible |
SERIALIZABLE | Prevented | Prevented | Prevented |
Higher levels are safer and slower, because the database must do more to keep transactions apart. READ_COMMITTED is the default in PostgreSQL, Oracle and SQL Server; MySQL's default is REPEATABLE_READ. Set a level with con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) before starting the transaction.
Common Mistakes
Leaving auto-commit on for multi-statement work. A failure halfway leaves half the changes saved.
Forgetting rollback() in the catch. The failed transaction stays open, holding locks, and its partial changes may be committed later by accident.
Not restoring auto-commit on a pooled connection. The next user of that connection inherits your setting.
Forgetting to commit. With auto-commit off, closing the connection without committing loses the changes on most databases.
Assuming a successful statement changed rows. An UPDATE matching nothing succeeds with a count of 0; check it, as transfer does.
Interview Corner
How do you manage transactions in JDBC?
By default a connection is in auto-commit mode. To run several statements as one transaction, call connection.setAutoCommit(false), execute the statements, then call commit(). If any statement fails, call rollback() in the exception handler. Savepoints (setSavepoint, rollback(savepoint)) allow partial rollback. The isolation level is set with setTransactionIsolation. Auto-commit should be restored afterward, especially for pooled connections.
What are the ACID properties?
Atomicity: a transaction's operations all succeed or all fail. Consistency: a transaction takes the database from one valid state to another, preserving its rules. Isolation: concurrent transactions don't interfere with each other; the degree is set by the isolation level. Durability: once a transaction commits, its changes persist even after a failure.
What are the transaction isolation levels?
READ_UNCOMMITTED allows dirty reads, non-repeatable reads and phantom reads. READ_COMMITTED prevents dirty reads. REPEATABLE_READ also prevents non-repeatable reads. SERIALIZABLE prevents all three, as if transactions ran one after another. Higher levels reduce concurrency.
Practice
- Easy: Add a third account for Advitha and transfer 500 to it. Then try a transfer of a negative amount and decide how the method should refuse it.
- Harder: Add a
transfer_logtable. Each successful transfer must insert a log row in the same transaction as the two updates. Show that a failed transfer leaves no log row.
Key Takeaways
Was this tutorial helpful?