Transaction Management in JDBC

Naveen AnamaneniUpdated · 8 min read

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

PropertyMeaningIn the transfer example
AtomicityAll statements happen, or noneThe debit and the credit succeed or fail together
ConsistencyRules of the database always holdA balance never goes below zero
IsolationConcurrent transactions don't see each other's unfinished workNobody sees the money "in flight"
DurabilityOnce committed, changes survive a crashA 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:

MethodDescription
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.
Commit keeps everything; rollback undoes everything

Checkpoint: A Safe Money Transfer

Transfer.java▶ Try it
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");
        }
    }
}
Output
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=3000

Explanation:

  1. transfer turns auto-commit off, runs the debit and the credit, and commits only if both worked. Any SQLException leads to rollback(). The finally block restores auto-commit so the connection is left as it was found.
  2. Transfer 1 succeeds: 2,000 moves from Navika to Srihitha, and both changes are committed together.
  3. 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.
  4. 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.
  5. 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:

ProblemWhat happens
Dirty readYou read a change another transaction hasn't committed, and it is later rolled back
Non-repeatable readYou read a row twice and get different values, because another transaction committed a change in between
Phantom readYou 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 readNon-repeatable readPhantom read
READ_UNCOMMITTEDPossiblePossiblePossible
READ_COMMITTEDPreventedPossiblePossible
REPEATABLE_READPreventedPreventedPossible
SERIALIZABLEPreventedPreventedPrevented

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

  1. 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.
  2. Harder: Add a transfer_log table. 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.

Quick check: Transactions

4 questions
1What is the default commit mode of a JDBC connection?
2The second of two updates fails. What should the code do?
3Which ACID property means "all or nothing"?
4Which isolation level prevents dirty reads but allows non-repeatable reads?

Key Takeaways

Was this tutorial helpful?

Lesson 498 of 578