Transaction Objects provide a clearly separated interface for transactions in the context of database access layers.
You applied the Relational Database Access Layer Framework. You need to define an interface for the start, commit and abort of transactions.
What will the interface for transactions look like?
Separation of concerns: An interface for transactions should separate transaction specific concerns from other concerns in the database access layer. The interface should be easy to use and easy to create. Collaboration with the exception handling mechanism is also necessary. Transactions must abort, whenever an unexpected exception arises and is not treated properly.
Use a transaction object. Have it rollback the transaction in its destructor so that open transactions are aborted in any case if not committed explicitly.
The Transaction is responsible for flushing the ViewCache when a transaction is started. During the transaction the ViewCache is filled with View objects. When a transaction is committed, the Transaction object issues a writeAndFlush() command to the ViewCache.
As an example we will show the lifecycle of a transaction object that is created by a main program. The locking strategy used is optimistic. The scenario is given by the following piece of user code.
void main ( void ) { try { Transaction trans; trans.start(); Order::printInvoice("47613","invoice.txt"); trans.commit(); } catch (...) {
// trans
is automaticaly destroyed here, resulting in an abort of the transaction } }; |
This results in the below interaction diagram.
Encapsulation: The Transaction objects allows clean encapsulation of transaction behavior at the cost of a relatively simple class. The use of a class also ensures that instances are destroyed at the end of a block. Transactions cannot be left open by negligence or in case of an unexpected exception.
The transaction object should be implemented as a Singleton [GOF95] as relational databases will usually not allow you to use nested transactions. Use the Singleton to prevent users from creating more than one (nested) transaction at a time.
You should also control the protocol of a transaction (no commit before a transaction is started) by using an internal state machine. The State Pattern [GOF95, Dys+96] may be used to avoid conditional code in Transaction objects due to dealing with internal states of the transaction like undefined, started, or aborted.
Consider using the Strategy Pattern [GOF95] if you want to give the APIs user the option to choose a locking policy for each transaction.
The interaction with the database is dependent on the locking policy used. In case of optimistic locking, the database transaction will be started just before the writeAndFlush() command is issued to the ViewCache. If the designer considers pessimistic locking more appropriate, the database transaction is started right after the ViewCache::flush() command resulting in a longer span of the transaction. Usually you will implement only one locking policy for an application. If you are building a reusable framework you should think of parameterizing the locking policy.
Brown and Whitenack describe a similar mechanism, called Transactions as Blocks, for Smalltalk [Bro+96].
Fowler has another version on the web and calls it the Unit of Work pattern. Beware that transactions that need to register objects are not very slick but explicit object registration to transaction objects can be found in quite a few access layers.
Optimistic Locking is a method often needed below transaction objects
The interface shown here can also be found in the ODMG C++ language binding for object databases [ODMG96].