简体   繁体   中英

JDBC Transaction error handling

The documentation about using transactions with jdbc suggests the following code

public void updateCoffeeSales(HashMap<String, Integer> salesForWeek)
    throws SQLException {

    PreparedStatement updateSales = null;
    PreparedStatement updateTotal = null;

    String updateString =
        "update " + dbName + ".COFFEES " +
        "set SALES = ? where COF_NAME = ?";

    String updateStatement =
        "update " + dbName + ".COFFEES " +
        "set TOTAL = TOTAL + ? " +
        "where COF_NAME = ?";

    try {
        con.setAutoCommit(false);
        updateSales = con.prepareStatement(updateString);
        updateTotal = con.prepareStatement(updateStatement);

        for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
            updateSales.setInt(1, e.getValue().intValue());
            updateSales.setString(2, e.getKey());
            updateSales.executeUpdate();
            updateTotal.setInt(1, e.getValue().intValue());
            updateTotal.setString(2, e.getKey());
            updateTotal.executeUpdate();
            con.commit();
        }
    } catch (SQLException e ) {
        JDBCTutorialUtilities.printSQLException(e);
        if (con != null) {
            try {
                System.err.print("Transaction is being rolled back");
                con.rollback();
            } catch(SQLException excep) {
                JDBCTutorialUtilities.printSQLException(excep);
            }
        }
    } finally {
        if (updateSales != null) {
            updateSales.close();
        }
        if (updateTotal != null) {
            updateTotal.close();
        }
        con.setAutoCommit(true);
    }
}

However, the error handling seems wrong to me?

If there is a NullPointerException inside the try block, it will not be caught. Instead, execution will go straight to the finally block, where it will call con.setAutoCommit(true) , which according the documentation will commit any transactions in progress. It seems that this is obviously not the intended behavior, as it commits an incomplete transaction.

I would think that this might just be a bug in the example, but other tutorials also forget to catch exceptions besides SqlException ( further example ).

Am I misunderstanding what's going on?

Before calling con.setAutoCommit(true) , the example closes the possibly outstanding prepared statements. As such, those statements will not get executed when switching back to auto-commit mode. No incomplete transactions will get committed there.

I checked the first tutorial you linked, it makes the mistake of not using a finally block, not cancelling the outstanding statements, and not restoring the auto-commit mode. That seems really careless.

In general, I recommend to stick to the official tutorials, or to sources you really really trust.

I'm inclined to think you're right; my solution to this was to write the following class some time ago which correctly rolls-back when an unexpected exception is raised. As a bonus, you get to wrap transactions up in a try-with-resources block which I find much more readable and easy to work with.

You use it like so:

try(Transaction trans = Transaction.create(conn)) {
  // execute multiple queries, use trans.getConn()
  trans.commit();
} catch (SQLException e) {
  // Handle exception, transaction is safely rolled-back and closed
}
// No need for a finally block, or to catch RuntimeException.

Here's the full class:

import static com.google.common.base.Preconditions.checkState;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * A Transaction can be used in a try-with-resources block to ensure a set of queries are
 * executed as a group.
 * 
 * try(Transaction trans = Transaction.create(conn)) {
 *   // execute multiple queries, use trans.getConn()
 *   trans.commit();
 * } catch (SQLException e) {
 *   // Handle exception, transaction is safely rolled-back and closed
 * }
 */
public final class Transaction implements AutoCloseable {
    private Connection conn;
    private boolean committed = false;
    private boolean rolledback = false;

    /**
     * Create a Transaction on the current connection, use to create
     * a try-with-resources block.
     * 
     * Note that if a transaction is started while another transaction is
     * ongoing (i.e. conn.getAutoCommit() == true) the earlier transaction
     * is committed. 
     */
    public static Transaction start(Connection conn) throws SQLException {
        return new Transaction(conn);
    }

    private Transaction(Connection conn) throws SQLException {
        this.conn = conn;
        // this is a no-op if we're not in a transaction, it commits the previous transaction if we are
        this.conn.setAutoCommit(true);
        this.conn.setAutoCommit(false);
    }

    /**
      * Call once all queries in the transaction have been executed,
      * to indicate transaction is complete and ready to be committed.
      * Should generally be the last line in the try block. 
     */
    public void commit() throws SQLException {
        if(committed) {
            throw new SQLException("Cannot commmit a transaction more than once");
        }
        if(rolledback) {
            throw new SQLException("Cannot commit a previously rolled-back transaction");
        }
        committed = true;
        getConn().commit();
    }

    /**
     * Call explicitly to cancel the transaction, called implicitly
     * if commit() is not called by the time the Transaction should
     * be closed.
     */
    public void rollback() throws SQLException {
        if(rolledback) {
            throw new SQLException("Cannot rollback a transaction more than once");
        }
        if(committed) {
            throw new SQLException("Cannot rollback a previously committed transaction");
        }
        rolledback = true;
        getConn().rollback();
    }

    /**
     * Should not be called directly, called in the try-with-resources
     * finally block to close the transaction.
     */
    @Override
    public void close() throws SQLException {
        try {
            if(!committed && !rolledback) {
                conn.rollback();
                throw new SQLException("Should explicitly rollback or commit transaction, rolling-back");
            }
        } finally {
            conn.setAutoCommit(true);
            conn = null;
        }
    }

    /**
     * Returns the Connection being used for this transaction.  You are encouraged
     * to use this method to access the transactional connection while inside the
     * transaction's try-with-resources block.
     */
    public Connection getConn() {
        checkState(conn != null, "Connection has already been closed");
        return conn;
    }
}

I haven't yet open-sourced the project this class is a part of, but I'd be happy to explicitly release this under an MIT license if anyone needs it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM