Use template for transactions

Code related to transactions is bulky, and has significant logic attached to it. Such code should not be repeated every time a transaction is required. Instead, define a template method which handles most of the processing of the transaction.

Example

Tx is a generic interface which can be applied to both:


package hirondelle.web4j.database;

/**
 Execute a database transaction.

 <P>Should be applied only to operations involving more than one SQL statement.
*/
public interface Tx {

  /**
   Execute a database transaction, and return the number of edited records.
  */
  int executeTx() throws DAOException;
  
}
 

TxTemplate is an implementation of Tx for local transactions. (An implementation for distributed transactions would be similar). It's an Abstract Base Class, and uses the template method design pattern. To implement a transaction, callers subclass TxTemplate, and provide a concrete implementation of executeMultipleSqls(Connection).
import java.sql.*;
import java.util.logging.*;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Consts;

/** 
* Template for executing a local, non-distributed transaction versus a 
* single database, using a single connection.
*
* <P>This abstract base class implements the template method design pattern.
*/
public abstract class TxTemplate implements Tx {
  
  //..elided
  
  /**
  * <b>Template</b> method calls the abstract method {@link #executeMultipleSqls}.
  * <P>Returns the same value as <tt>executeMultipleSqls</tt>.
  *
  * <P>A <tt>rollback</tt> is performed if <tt>executeMultipleSqls</tt> fails.
  */
  public final int executeTx() throws DAOException {
    int result = 0;
    fLogger.fine(
      "Editing within a local transaction, with isolation level : " + fTxIsolationLevel
    );
    ConnectionSource connSource = BuildImpl.forConnectionSource();
    if(Util.textHasContent(fDatabaseName)){
      fConnection = connSource.getConnection(fDatabaseName);
    }
    else {
      fConnection = connSource.getConnection();
    }
    
    try {
      TxIsolationLevel.set(fTxIsolationLevel, fConnection);
      startTx();
      result = executeMultipleSqls(fConnection);
      endTx(result);
    }
    catch(SQLException rootCause){
      fLogger.fine("Transaction throws SQLException.");
      rollbackTx();
      String message = 
        "Cannot execute edit. ErrorId code : " +  rootCause.getErrorCode() + 
        Consts.SPACE + rootCause
      ;
      if (rootCause.getErrorCode() == DbConfig.getErrorCodeForDuplicateKey().intValue()){
        throw new DuplicateException(message, rootCause);
      }
      throw new DAOException(message, rootCause);
    }
    catch (DAOException ex){
      fLogger.fine("Transaction throws DAOException.");
      rollbackTx();
      throw ex;
    }
    finally {
      DbUtil.logWarnings(fConnection);
      DbUtil.close(fConnection);
    }
    fLogger.fine("Total number of edited records: " + result);
    return result;
  }

  /**
  * Execute multiple SQL operations in a single local transaction.
  *
  * <P>This method returns the number of records edited. 
  */
  public abstract int executeMultipleSqls(
    Connection aConnection
  ) throws SQLException, DAOException;
  
  // PRIVATE
  
  private Connection fConnection;
  private String fDatabaseName;
  private final TxIsolationLevel fTxIsolationLevel;
  
  private static final boolean fOFF = false;
  private static final boolean fON = true;
  
  private static final Logger fLogger = Util.getLogger(TxTemplate.class);  

  private void startTx() throws SQLException {
    fConnection.setAutoCommit(fOFF);
  }
  
  private void endTx(int aNumEdits) throws SQLException, DAOException {
    if ( BUSINESS_RULE_FAILURE == aNumEdits ) {
      fLogger.severe("Business rule failure occured. Cannot commit transaction.");
      rollbackTx();
    }
    else {
      fLogger.fine("Commiting transaction.");
      fConnection.commit();
      fConnection.setAutoCommit(fON);
    }
  }
  
  private void rollbackTx() throws DAOException {
    fLogger.severe("ROLLING BACK TRANSACTION.");
    try {
      fConnection.rollback();
    }
    catch(SQLException ex){
      throw new DAOException("Cannot rollback transaction", ex);
    }
  }
}
 

Here's a concrete implementation named RoleDAO. It has a change method which updates the roles attached to a user. The roles are stored in a cross-reference table. In this case, an update is implemented as 'delete all old, then add all new'. Note the lack of try..catch blocks in this class.
final class RoleDAO {

  //..elided  

  /**
  * Update all roles attached to a user.
  * 
  * <P>This implementation will treat all edits to user roles as 
  * '<tt>DELETE-ALL</tt>, then <tt>ADD-ALL</tt>' operations. 
  */
  boolean change(UserRole aUserRole) throws DAOException {
    Tx update = new UpdateTransaction(aUserRole);
    return Util.isSuccess(update.executeTx());
  }
  
  // PRIVATE //
  
  /** Cannot be a {@link hirondelle.web4j.database.TxSimple}, since there is looping. */
  private static final class UpdateTransaction extends TxTemplate {
    UpdateTransaction(UserRole aUserRole){
      super(ConnectionSrc.ACCESS_CONTROL);
      fUserRole = aUserRole;
    }
    public int executeMultipleSqls(
      Connection aConnection
    ) throws SQLException, DAOException {
      int result = 0;
      //perform edits using a shared connection
      result = result + DbTx.edit(aConnection, ROLES_DELETE, fUserRole.getUserName());
      for(Id roleId : fUserRole.getRoles()){
        result = result + DbTx.edit(aConnection,ROLES_ADD,fUserRole.getUserName(),roleId);
      }
      return result;
    }
    private UserRole fUserRole;
  }
}
 

See Also :
Template method
Simplify database operations
A Web App Framework WEB4J
Use template for repeated try catch