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. It also demonstrates some techniques for unit and integration testing an application as well as separating Spring's configuration files so that one is responsible for describing how the core business classes are configured and others that are responsible for the database environment and application of AOP.
This quickstart assumes you have installed a way to run NUnit tests within your IDE. Some excellent tools that let you do this are TestDriven.NET and ReSharper.
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 recorded in a credit and 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 of these interfaces using
AdoTemplate
are in the namespace
Spring.TxQuickStart.Dao.Ado.
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, "insert into Credits (CreditAmount) VALUES (@amount)", "amount", DbType.Decimal, 0, creditAmount); } }
and for the Debit DAO
public class AccountDebitDao : AdoDaoSupport, IAccountDebitDao { public void DebitAccount(float debitAmount) { AdoTemplate.ExecuteNonQuery(CommandType.Text, "insert into dbo.Debits (DebitAmount) VALUES (@amount)", "amount", DbType.Decimal, 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 interface,
IAccountManager
, is shown below.
public class AccountManager : IAccountManager { private IAccountCreditDao accountCreditDao; private IAccountDebitDao accountDebitDao; private float maxTransferAmount = 1000000; public AccountManager(IAccountCreditDao accountCreditDao, IAccountDebitDao accountDebitDao) { this.accountCreditDao = accountCreditDao; this.accountDebitDao = accountDebitDao; } public float MaxTransferAmount { get { return maxTransferAmount; } set { maxTransferAmount = value; } } [Transaction] public void DoTransfer(float creditAmount, float debitAmount) { accountCreditDao.CreateCredit(creditAmount); if (creditAmount > maxTransferAmount || debitAmount > maxTransferAmount) { throw new ArithmeticException("see a teller big spender..."); } accountDebitDao.DebitAccount(debitAmount); } }
The if statement is a poor-mans representation of business logic, namely that there is a policy that does not allow the use of this service for amounts larger than $1,000,000. If the credit or debit amount is larger than 1,000,000 then and exception will be thrown. We can write a unit test that will test for this business logic and provide stub implementations of the DAO objects so that our tests are not only independent of the database but will also execute very quickly.
Note | |
---|---|
Notice the Transaction attribute on the
|
The NUnit unit test for AccountManager is shown below
public class AccountManagerUnitTests { private IAccountManager accountManager; [SetUp] public void Setup() { IAccountCreditDao stubCreditDao = new StubAccountCreditDao(); IAccountDebitDao stubDebitDao = new StubAccountDebitDao(); accountManager = new AccountManager(stubCreditDao, stubDebitDao); } [Test] public void TransferBelowMaxAmount() { accountManager.DoTransfer(217, 217); } [Test] [ExpectedException(typeof(ArithmeticException))] public void TransferAboveMaxAmount() { accountManager.DoTransfer(2000000, 200000); } }
Running these tests we exercise both code pathways through the
method DoTransfer
. Nothing we have done so far is
Spring specific (aside from the presence of the [Transaction] attribute.
Now that we know the class works in isolation, we can now 'wire' up the
application for use in production by specifying how the service and DAO
layers are related. This configuration file is shown below and can loosely
be referred to as your 'application blueprint'. This configuration file is
named application-config.xml and is an embedded resource inside the 'main'
project, Spring.TxQuickStart.
<objects xmlns='http://www.springframework.net'> <!-- DAO Implementations --> <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 that performs multiple data access operations --> <object id="accountManager" type="Spring.TxQuickStart.Services.AccountManager, Spring.TxQuickStart"> <constructor-arg name="accountCreditDao" ref="accountCreditDao"/> <constructor-arg name="accountDebitDao" ref="accountDebitDao"/> </object> </objects>
This configuration is selecting the real ADO.NET implementations that will insert records into the database. We can now write a NUnit integration test that will test the service and DAO layers. To do this we add on configuration information specific to our test environment. This extra configuration information will determine what databases we speak to and what transaction manager (local or distribute) to use. The code for this integration style NUnit test is shown below
[TestFixture] public class AccountManagerTests { private AdoTemplate adoTemplateCredit; private AdoTemplate adoTemplateDebit; private IAccountManager accountManager; [SetUp] public void SetUp() { // Configure Spring programmatically NamespaceParserRegistry.RegisterParser(typeof(DatabaseNamespaceParser)); NamespaceParserRegistry.RegisterParser(typeof(TxNamespaceParser)); NamespaceParserRegistry.RegisterParser(typeof(AopNamespaceParser)); IApplicationContext context = new XmlApplicationContext( "assembly://Spring.TxQuickStart.Tests/Spring.TxQuickStart/system-test-local-config.xml" ); accountManager = context["accountManager"] as IAccountManager; CleanDb(context); } [Test] public void TransferBelowMaxAmount() { accountManager.DoTransfer(217, 217); int numCreditRecords = (int)adoTemplateCredit.ExecuteScalar(CommandType.Text, "select count(*) from Credits"); int numDebitRecords = (int)adoTemplateDebit.ExecuteScalar(CommandType.Text, "select count(*) from Debits"); Assert.AreEqual(1, numCreditRecords); Assert.AreEqual(1, numDebitRecords); } [Test] [ExpectedException(typeof(ArithmeticException))] public void TransferAboveMaxAmount() { accountManager.DoTransfer(2000000, 200000); } private void CleanDb(IApplicationContext context) { IDbProvider dbProvider = (IDbProvider)context["DebitDbProvider"]; adoTemplateDebit = new AdoTemplate(dbProvider); adoTemplateDebit.ExecuteNonQuery(CommandType.Text, "truncate table Debits"); dbProvider = (IDbProvider)context["CreditDbProvider"]; adoTemplateCredit = new AdoTemplate(dbProvider); adoTemplateCredit.ExecuteNonQuery(CommandType.Text, "truncate table Credits"); } }
The essential element is to create an instance of Spring's
application context where the relevant layers of the application are
'wired' together. The IAccountManager
implementation is retrieved from the IoC container and stored as a field
of the test class. The basic logic of the test is the same as in the unit
test but in addition there is the verification of actions performed in the
database. The set up method puts the database tables into a known state
before running the tests. Other techniques for performing integration
testing that can alleviate the need to do extensive database state
management for integration tests is described in the testing section.
The configuration file system-test-local-config.xml shown in the previous program listing includes application-config.xml and specifies the database to use and the local (not distributed) transaction manager AdoPlatformTransactionManager. This configuration file is shown below
<objects xmlns="http://www.springframework.net" xmlns:db="http://www.springframework.net/database" xmlns:tx="http://www.springframework.net/tx"> <!-- Imports application configuration --> <import resource="assembly://Spring.TxQuickStart/Spring.TxQuickStart/application-config.xml"/> <!-- Imports additional aspects --> <!-- <import resource="assembly://Spring.TxQuickStart.Tests/Spring.TxQuickStart/aspects-config.xml"/> --> <!-- Database Providers --> <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"/> <alias name="DebitDbProvider" alias="CreditDbProvider"/> <!-- Transaction Manager if using a single database that contain both credit and debit tables --> <object id="transactionManager" type="Spring.Data.Core.AdoPlatformTransactionManager, Spring.Data"> <property name="DbProvider" ref="DebitDbProvider"/> </object> <!-- Transaction aspect --> <tx:attribute-driven/> </objects>
Moving from top to bottom in the configuration file, the 'application-blueprint' configuration file is included. Then the database type and connection parameters are specified for the two databases. The names of these providers must match those specific in application-config.xml. Since the two names point to the same database, an alias configuration element is used to have them point to the same dbProvider under different names. The type of transaction manager is then selected, in this case we are showing the use of local transactions with AdoPlatformTransactionManager. Running the tests will result in 217 being entered into the Credits and Debits table of each database. You can fire up SQL Server Management Studio or equivalent to verify this.
To switch to a distributed transaction you can refer to the configuration file system-test-dtc-config.xml, which is shown below
<objects xmlns='http://www.springframework.net' xmlns:db="http://www.springframework.net/database" xmlns:tx="http://www.springframework.net/tx"> <!-- Imports application configuration --> <import resource="assembly://Spring.TxQuickStart/Spring.TxQuickStart/application-config.xml"/> <!-- Imports additional aspects --> <!-- <import resource="assembly://Spring.TxQuickStart.Tests/Spring.TxQuickStart/aspects-config.xml"/> --> <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> <!-- Transaction aspect --> <tx:attribute-driven/> </objects>
TxScopeTransactionManager uses .NET 2.0 System.Transactions as the implementation, allowing for distributed transactions between the two different databases listed. 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.
You can also use the configuration file system-test-dtc-es-config.xml that will use EnterpriseServices to perform transaction management.
Using Rollback rules allows you to specify which exceptions will not cause a rollback and instead only stop 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(ArithmeticException) })] public void DoTransfer(float creditAmount, float debitAmount) { accountCreditDao.CreateCredit(creditAmount); if (creditAmount > maxTransferAmount || debitAmount > maxTransferAmount) { throw new ArithmeticException("see a teller big spender..."); } accountDebitDao.DebitAccount(debitAmount); }
All that has changed is the use of the NoRollbackFor property on the transaction attribute.
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 ArithmethicException should not rollback the database transaction. Running the test code below verifies that the exception still propagates out of the method.
[Test] public void DeclarativeWithAttributesNoRollbackFor() { try { accountManager.DoTransfer(2000000, 2000000); Assert.Fail("Should have thrown Arithmetic Exception"); } catch (ArithmeticException) { int numCreditRecords = (int)adoTemplateCredit.ExecuteScalar(CommandType.Text, "select count(*) from Credits"); int numDebitRecords = (int)adoTemplateDebit.ExecuteScalar(CommandType.Text, "select count(*) from Debits"); Assert.AreEqual(1, numCreditRecords); Assert.AreEqual(0, numDebitRecords); } }
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 methods that have the Transaction attribute applied. In this example we will add logging of thrown exceptions using Spring's ExceptionHandlerAdvice as well as logging of the service layer method invocation. No code is required to be changed in order to have this additional functionality. Instead all you have to do is uncomment the line
<import resource="assembly://Spring.TxQuickStart.Tests/Spring.TxQuickStart/aspects-config.xml"/>
in either system-test-dtc-config.xml or system-test-local-config.xml The aspect configuration file is shown below
<objects xmlns='http://www.springframework.net' xmlns:aop="http://www.springframework.net/aop"> <object name="exceptionAdvice" type="Spring.Aspects.Exceptions.ExceptionHandlerAdvice, Spring.Aop"> <property name="exceptionHandlers"> <list> <value>on exception name ArithmeticException log 'Logging an exception thrown from method ' + #method.Name </value> </list> </property> </object> <object name="loggingAdvice" type="Spring.Aspects.Logging.SimpleLoggingAdvice, Spring.Aop"> <property name="logUniqueIdentifier" value="true"/> <property name="logExecutionTime" value="true"/> <property name="logMethodArguments" value="true"/> <property name="Separator" value=";"/> <property name="HideProxyTypeNames" value="true"/> <property name="UseDynamicLogger" value="true"/> <property name="LogLevel" value="Info"/> </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:advisor id="loggingAdvisor" order="2" advice-ref="loggingAdvice" pointcut-ref="txAttributePointcut"/> </aop:config> </objects>
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 logging of method arguments and execution time
is specified by configuring an instance of
Spring.Aspects.Logging.SimpleLoggingAdvice
.
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. Running the test TransferBelowMaxAmount will then log the following messages
INFO - Entering DoTransfer;45b6af04-b736-4efa-a489-45462726ddf2;creditAmount=217; debitAmount=217 INFO - Exiting DoTransfer;45b6af04-b736-4efa-a489-45462726ddf2;1328.125 ms;return=
When the test case of the test TransferAboveMaxAmount is run the following messages are logged
INFO - Entering DoTransfer;d94bc81b-a4ff-4ca1-9aaa-f2834f262307;creditAmount=2000000; debitAmount=200000 INFO - Exception thrown in DoTransferDoTransfer;d94bc81b-a4ff-4ca1-9aaa-f2834f262307;1140.625 System.ArithmeticException: see a teller big spender... at Spring.TxQuickStart.Services.AccountManager.DoTransfer(Single creditAmount, Single debitAmount) in L:\projects\Spring.Net\examples\Spring\Spring.TxQuickStart\src\Spring\Spring.TxQuickStart\TxQuickStart\Services\AccountManager.cs:line 36 at Spring.DynamicReflection.Method_DoTransfer_ec48557f22b149958fd2243413136600.Invoke(Object target, Object[] args) at Spring.Reflection.Dynamic.SafeMethod.Invoke(Object target, Object[] arguments) in l:\projects\Spring.Net\src\Spring\Spring.Core\Reflection\Dynamic\DynamicMethod.cs:line 108 at Spring.Aop.Framework.DynamicMethodInvocation.InvokeJoinpoint() in l:\projects\Spring.Net\src\Spring\Spring.Aop\Aop\Framework\DynamicMethodInvocation.cs:line 89 at Spring.Aop.Framework.AbstractMethodInvocation.Proceed() in l:\projects\Spring.Net\src\Spring\Spring.Aop\Aop\Framework\AbstractMethodInvocation.cs:line 257 at Spring.Transaction.Interceptor.TransactionInterceptor.Invoke(IMethodInvocation invocation) in l:\projects\Spring.Net\src\Spring\Spring.Data\Transaction\Interceptor\TransactionInterceptor.cs:line 80 at Spring.Aop.Framework.AbstractMethodInvocation.Proceed() in l:\projects\Spring.Net\src\Spring\Spring.Aop\Aop\Framework\AbstractMethodInvocation.cs:line 282 at Spring.Aspects.Logging.SimpleLoggingAdvice.InvokeUnderLog(IMethodInvocation invocation, ILog log) in l:\projects\Spring.Net\src\Spring\Spring.Aop\Aspects\Logging\SimpleLoggingAdvice.cs:line 185 TRACE - Logging an exception thrown from method DoTransfer