Managing DAO transactions in Java

by Keld H. Hansen

Introduction

In one of my recent articles, "Using DAOs in Apache Struts", I described how to use the DAO design pattern in various application types: Java main applications as well as servlet applications, implemented using Apache Struts, for example. In this article the focus will be on some of the more advanced situations a developer faces when using DAOs, starting with transactions. A transaction is defined as a series of operations or actions, that either must be entirely completed without failures, or none must be completed. Coding proper transaction handling using a given back-end system, for example a database, is by no means trivial. Luckily for us, most back-end systems come with ready-to-use modules for transaction handling, so it's simply a matter of understanding and using these modules correctly.

If you're fond of frameworks like Hibernate or Spring, you don't need to know how to implement transactions, since the frameworks may handle them for you. Most developers however will need to know the basics about transaction handling, in order to use frameworks correctly or when debugging system errors. This article will therefore explain the details about transaction handling, but first it's important to look at the various layers of a DAO application and the purpose of each layer.   

The code examples presented below will use classes presented in "Using DAOs in Apache Struts", and all code may be downloaded from this zip-file.   

The DAO layer

The DAOs form a simple, thin layer to the back-end system. The most important characteristics are:

Since a DAO must be able to implement various back-end systems, it's common practice to let every DAO implement a specific Java Interface. Furthermore, to be able to easily shift from one DAO implementation to another, it's also a good practice to let "factory" classes generate the DAO instances. In "Using DAOs in Apache Struts" there are coding examples to show you how this is being done.

The Transaction Handler layer

Since the DAOs are only simple facades to the back-end, something intelligent is needed in front of them. This is where the Transaction Handlers come into play. You might also call these modules supervisors or managers, there is--to my knowledge--no standardized name for this layer. It's important also for this layer to emphasize what its role is:

The Transaction Object

Since the Transaction Handler must not have any knowledge about how transactions are actually implemented on the back-end, a standardized convention is needed for the handler, when calling the DAOs, to tell them if they should participate in a specific transaction. A common solution is to introduce a Transaction object which encapsulates the way transactions are implemented. The handler may now initialize the Transaction object and pass it to the DAOs.

It's important to realize that a given implementation of such a transaction object normally will contain code or properties that closely matches the DAOs and the given back-end implementation. A transaction object for a database back-end implementation using JDBC may for example contain an SQL Connection object.

So this situation resembles the DAO classes and their interfaces: a Java Interface to describe the transaction object is needed, and when DAO instances are generated by the factory class a Transaction instance should also be generated.

The following figure shows how the Transaction Handler uses the Transaction and DAO instances created by a factory class.


Figure 1: The relations between classes and interfaces

The Transaction Interface

It's well known that when working with JDBC transactions methods like commit and rollback are used. Since our interface must work on many different back-ends a new set of names would be appropriate, for example:

package dk.hansen

public interface TransactionIF {

  /* Start a transaction */
  public void begin() throws DAOException;

  /* Rollback a transaction */
  public void rollback() throws DAOException;

  /* End a transaction */
  public void end() throws DAOException;

}

The DAOException class is a simple extension of a Java Exception capable of handling chained exceptions.

With a given implementation of this Interface the code for a "create" method in the Transaction Handler will look like this:

package dk.hansen;

import org.apache.log4j.Logger;

public class TransactionHandler {

  private static Logger logger = Logger.getLogger(TransactionHandler.class.getName());

  public void create(...) throws DAOException {
    TransactionIF trans = new MyTransaction();
    /* Start a transaction */ 
    try {
      trans.begin();
    } catch (DAOException e) {
      String msg = "'begin transaction' failed.";
      logger.info(msg);
      throw new DAOException(msg, e);
    }

    try {
      // Calls to DAOs here...
      someDao.setTransaction(trans);
      ...
    } catch (DAOException e) {
      try {
        trans.rollback();
      } catch (DAOException e1) {
        logger.info("'rollback transaction' failed.");
      }
      String msg = "create failed.";
      logger.info(msg);
      throw new DAOException(msg, e);
    }

    /* Stop the transaction */
    try {
      trans.end();
    } catch (DAOException e) {
      String msg = "'end transaction' failed.";
      logger.info(msg);
      throw new DAOException(msg, e);
    }
  }

}

Note, that the TransactionHandler

  1. creates an instance of the Transaction class
  2. uses the begin, end, rollback methods
  3. passes the instance to the DAOs

It's evident that the TransactionHandler knows nothing about how transactions are implemented. He simply follows the conventions that have been set up.

To see if this setup actually works a "real" implementation of the TransactionIF interface, for example using JDBC transactions, must be made.

A JDBC implementation of the Transaction interface

To work with JDBC transactions it's necessary to have access to a Connection object. The challenge is therefore to let the Transaction object and the DAOs share Connections. As just mentioned this is a job for the Transaction Handler, which will work as follows:

  1. Get DAO instances through a DAO factory class
  2. Get a Transaction instance containing Connections through the same factory class
  3. Pass the transaction instance to the DAOs which may get Connections through a getter method
  4. Invoke the begin method, the DAO methods, end/rollback methods to form business transactions   

To match the setup from my first article, a DataSource object will be used to produce the Connections. The DataSource object is produced by the DAO/Transaction factory classes, so we will simply let the factory class place the DataSource instance in the Transaction object. The Transaction Interface will therefore be expanded for JDBC transactions to look like this: 

package dk.hansen;

import java.sql.Connection;
import javax.sql.DataSource;

public interface JDBCTransactionIF extends TransactionIF {

  // Get a JDBC connection
  public Connection getConnection() throws DAOException; 

  // Set a datasource 
  public void setDataSource(DataSource ds);
}

A complete JDBCTransaction class implementing this interface can be viewed here. Some of the important parts of the class are these:

In order for this mechanism to work a DAO may only use Connections from the Transaction object. Whether a transaction is set up for several DAO methods is the responsibility of the Transaction Handler, which simply has to use the begin, end and rollback methods to start and stop a transaction.  

The Transaction Handler only has to use the begin, end and rollback methods for a real transaction. For non-transactional requests, like a database look-up (SQL SELECT), he should only set the Transaction instance on the DAO that he calls.    

The final Transaction Handler class

As an example I'll show how to implement a "rename" function in the Transaction Handler. The code uses classes from "Using DAOs in Apache Struts", and one should note that the DVDManager class is a DAO. The specs for the rename function is to rename a DVD's id by appending "-2005" to it. Since the id is the key in the database this function must be implemented by a delete operation followed by a create. These operations must be part of a transaction to keep the database in a well defined state.

The specs for the rename function is this:

public void rename(String id, String append) throws DAOException    

When the Transaction Handler starts it should first create the DAO and Transaction instances:

private TransactionIF trans;
private DVDManagerIF dao;

public TransactionHandler() throws DAOException {
  DVDManagerFactoryIF factory = new JdbcDVDManagerFactory();
  trans = factory.createTransaction();
  dao   = factory.createDVDManager();
}

 The rename method may then be coded like this (the important parts are bolded):

/**
* Rename a DVD by id by appending a text to its id 
* @param id The id to look for
* @param append The text to append
* @throws DAOException
*/
public void rename(String id, String append) throws DAOException {
  dao.setTransaction(trans);
  DVD dvd = dao.getDVD(id);
  if (dvd == null) throw new DAOException("DVD " + id + " not found");
 
  try {
    trans.begin();
  } catch (DAOException e) {
    String msg = "'begin transaction' failed.";
    logger.info(msg);
    throw new DAOException(msg, e);
  }

  try {
    dao.deleteDVD(id);
    dao.createDVD(id + append, dvd.getTitle());
  } catch (DAOException e) {
    try {
      trans.rollback();
      logger.info("'rollback' successful.");
    } catch (DAOException e1) {
      logger.info("'rollback transaction' failed", e1);
    }
    String msg = "rename failed.";
    logger.info(msg);
    throw new DAOException(msg, e);
  }

  try {
    trans.end();
  } catch (DAOException e) {
    String msg = "'end transaction' failed.";
    logger.info(msg);
    throw new DAOException(msg, e);
  }
}

Note that the initial call to getDVD may be held outside the transaction. Still, the transaction instance must be given to the DAO so it can obtain its Connection object.

When everything runs fine the following is written to the console:

dk.hansen.JdbcDVDManagerFactory - JdbcDVDManagerFactory. JDBC driver loaded
dk.hansen.JdbcDVDManagerFactory - JdbcDVDManagerFactory. Datasource loaded
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - New Connection created
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@1cb25f1 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.getDVD, id=ID1
dk.hansen.JDBCTransaction - Begin transaction
dk.hansen.JDBCTransaction - New Connection created
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@503429 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.getDVD, id=ID1
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@503429 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.deleteDVD, id=ID1
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@503429 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.getDVD, id=ID1-2005
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@503429 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.createDVD, id=ID1-2005
dk.hansen.JDBCTransaction - End transaction
dk.hansen.JDBCTransaction - Close connection

Note how the first getDVD call (outside the transaction) obtains a connection with address "1cb25f1", and the following method calls (inside the transaction) all get the same connection at address "503429 ".

If something goes wrong rollback will be called:

. . .
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@503429 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.deleteDVD, id=ID1
dk.hansen.JDBCTransaction - Get a connection
dk.hansen.JDBCTransaction - Connection com.mysql.jdbc.Connection@503429 returned
dk.hansen.DatabaseDVDManager - DatabaseDVDManager.getDVD, id=ID1-2005
dk.hansen.JDBCTransaction - Rollback transaction
dk.hansen.JDBCTransaction - Close connection
dk.hansen.TransactionHandler - 'rollback' successful.
dk.hansen.TransactionHandler - rename failed.

dk.hansen.DAOException: rename failed.
at dk.hansen.TransactionHandler.rename(TransactionHandler.java:86)
at dk.hansen.TransactionHandler.main(TransactionHandler.java:102)
Caused by: dk.hansen.DAOException: Id ID1-2005 is already used
at dk.hansen.DatabaseDVDManager.createDVD(DatabaseDVDManager.java:43)
at dk.hansen.TransactionHandler.rename(TransactionHandler.java:76)
... 1 more
Exception in thread "main" 

Testing the Transaction Handler

JUnit is the obvious choice when testing that the transaction handling works as intended. Here's some code that will try to insert the same DVD twice. This will result in an error and will force a rollback. The test is to see that the first--correctly inserted--DVD has been rolled back and is no longer in the database:

public void testTransaction4() throws DAOException {
  trans.begin();

  String id = "ID3";
  String title = "Troy";
  manager.createDVD(id, title);

  // Check for correct insert
  DVD dvd = manager.getDVD(id);
  assertNotNull("Test for ID3", dvd);

  // Try a double insert
  try {
    manager.createDVD(id, title);
    fail("Double create must throw an exception");
  } catch (DAOException e) {
    trans.rollback();
  }

  // Check that the inserted record has been rolled back
  dvd = manager.getDVD(id);
  assertNull("Test for ID3", dvd);
}

If the test is successful the JUnit will show a "green bar".

Conclusion

The article has shown how to use a Transaction object to manage the DAO transactions in a simple and back-end neutral way. There is a set of conventions that have to be followed: the business logic must call the begin/end/rollback methods on the Transaction object, but that's all. By using interfaces to define the DAOs and the Transaction object it's a simple thing to replace the back-end system. This is especially useful during test, since this allows you to use a mock-up of the back-end.

Happy coding!

Other resources