How do I use savepoints in JDBC transactions?

Savepoints in JDBC provide fine-grained control over transactions by allowing you to roll back to a specific point within a transaction rather than undoing everything. This is particularly useful for handling optional operations or partial failures.

Key Steps to Use Savepoints

  1. Disable Auto-commit: Savepoints only work within a manual transaction.
  2. Set a Savepoint: Use connection.setSavepoint() to mark a logical point in your execution.
  3. Rollback to Savepoint: If an error occurs, use connection.rollback(savepoint).
  4. Release or Commit: Release the savepoint once it’s no longer necessary (though commit or a full rollback will also clear them).

Implementation Example

Here is how you can implement this in your project.

package org.kodejava.jdbc;

import java.sql.*;

public class SavepointExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/your_database";
        String user = "root";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            // 1. Disable auto-commit
            conn.setAutoCommit(false);

            try (Statement stmt = conn.createStatement()) {
                // Execute a required operation
                stmt.executeUpdate("INSERT INTO orders (item, qty) VALUES ('Laptop', 1)");

                // 2. Set a savepoint before an "optional" or risky operation
                Savepoint savepoint1 = conn.setSavepoint("Savepoint1");

                try {
                    // Try an optional operation (e.g., updating a secondary table)
                    stmt.executeUpdate("INSERT INTO loyalty_points (user_id, points) VALUES (1, 100)");
                } catch (SQLException e) {
                    // 3. Roll back to the savepoint if the optional part fails
                    System.out.println("Optional operation failed, rolling back to savepoint.");
                    conn.rollback(savepoint1);
                }

                // 4. Commit the overall transaction
                conn.commit();
                System.out.println("Transaction committed successfully.");

            } catch (SQLException e) {
                // If the main operation fails, roll back everything
                conn.rollback();
                e.printStackTrace();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Things to Keep in Mind

  • Named vs. Unnamed Savepoints: conn.setSavepoint() returns an unnamed savepoint with a system-generated ID. conn.setSavepoint("Name") creates a named one which can be easier for debugging.
  • Release Savepoints: While not strictly mandatory in all drivers, calling connection.releaseSavepoint(savepoint) can help free up resources if you have many savepoints in a long-running transaction.
  • Driver Support: Most modern databases (MySQL, PostgreSQL, Oracle, SQL Server) support savepoints, but you can check programmatically using DatabaseMetaData.supportsSavepoints().
  • Transaction Scope: Once a transaction is committed or rolled back entirely, all associated savepoints are automatically released and become invalid.

How do I use transactions in JDBC?

Using transactions in JDBC is essential when you need to ensure that a group of SQL statements either all succeed or all fail together (maintaining Atomicity).

By default, a JDBC Connection is in auto-commit mode, meaning every single SQL statement is treated as its own transaction and committed immediately.

To manage transactions manually, follow these three main steps:

1. Disable Auto-Commit

The first step is to tell the connection not to commit automatically after every execution.

connection.setAutoCommit(false);

2. Perform Your Database Operations

Execute your SQL statements (inserts, updates, deletes). If any of these throw an exception, you should catch it to handle the failure.

3. Commit or Rollback

  • commit(): If everything went well, save the changes permanently.
  • rollback(): If an error occurred, undo all changes made since the last commit.

Basic Example

Here is a clean pattern using a try-with-resources block for the connection and a nested try-catch for the transaction logic:

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    // Step 1: Disable auto-commit
    conn.setAutoCommit(false);

    try (PreparedStatement pstmt1 = conn.prepareStatement("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
         PreparedStatement pstmt2 = conn.prepareStatement("UPDATE accounts SET balance = balance + 100 WHERE id = 2")) {

        // Execute operations
        pstmt1.executeUpdate();
        pstmt2.executeUpdate();

        // Step 3a: Commit changes
        conn.commit();
        System.out.println("Transaction committed successfully!");

    } catch (SQLException e) {
        // Step 3b: Rollback changes if something goes wrong
        conn.rollback();
        System.err.println("Transaction rolled back due to error.");
        e.printStackTrace();
    }
} catch (SQLException e) {
    e.printStackTrace();
}

Important Tips:

  • Always use rollback() in the catch block: If you don’t roll back on failure, the connection might hold onto locks or leave the session in an inconsistent state.
  • Savepoints: If you have a very long transaction and want to roll back only a part of it, you can use conn.setSavepoint().
  • Transaction Isolation: You can control how “isolated” your transaction is from others using conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE), though the default is usually enough for standard applications.

How do I show Spring transaction in log / console?

When you use the Spring framework @Transactional annotation in your service layer you might want to see what is happening in your code related to database transaction. You want to see when a transaction is started, when it is committed or rollbacked.

To activate the log for transactional message you can add the following configurations in your application properties file. For example when using the JpaTransactionManager you can set the log level to DEBUG.

logging.level.root=INFO

logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG

Running the spring boot application with these configuration, the JpaTransactionManager will write something line these on your log file or console:

2023-03-29T23:06:52.194+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.kodejava.webapp.accounting.service.impl.CalculationServiceImpl.recalculate]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2023-03-29T23:06:52.194+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(974784570<open>)] for JPA transaction
2023-03-29T23:06:52.195+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@4150907e]
...
...
2023-03-29T23:06:52.195+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(974784570<open>)] for JPA transaction
2023-03-29T23:06:52.195+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
...
...
2023-03-29T23:06:52.237+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2023-03-29T23:06:52.237+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(974784570<open>)]
2023-03-29T23:06:52.237+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(974784570<open>)] after transaction