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 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.
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:
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
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
Transaction
classbegin
, end
, rollback
methods 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.
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 Connection
s. As just mentioned this is a job for the Transaction Handler,
which will work as follows:
Transaction
instance containing Connections
through the same factory
classConnections
through a getter methodTo match the setup from my first article, a DataSource
object will be used to
produce the Connection
s. 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:
begin
method disables automatic commits by: connection.setAutoCommit(false);
This will allow transactional processing to take place. getConnection
method is called (probably by a DAO)
an existing or a new Connection
instance is returned. In order for this mechanism to work a DAO may only use Connection
s
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
, en
d
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.
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"
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".
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!