Transactions QuickStartIntroductionThe 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.To follow this Quarts QuickStart load the solution file found in
the directory
<spring-install-dir>\examples\Spring\Spring.TxQuickStartApplication OverviewThe 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.InterfacesThe 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);
}ImplementationThe 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.Notice the Transaction attribute on the
DoTransfer method. This attribute can be read by
Spring and used to create a transactional proxy to AccountManager in
order to perform declarative transaction management.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.ConfigurationThe 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.Rollback RulesUsing 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);
}
}Adding additional AspectsTransactional 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 messagesINFO - 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 loggedINFO - 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