The Transaction Quickstart demonstrates Spring's transaction management features. The database schema are two simple tables, credit and debit, which contain an Identifier and an Amount. The quick start shows the use of declarative transactions using attributes and also the ability to change the transaction manager (local or distributed) via changes to only the configuration files - no code changes are required.
The design of the application is very simple and consists of two
logical layers, a business service layer in the namespace
Spring.TxQuickStart.Services and a DAO layer in the
namespace Spring.TxQuickStart.Dao. As this is just a
toy example the business service layer does nothing more than call two DAO
objects. The business service is to transfer money in a bank account and
is blatantly taken from the book Pro
ADO.NET by Sahil Malik. The transfer service is defined by the
interface IAccountManager with the
implementation AccountManager located in the
namespace Spring.TxQuickStart.Services. The money
is 'contained' in a credit table and a debit table in the database. The
SQL Server schema for the tables is located in the file
CreditsDebitsSchema.sql. Transferring the money requires an ACID operation
on these two tables. The credit operation is defined via a
IAccountCreditDao interface and the debit
operation via an IAccountDebitDao
interface. Implementations based on AdoTemplate are
in the namespace Spring.TxQuickStart.Dao.Ado. Note that
Spring's transaction management framework allows the mixing of data access
technologies within the same transaction, i.e. ORM and ADO.NET. A
demonstration of this features will be added to this quick start in a
future release.
The Manager and DAO interfaces are shown below
public interface IAccountManager
{
void DoTransfer(float creditAmount, float debitAmount);
}
public interface IAccountCreditDao
{
void CreateCredit(float creditAmount);
}
public interface IAccountDebitDao
{
void DebitAccount(float debitAmount);
}The implementation of the Account Credit DAO is shown below
public class AccountCreditDao : AdoDaoSupport, IAccountCreditDao
{
public void CreateCredit(float creditAmount)
{
AdoTemplate.ExecuteNonQuery(CommandType.Text,
String.Format("insert into Credits (CreditAmount) VALUES ({0})",
creditAmount));
}
}and for the Debit DAO
public class AccountDebitDao : AdoDaoSupport, IAccountDebitDao
{
public void DebitAccount(float debitAmount)
{
AdoTemplate.ExecuteNonQuery(CommandType.Text,
String.Format("insert into dbo.Debits (DebitAmount) VALUES ({0})",
debitAmount));
}
}Both of these DAO implementations inherit from Spring's AdoDaoSupport class that provides convenient access to an AdoTemplate for performing data access operations. With no other properties that can be configured in these implementations, the only configuration required is setting of AdoDaoSupport's DbProvider property representing the connection to the database.
The implementation of the service layer class, IAccountManager, is show below with extraneous declarations of properties for the DAO objects and boolean 'throwException' property.
public class AccountManager : IAccountManager
{
// fields and properties for DAO objects and boolean throwException not shown
[Transaction]
public void DoTransfer(float creditAmount, float debitAmount)
{
creditDao.CreateCredit(creditAmount);
if (ThrowException)
{
throw new ArithmeticException("Couldn't do the math....");
}
debitDao.DebitAccount(debitAmount);
}
}The throw exception property, if true, will throw the shown exception and is used to demonstrate rollback behavior. Note the Transaction annotation above the method. This will be read by Spring and used to create a transactional proxy to AccountManager in order to perform declarative transaction management.
The driver for the program is an NUnit test. It is convenient to download a VS.NET add-in, such as TestDriven.NET or Resharper to be able to run the unit test from within VS.NET. The code for the unit test driver is shown below
[TestFixture]
public class AccountManagerTests
{
private IApplicationContext ctx;
[SetUp]
public void SetUp()
{
// Configure Spring programmatically
NamespaceParserRegistry.RegisterParser(typeof(DatabaseNamespaceParser));
NamespaceParserRegistry.RegisterParser(typeof(TxNamespaceParser));
NamespaceParserRegistry.RegisterParser(typeof(AopNamespaceParser));
string ctxName = "DTCAppContext.xml"; // for .NET 2.0
//string ctxName = "DTC1.1AppContext.xml"; // for .NET 1.1
ctx = new XmlApplicationContext(
"assembly://Spring.TxQuickStart.Tests/Spring.TxQuickStart/" + ctxName);
}
[Test]
public void DeclarativeWithAttributes()
{
IAccountManager mgr = ctx["accountManager"] as IAccountManager;
mgr.DoTransfer(217, 217);
}
}The essential element is to create an instance of Spring's application context where the relevant layers of the application are 'wired' together. The top level object for the purposes of this quick start is the account manager, which is retrieved from Spring's application context and the method DoTransfer is executed.
Note, future releases will instead have as the driver program a console application.
The configuration for application is shown below
<objects xmlns='http://www.springframework.net'
xmlns:db="http://www.springframework.net/database"
xmlns:tx="http://www.springframework.net/tx"
xmlns:aop="http://www.springframework.net/aop">
<!-- Database Providers -->
<db:provider id="DebitDbProvider"
provider="System.Data.SqlClient"
connectionString="Data Source=MARKT60\SQL2005;Initial Catalog=Debits;User ID=springqa; Password=springqa"/>
<db:provider id="CreditDbProvider"
provider="System.Data.SqlClient"
connectionString="Data Source=MARKT60\SQL2005;Initial Catalog=Credits;User ID=springqa; Password=springqa"/>
<!-- Transaction Manager if using two databases, one containing the credit table and the other a debit table -->
<object id="transactionManager"
type="Spring.Data.Core.TxScopeTransactionManager, Spring.Data">
</object>
<!-- DAO Layer -->
<object id="accountCreditDao" type="Spring.TxQuickStart.Dao.Ado.AccountCreditDao, Spring.TxQuickStart">
<property name="DbProvider" ref="CreditDbProvider"/>
</object>
<object id="accountDebitDao" type="Spring.TxQuickStart.Dao.Ado.AccountDebitDao, Spring.TxQuickStart">
<property name="DbProvider" ref="DebitDbProvider"/>
</object>
<!-- The service layer that performs multiple data access operations -->
<object id="accountManager"
type="Spring.TxQuickStart.Services.AccountManager, Spring.TxQuickStart">
<property name="AccountCreditDao" ref="accountCreditDao"/>
<property name="AccountDebitDao" ref="accountDebitDao"/>
<property name="ThrowException" value="false"/>
</object>
<!-- Enable declarative transaction management -->
<tx:attribute-driven/>
</objects>Moving from top to bottom in th configuration file, the database type and connection parameters are first specified for the two databases. The type of transaction manager is then selected, in this case we are showing the use of TxScopeTransactionManager that uses .NET 2.0 System.Transactions as the implementation, allowing for distributed transactions between the two different databases listed. The DAO layer is then configured with each DAO object referring to its appropriate database. The service layer then ties together the two DAO objects and configures the AccountManager not to throw exceptions. Lastly, declarative transaction management through the use of attributes is enable. In a larger application the different layers would typically be broken up into individual configuration files and imported into the main configuration file. This allows your configuration to mirror your architecture.
Running the tests will result in 217 being entered into the Credits and Debits table of each database. You should fire up SQL Server Management Studio or equivalent to verify this. It is also interesting to view the distributed transaction monitor that is part of the Component Services GUI. If you change the property ThrowException to true, and re-run the test, then you will not have the value 217 entered in either table.
If we need to switch from distributed transactions to local transactions with a single database, then all that needs to change is the configuration of the database providers as well as selecting a different transaction manager. The application code does not need to change. The configuration to use a single database is listed below
<db:provider id="DebitDbProvider"
provider="System.Data.SqlClient"
connectionString="Data Source=MARKT60\SQL2005;Initial Catalog=CreditsAndDebits;User ID=springqa; Password=springqa"/>
<db:provider id="CreditDbProvider"
provider="System.Data.SqlClient"
connectionString="Data Source=MARKT60\SQL2005;Initial Catalog=CreditsAndDebits;User ID=springqa; Password=springqa"/>
<object id="transactionManager"
type="Spring.Data.AdoPlatformTransactionManager, Spring.Data">
<property name="DbProvider" ref="DbProvider"/>
</object>Notice that the Initial Catalog value has changed.
Using Rollback rules you to specify which exceptions will not cause a rollback and instead only top execution flow, committing the work done up to the exception. An alternative implementation of AccountManager's DoTransfer method (included in the sample code) is shown below.
[Transaction(NoRollbackFor = new Type[] { typeof(ApplicationException) })]
public void DoTransfer(float creditAmount, float debitAmount)
{
creditDao.CreateCredit(creditAmount);
DoThrowException();
debitDao.DebitAccount(debitAmount);
}
public void DoThrowException()
{
throw new ApplicationException("Testing No Rollback 'Rule'");
}The expected behavior is that the credit table will be updated even though the exception is thrown. This is due to specifying that exceptions of the type ApplicationException should not rollback the database transaction. Running the test code below, verifies that the exception still propagates out of the method. Use SQL Server Management Studio or equivalent to verify the state of the Credit and Debit table.
[Test]
[ExpectedException(typeof(ApplicationException), "Testing No Rollback 'Rule'")]
public void DeclarativeWithAttributesNoRollbackFor()
{
IAccountManager mgr = ctx["accountManager"] as IAccountManager;
mgr.DoTransfer(314, 314);
}Transactional advice is just one type of advice that can be applied to the service layer. You can also configure other pieces of advice to be executed as part of the general advice chain that is associated with the program methods that have the Transaction attribute applied. In this example we will add logging of thrown exceptions using Spring's ExceptionHandlerAdvice. No code is required to be changed in order to have this additional functionality. Instead modify the configuration to the following
<tx:attribute-driven order="10"/>
<object name="exceptionAdvice" type="Spring.Aspects.Exceptions.ExceptionHandlerAdvice, Spring.Aop">
<property name="exceptionHandlers">
<list>
<value>on ArithmeticException log 'Logging an exception thrown from method ' + #method.Name </value>
</list>
</property>
</object>
<object id="txAttributePointcut" type="Spring.Aop.Support.AttributeMatchMethodPointcut, Spring.Aop">
<property name="Attribute" value="Spring.Transaction.Interceptor.TransactionAttribute, Spring.Data"/>
</object>
<aop:config>
<aop:advisor id="exceptionProcessAdvisor" order="1"
advice-ref="exceptionAdvice"
pointcut-ref="txAttributePointcut"/>
</aop:config>The transaction aspect is now additionally configured with an order
value of "10", which will place it after the execution of the exception
aspect, which is configured to use an order value of 1. The behavior for
logging the exception is specified by creating and configuring an instance
of
Spring.Aspects.Exceptions.ExceptionHandlerAdvice.
The location where that behavior is applied, the pointcut, is the
Transaction attribute. The aop configuration section on the bottom is what
ties together the behavior and where it will take place in the program
flow. Under the covers the transaction configuration,
<tx:attribute-driven/> creates similar advice and pointcut
definitions. Setting the ThrowException property of AccountManager to
true, you will see in the log file the message
LogExceptionHandler - Logging an exception thrown from method DoTransfer