目前有很多种数据访问技术。在.NET FCL中,有三类API可以执行事务管理,分别是ADO.NET、企业服务和
System.Transactions。其它的数据访问技术,如对象关系映射(object relational mappers)和结果集映射(result-set mapping)等等的应用也很广泛,每种技术也都有自己的事务管理API。事务管理的代码一般是直接和各种事务API绑定在一起的,所以在开发时必须根据所用的具体技术来决定采用哪种API。但是,这种代码与事务API的紧耦合决定了很难通过简单的重构来解决更换数据访问技术的问题。而Spring.NET的事务框架允许在各种数据访问技术之上使用相同的API。通过配置或者集中的编程方式,可以很容易的更换后台事务API,而不需要对代码进行“大修”。
我们可以用业界公认的最佳方式来建立一种数据访问机制。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为存储用户账号的信息。
数据库访问层:
AccountDao
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));
}
}
UserDao
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));
}
}
业务处理层:当我们插入用户信息后,需要为这个用户建立一个账号,当我们删除用户时,需要将帐号信息一起删除。这里我们就会用到了事务。
UserService
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);
}
}
配置:
Dao.xml
<?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>
App.config
<?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>
我们新建一个单元测试:
TransactionTest
[TestFixture]
public class TransactionTest
{
[Test]
public void AdoTransaction()
{
IApplicationContext ctx = ContextRegistry.GetContext();
IUserService service = (IUserService)ctx.GetObject("userService");
service.SaveData("刘冬", 26, "1233456");
}
}
输出效果:两条数据已经插入数据库(图2)
图2
我们修改一下UserService的DeleteData方法,在调用 UserDao.Delete(name)以后抛出异常,测试数据是否回滚。
DeleteData
[Transaction]
public void DeleteData(string name)
{
UserDao.Delete(name);
new Exception("测试数据是否回滚");
AccountDao.Delete(name);
}
如果数据没有回滚,则我们得到的数据为UserTable表中没有数据,AccountTable表中有数据。
TransactionTest
public class TransactionTest
{
[Test]
public void AdoTransaction()
{
IApplicationContext ctx = ContextRegistry.GetContext();
IUserService service = (IUserService)ctx.GetObject("userService");
service.DeleteData("刘冬");
}
}
输出效果:图3
图3
我们发现数据已经回滚。
Spring.NET框架帮我们很好的管理了事务。但在我们的编码过程中经常使用到try...catch语句。要是在DeleteData方法中加入try...catch语句会回滚吗?我们修改一下UserService的SaveData方法
SaveData
[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字段是主键,我再插入一条数据就会出现异常。
AdoTransaction
[Test]
public void AdoTransaction()
{
IApplicationContext ctx = ContextRegistry.GetContext();
IUserService service = (IUserService)ctx.GetObject("userService");
service.SaveData("刘冬冬", 27, "1233456");
}
输出效果:图4
图4
我们发现在使用try...catch语句以后并没有回滚。从以上的效果中,我们可以得出结论:使用try...catch的异常叫作“运行期异常”,Spring.NET的事务管理默认是不对运行期异常回滚的。
如果要实现没有try...catch语句不回滚事务,我们需要在方法上标记[Transaction(NoRollbackFor = new Type[] { typeof(Exception) })]。NoRollbackFor 属性是Type数组,意思是运到Exception异常就不回滚,您也可以在增加其它的异常条件,如typeof(ArithmeticException)。这样,当遇到标有包含NoRollbackFor 属性的异常时,就不进行回滚。
代码下载
参考Spring.NET中文手册
返回目录