Programmatic Transactions
手动事务
本章里,我们将重用前面章节里使用过的数据库,作为提醒,在图7-2中再次显示了相关数据模型。
在服务组件里,你可以通过特性(Attribute)来使用事务。在研究这些特性之前,让我们先看一下如何以编程的方式实现事务,这样你可以更容易地对比这些技术。
如果使用SQL Server的.NET数据提供程序,你可以用SqlTransaction这个类来实现事务。只需在SqlConnection上调用BeginTransaction方法,返回一个SqlTransaction类的对象,这样就创建了一个新的事务。SqlTransaction类的Commit和Rollback方法可以用于完成或者回滚事务。
让我们以CourseData类为例来看一下。首先一个连接字符串被传入到这个类的构造函数,如代码段7-1所示。如果有必要的话,这个类的调用者可以从配置文件中读取连接字符串。为了避免每次方法调用都创建新的SqlCommand对象,我们在构造函数中生成一个新的SqlCommand对象,接着为这个命令对象添加查询参数,这些参数用于传递新的课程记录的数据。

图7-2 示例数据库
代码段7-1 手动事务
using System;
using System.Data;
using System.Data.SqlClient;
namespace Samples.Courses.Data
{
public class CourseData
{
private string connectionString;
private SqlCommand insertCourseCommand;
public CourseData(string connectionString)
{
this.connectionString = connectionString;
insertCourseCommand = new SqlCommand();
insertCourseCommand.CommandText = "INSERT INTO " +
"Courses (CourseId, Number, Title, Active) " +
"VALUES(@CourseId, @Number, @Title, @Active)";
insertCourseCommand.Parameters.Add("@CourseId",
SqlDbType.UniqueIdentifier);
insertCourseCommand.Parameters.Add("@Number",
SqlDbType.NChar, 10);
insertCourseCommand.Parameters.Add("@Title",
SqlDbType.NVarChar, 50);
insertCourseCommand.Parameters.Add("@Active",
SqlDbType.Bit);
}
在代码段7-2中展示了AddCourse方法:先生成一个连接对象,把它和前面建立的命令对象相关联,然后打开连接。在一个try语句块内,通过ExecuteNonQuery方法来执行INSERT语句。在这之前,一个新的事务已通过connection.BeginTransaction方法被创建,返回的事务对象自动和这个连接相关联。如果数据库插入操作成功,这个事务就会被完成。如果在插入时由于一些错误ExecuteNonQuery方法抛出异常,则在catch语句块内,捕获该异常,然后执行回滚操作。
代码段7-2 使用手动事务的AddCourse方法
public void AddCourse(Course course)
{
int customerId = -1;
SqlConnection connection =
new SqlConnection(connectionString);
insertCourseCommand.Connection = connection;
insertCourseCommand.Parameters["@CourseId"].Value =
course.CourseId;
insertCourseCommand.Parameters["@Number"].Value =
course.Number;
insertCourseCommand.Parameters["@Title"].Value =
course.Title;
insertCourseCommand.Parameters["@Active"].Value =
course.Active;
connection.Open();
try
{
insertCourseCommand.Transaction =
connection.BeginTransaction();
insertCourseCommand.ExecuteNonQuery();
insertCourseCommand.Transaction.Commit();
}
catch
{
insertCourseCommand.Transaction.Rollback();
throw;
}
finally
{
connection.Close();
}
}
在客户端测试程序的Test类中(见代码段7-3),我们用CourseData类创建一个新的课程,而被传递到其构造函数的数据库连接字符串是用ConfigurationSettings类读取配置文件而得到的。
代码段7-3 客户端测试程序
using System;
using System.Configuration;
using Samples.Courses.Data;
using Samples.Courses.Entities;
class Test
{
static void Main()
{
// read the connection string from the configuration file
string connectionString =
ConfigurationSettings.AppSettings["Database"];
// write a new course to the database
CourseData db = new CourseData(connectionString);
Course c = new Course();
c.Number = "MS-2389";
c.Title = "Programming ADO.NET";
c.Active = true;
db.AddCourse(c);
}
}
代码段7-4展示了被用于测试程序的配置文件。在配置文件里,数据库连接字符串被定义在<appSettings>节点下的一个子节点中。
代码段7-4 用于定义连接字符串的应用程序配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="Database"
value="server=localhost;database=Courses;
trusted_connection=true" />
</appSettings>
</configuration>
如前面的例子所示,编写使用手动事务的程序并不难。然而,如果一个事务由多个SQL命令组成时又怎么办呢?如果这些查询都是前后相连的,那么把它们都放到同一个try语句块内就能轻松解决问题。但是,在一个事务需要贯穿于多个组件时,情况就不那么简单了。
如果多个不同的类需要参与同一个事务(譬如,要添加一个客户,并且同时一个课程条目也得完成),处理这种事务就不像前面那么容易了。看一下,该如何来处理该类情形。
对于图7-3显示的场景,在同一个事务下操作是有必要的:如果一个学生想要改变她的课程表,她必须从一个课程名单中移除然后添加到另外一个里去。那么下面这些情况都不允许发生: 她被添加到了一个新的课程名单中,但未能被从原来的课程名单中移除,或者她被从一个课程中移除,却未能被添加到另外一个新的课程中。从课程名单中同时添加和移除的操作必须是同一个事务的组成部分。

图7-3 多个组件间的事务
对应该参与同一个事务的,横跨多个对象类的多个数据库操作,你可以通过将跟这个事务有关的连接对象作为参数传到各个类的相干方法里的方法来实现。在这些方法内部,可以用同一个数据库连接对象来参与同一个事务。
当涉及很多类以及更大的场景下时,这就会变得非常复杂。你必须决定一个类是否需要参与同一个事务,或者是否需要创建一个新的事务。如果一个对象在不使用数据库的方法中被调用的情况又该怎么办呢?使用上面这种手法,似乎需要添加一个额外参数用于传递数据库连接对象。
企业服务提供了一种途径来解决这种复杂性:自动事务。






