我们可以用业界公认的最佳方式来建立一种数据访问机制。Martin Fowler的著作《Patterns of Enterprise Application Architecture》讲到了许多在实际应用中非常成功的数据访问方法。其一便是在应用程序架构中引入一个数据访问层。数据访问层不仅要考虑到与不同的数据库和数据访问技术的兼容性,而且职责要严格限制在数据访问功能上。数据访问层应该只包含数据访问对象(DAO)以及“创建/获取/更新/删除”(CRUD,Create/Retrieve/Update/Delete)的操作,不应该涉及任何业务逻辑。业务逻辑应该位于单独的业务服务层,并且需要与一或多个DAO协作来完成高层次的用户功能。
为了在事务中“要么全执行要么全不执行”这些用户功能,事务环境(transaction context)就应该由业务服务层(或某个“更高”的层次)控制。在实现上,一个很重要的细节是如何让DAO了解在其它层次中开始的“外部”事务。如果让DAO自己负责连接和事务的管理,就把问题看的过于简单化了,因为此时每个DAO都会执行自己的事务/资源管理,所以无法在同一事务中执行多个DAO操作。我们需要一种有效的手段,将连接/事务成对的从业务服务层传递给DAO。方法有很多种,最不具侵入性的就是将连接/事务作为方法参数显式的传递给DAO。另一种方法是将连接/事务放在线程本地存储内。不管使用哪种方法,只要在用ADO.NET,就必须得自己创建一个基础框架来完成这个任务。
但是,等一下,企业服务不是能解决这个问题吗——还有System.Transactions命名空间呢?关于这个嘛,答案是对...也不对。企业服务确实能够让我们在事务环境中使用“原生”的ADO.NET在同一事务中执行多个DAO操作。但它的缺点是必须通过MS-DTC(Microsoft Distributed Transaction Coordinator)使用分布式(全局的)事务。如果只为了使用全局事务就必须依赖MS-DTC,那应用程序在性能上就会大打折扣了。
使用.NET2.0新增System.Transactions命名空间下的TransactonScope类时,也有相同的问题。TransactonScope类的目的实际上是——用using语句使一段代码成为事务性代码。只要访问事务性的资源,using语句中的普通ADO.NET代码就会在一个ADO.NET本地事务中运行。但是,System.Transactions(和数据库)的“神奇“之处在于,如果需要访问第二个事务性资源,本地事务就会升级为分布式事务。这个过程叫做PSPE(Promotable Single Phase Enlistment)。此外,还需提醒读者:在同一个数据库上使用同一连接字符串打开第二个连接,就会使本地事务升级为分布式事务。所以,如果每个DAO都执行自己的连接管理,那就完了:本地事务会突然升级为分布式事务!如果应用程序只使用一个数据库,要想避免这个问题,就必须将唯一的连接对象传递给系统中的所有DAO。另外要注意的是某些数据库不支持PSPE,就算用单个数据库连接也会使用分布式事务(比如Oracle)。
Spring.NET的声明式事务管理功能非常强大。在数据库事务领域,讨论声明式事务管理的话题并不是很多,因为现在开发人员已经可以不再直接用凌乱繁复的事务API来进行事务管理了,而是可以通过在类和方法上应用某些特性来进行。但是在FCL中,只有企业服务提供了这一功能。Spring.NET则填补了这个空白——不管使用哪种事务管理技术:ADO.NET,还是(最常用的)System.Transactions,都可以使用声明式的事务管理。另外,企业服务也有自己的问题,举例来说,如果需要为查询/读取操作和创建/更新/删除操作设置不同的隔离等级,就必须将这两组操作分隔在不同的类中实现,因为在企业服务中,声明式事务的元数据只对类有效。但总的来说,企业服务,特别是在XP sp2和Server 2003中新出现的“无组件服务”,还有在应用程序进程内驻留的功能都是相当不错的。不过,虽然有这些优点,企业服务仍尚未在开发社区中引起很大的关注。
Spring.NET事务管理的宗旨,就是要减轻FCL或第三方数据访问技术给开发人员带来的这些“痛苦”。它支持声明式事务管理,可以用配置方式获取事务选项的元数据——目前支持两种声明方式:在代码中用.NET特性声明;在IoC容器中用XML声明。
最后,Spring.NET事务管理还允许在同一事务中使用不同的数据访问技术——例如混合使用ADO.NET和NHibernate。
好了,再讲估计就有点烦人了,现在我们来看代码。
在Spring.NET中,提供了以下实现类:
AdoPlatformTransactionManager- 基于本地ADO.NET的事务。
ServiceDomainPlatformTransactionManager- 由企业服务提供的分布式事务管理器。
TxScopePlatformTransactionManager- 由System.Transactions提供的本地/分布式的事务管理器。
ITransactionDefinition接口封装了以下信息:
Isolation:该事务对其它事务操作的隔离级别。例如,用来表示某个事务是否能看到其它事务写入的、但尚未提交的信息。
Propagation:一般情况下,TransactionScope内的代码都会在为其指定的事务中运行。但是,该属性可用来设置如果某个事务环境已经存在时,该事务内的方法是否要执行:比如说,是简单的让它在现有事务中继续运行呢(这是一般情况),还是挂起现有事务然后创建一个新事务来运行。
Timeout:在超时(并且被事务基础框架自动回滚之前)前该事务可以运行多久。
Read-only状态:只读的事务不会修改任何数据。在某些情况下(比如使用NHibernate时),只读事务能显著提高性能。
让我们从实现代码中学习Spring.NET事务管理的机制。
准备条件:数据中建了2张表,如图1
UserTable为存储用户信息的表,AccountTable为存储用户账号的信息。
数据库访问层:
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
public interface IAccountDao
{
void Create(string name, string userName);
void Delete(string userName);
}
public class AccountDao : AdoDaoSupport, IAccountDao
{
public void Create(string name, string userName)
{
AdoTemplate.ExecuteNonQuery(CommandType.Text,
String.Format("INSERT INTO AccountTable (UserName, AccountName) VALUES ('{0}', '{1}')", userName, name));
}
public void Delete(string userName)
{
AdoTemplate.ExecuteNonQuery(CommandType.Text,
String.Format("DELETE FROM AccountTable WHERE UserName = '{0}'", userName));
}
}
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
public interface IUserDao
{
void Create(string name, int age);
void Delete(string name);
DataSet Get(string name);
}
public class UserDao : AdoDaoSupport, IUserDao
{
public void Create(string name, int age)
{
AdoTemplate.ExecuteNonQuery(CommandType.Text,
string.Format("INSERT INTO UserTable (UserName, UserAge) VALUES ('{0}', {1})", name, age));
}
public void Delete(string name)
{
AdoTemplate.ExecuteNonQuery(CommandType.Text,
string.Format("DELETE FROM UserTable WHERE UserName = '{0}'", name));
}
public DataSet Get(string name)
{
return AdoTemplate.DataSetCreate(CommandType.Text,
string.Format("SELECT * FROM UserTable WHERE UserName = '{0}'", name));
}
}
业务处理层:当我们插入用户信息后,需要为这个用户建立一个账号,当我们删除用户时,需要将帐号信息一起删除。这里我们就会用到了事务。
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
public interface IUserService
{
void SaveData(string name, int age, string accountName);
void DeleteData(string name);
DataSet Get(string name);
}
public class UserService : IUserService
{
public IUserDao UserDao { get; set; }
public IAccountDao AccountDao { get; set; }
[Transaction]
public void SaveData(string name, int age, string accountName)
{
UserDao.Create(name, age);
AccountDao.Create(accountName, name);
}
[Transaction]
public void DeleteData(string name)
{
UserDao.Delete(name);
AccountDao.Delete(name);
}
[Transaction(ReadOnly = true)]
public DataSet Get(string name)
{
return UserDao.Get(name);
}
}
配置:
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net"
xmlns:db="http://www.springframework.net/database"
xmlns:tx="http://www.springframework.net/tx">
<db:provider id="DbProvider"
provider="SqlServer-1.1"
connectionString="Server=(local);Database=SpringLesson16;Uid=sa;Pwd=;Trusted_Connection=False"/>
<object id="userDao" type="Dao.UserDao, Dao">
<property name="AdoTemplate" ref="adoTemplate"/>
object>
<object id="accountDao" type="Dao.AccountDao, Dao">
<property name="AdoTemplate" ref="adoTemplate"/>
object>
<object id="userService" type="Service.UserService, Service">
<property name="UserDao" ref="userDao"/>
<property name="AccountDao" ref="accountDao"/>
object>
<object id="adoTemplate" type="Spring.Data.Core.AdoTemplate, Spring.Data">
<property name="DbProvider" ref="DbProvider"/>
<property name="DataReaderWrapperType" value="Spring.Data.Support.NullMappingDataReader, Spring.Data"/>
object>
<object id="transactionManager"
type="Spring.Data.Core.AdoPlatformTransactionManager, Spring.Data">
<property name="DbProvider" ref="DbProvider"/>
object>
<tx:attribute-driven/>
objects>
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>
<section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core"/>
sectionGroup>
configSections>
<spring>
<parsers>
<parser type="Spring.Data.Config.DatabaseNamespaceParser, Spring.Data"/>
<parser type="Spring.Transaction.Config.TxNamespaceParser, Spring.Data"/>
parsers>
<context>
<resource uri="assembly://Dao/Dao/Dao.xml"/>
context>
spring>
configuration>
我们新建一个单元测试:
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
[TestFixture]
public class TransactionTest
{
[Test]
public void AdoTransaction()
{
IApplicationContext ctx = ContextRegistry.GetContext();
IUserService service = (IUserService)ctx.GetObject("userService");
service.SaveData("刘冬", 26, "1233456");
}
}
我们修改一下UserService的DeleteData方法,在调用 UserDao.Delete(name)以后抛出异常,测试数据是否回滚。
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
[Transaction]
public void DeleteData(string name)
{
UserDao.Delete(name);
new Exception("测试数据是否回滚");
AccountDao.Delete(name);
}
如果数据没有回滚,则我们得到的数据为UserTable表中没有数据,AccountTable表中有数据。
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
public class TransactionTest
{
[Test]
public void AdoTransaction()
{
IApplicationContext ctx = ContextRegistry.GetContext();
IUserService service = (IUserService)ctx.GetObject("userService");
service.DeleteData("刘冬");
}
}
我们发现数据已经回滚。
Spring.NET框架帮我们很好的管理了事务。但在我们的编码过程中经常使用到try...catch语句。要是在DeleteData方法中加入try...catch语句会回滚吗?我们修改一下UserService的SaveData方法
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
[Transaction]
public void SaveData(string name, int age, string accountName)
{
try
{
UserDao.Create(name, age);
AccountDao.Create(accountName, name);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
当前的数据库AccountTable表中已经存在字段为AccountName的“123456”记录。因为AccountName字段是主键,我再插入一条数据就会出现异常。
![ContractedBlock.gif](http://img.e-com-net.com/image/info8/70c152915daf47829a33744d52567c74.gif)
![ExpandedBlockStart.gif](http://img.e-com-net.com/image/info8/a851f6ebab08498cbe1d2f2c2b23a0c9.gif)
[Test]
public void AdoTransaction()
{
IApplicationContext ctx = ContextRegistry.GetContext();
IUserService service = (IUserService)ctx.GetObject("userService");
service.SaveData("刘冬冬", 27, "1233456");
}
我们发现在使用try...catch语句以后并没有回滚。从以上的效果中,我们可以得出结论:使用try...catch的异常叫作“运行期异常”,Spring.NET的事务管理默认是不对运行期异常回滚的。
如果要实现没有try...catch语句不回滚事务,我们需要在方法上标记[Transaction(NoRollbackFor = new Type[] { typeof(Exception) })]。NoRollbackFor 属性是Type数组,意思是运到Exception异常就不回滚,您也可以在增加其它的异常条件,如typeof(ArithmeticException)。这样,当遇到标有包含NoRollbackFor 属性的异常时,就不进行回滚。
代码下载
参考Spring.NET中文手册
返回目录