本文英文原版及代码下载:
http://www.4guysfromrolla.com/webtech/080305-1.shtml
在SQL Server存储过程里管理Transactions
导言:
上周的文章《Maintaining Database Consistency with Transactions》里我们探讨了到底什么是transactions,以及当对多个数据表发出命令时,是如何确保数据库的数据一致性的.具体的说,transactions确保一系列modifying statement的“原子性”,也就是所有的步骤要么都成功要么都失败.Transactions确保原子数(atomicity)抵御这些威胁:当删除一条记录时因为一个外键约束而无法删除,或试图将一个string值插入到某个数据库表的一numeric列里——还比如一些灾难性的错误,如断电、硬盘驱动器发生碰撞等等。简单的说,当把多个modifying command封装到一个transaction里的时候,执行后的数据库状态将只会为如下2者之一:要么执行失败而回到执行命令之前的状态,要么所有的命令都执行成功——除此外没有中间状态(in-between' state).
典型的事务示例是在不同的帐户之间转账。该过程包括2个步骤:从一个帐户上扣除金额,然后添加到另一个帐户上.我们要避免的是完成了第一步后,因为比如断电等的原因而第二步执行失败.
除了探讨使用transaction的目的之外,上周的文章我们还探讨了如何用ADO.NET将多个modifying statements封装到一个transaction里面。具体来说,我们使用到了SqlTransaction 和 SqlConnection类。一个transaction是由调用SqlConnection类的BeginTransaction()开始,而由SqlTransaction对象的 Commit()或Rollback()来提交或回滚.
本文我们将继续探讨transaction,考察如何直接通过存储过程来创建、提交或回滚transaction.在考察使用transaction的T-SQL语法后,我们来探讨在什么时候我们应该选择直接在一个存储过程里使用transaction.
Stored Procedures点滴...
存储过程类似于一个计算机程序的method,可以接受一系列的输入参数,也可以返回数据.在一个存储过程里你可以写一些要执行的T-SQL statement.与ad-hoc SQL statement相比,存储过程有如下的优势:
.在软件层次体系里额外增加了一层来进行封装
.将程序代码与具体的数据相关的细节分隔开来
.增强了安全性,你可以拒绝对数据库里table的访问,而只允许执行web运行程序要用到的存储过程
.提升了性能,因为SQL server能将存储过程要执行的内容进行缓存,相反,如果是使用ad-hoc查询来执行的话每次使用的时候都要重新进行计算.
本文不打算对创建和使用一个存储过程,以及带来的好处进行深入探讨.对存储过程的更多探讨你可以参考Rob Howard的博客《Don't use stored procedures yet? Must be suffering from NIHS (Not Invented Here Syndrome)》,Nathan Pond的文章《Writing a Stored Procedure》详细地探讨了如何在SQL Server里创建存储过程,更多信息你可以查阅msdn的技术文档.
在一个存储过程里创建Transactions
在上篇文章《Maintaining Database Consistency with Transactions》里我们说过,当使用transaction时,一般来说你要使用如下的步骤:
1.明确指出你要开启一个transaction.包含的所有指令从那一刻起都作为逻辑的,原子操作的一部分.
2.发出指令——也就是transaction里包含的那些INSERT, UPDATE, 以及DELETE指令.
3.如果这些指令引发了一个错误,对transaction进行回滚。回滚的作用在于不执行这些指令.
4.如果一切顺利,则提交transaction.完成对数据库的操作.
我们知道在ADO.NET里,这些步骤是这样来完成的:在Try ... Catch语句里通过调用BeginTransaction()方法来调用transaction,如果发生了任何的与SQL相关的错误,我们就应该在Catch语句里通过调SqlTransaction对象的Rollback()方法来回滚transaction;如果没有错误发生的话,就通过Commit()方法来提交transaction.虽然在存储过程里开启、提交、回滚一个transaction与此稍有不同,但总体概念是一样的.
首先,我们需要宣称开始一个transaction,我们可以使用T-SQL命令 BEGIN TRANSACTION来轻松的实现.接下了,我们要发出transaction里封装的一系列的指令,比如INSERT, UPDATE, 和/或 DELETE.
如果你使用的Microsoft SQL Server早于2005版本,那么对每个modifying statement,你都需要检查是否发生了错误,如何是的话,你要对transaction进行回滚并引发一个异常.具体来说,你要检查@@ERROR的值,如果不为0的话,那就说明刚执行的那条指令引发了错误,在这种情况下,你需要调用关键字ROLLBACK来回滚transaction,然后使用RAISERROR来引发一个错误,RAISERROR将终止对存储过程的调用,并对调用的.NET程序引发一个异常.
如果你使用的是Microsoft SQL Server 2005以及更高版本,那么你可以使用TRY...CATCH语句就行了.本文探讨的是
在早于2005版本里的用法,关于对SQL Server 2005如何使用TRY...CATCH语句的问题,请参阅文章《TRY...CATCH in SQL Server 2005: An Easier Approach to Rolling Back Transactions in the Face of an Error》
最后,在存储过程的末尾,如果没有发生错误则提交该transaction,我们可以使用关键字COMMIT来实现.
示例...
下面的代码片段展示了使用一个存储过程来维护数据的一致性.具体来说,有2张相互关联的表Departments和 Employees,我们要用该存储过程来删除某个具体的department.由于每个Employees记录多有一个与Departments表相关联的外键约束。所以,要想从Departments表里删除一条记录的话,必须首先将Employees表里的相关记录删除掉.这是因为我们希望要么将某个部门以及所有的相关人员都删除掉,要么都不删除,所以我们将这2个DELETE statement封装在一个transaction里.
CREATE PROCEDURE DeleteDepartment
(
@DepartmentID int
)
AS
-- This sproc performs two DELETEs. First it deletes all of the
-- department's associated employees. Next, it deletes the department.
-- STEP 1: Start the transaction
BEGIN TRANSACTION
-- STEP 2 & 3: Issue the DELETE statements, checking @@ERROR after each statement
DELETE FROM Employees
WHERE DepartmentID = @DepartmentID
-- Rollback the transaction if there were any errors
IF @@ERROR <> 0
BEGIN
-- Rollback the transaction
ROLLBACK
-- Raise an error and return
RAISERROR ('Error in deleting employees in DeleteDepartment.', 16, 1)
RETURN
END
DELETE FROM Departments
WHERE DepartmentID = @DepartmentID
-- Rollback the transaction if there were any errors
IF @@ERROR <> 0
BEGIN
-- Rollback the transaction
ROLLBACK
-- Raise an error and return
RAISERROR ('Error in deleting department in DeleteDepartment.', 16, 1)
RETURN
END
-- STEP 4: If we reach this point, the commands completed successfully
-- Commit the transaction....
COMMIT
该RAISERROR方法将指定返回到客户端的错误信息,关于RAISERROR的更多信息请参阅技术文档.
事务管理: Stored Procedures or ADO.NET?
本文连同上周的那篇考察使用ADO.NET来管理事务的文章为我们提过了2种途径来为你的数据驱动程序使用事务提供支持.那么我们应该使用哪个方法呢?我自己2种都在用.对于简单的,在编译时很明确的批指令(batch statement),我倾向于使用存储过程.上面的例子就是极好的证明,当从一个数据表里删除记录时连同它的子记录一起删除掉.
不过在有些时候,我们发出了一系列的原子声明(atomic statement),但直到运行的时候才能确定到底该运行哪一个.或者有数量不等的存储过程,需要根据用户的选择来确定调用哪一个.例如,我们用一个CheckBoxList Web控件来列出所有部门的清单,用户自己选择要删除的部门.
在页面回传的时候,我们遍历CheckBoxList的Item项,再调用我们在上面探讨的那个DeleteDepartment存储过程.现在删除每个部门以及相关员工都当做一个事务来对待(因为我们在存储过程里BEGIN TRANSACTION了) ,但是我们希望所有的每个部门删除行为都作为一个atomic statement,换句话说,假定用户选择删除5个部门,如果在删除第4个部门的时候发生了错误,我们希望对前面3个部门的删除操作“失效”.我们可以这样来实现,把对Items collection的循环进行封装,然后在一个transaction里调用DeleteDepartment存储过程.(这就是一个在ADO.NET层和存储过程层都提供transaction支持的例子)
这么一来我们的代码看起来和下面的差不多:
'Create a connection
Dim myConnection As New SqlConnection(myConnString)
myConnection.Open()
'Start the transaction
Dim myTrans As SqlTransaction = myConnection.BeginTransaction()
Dim sql as String = "DeleteDepartment"
Try
'Create the SqlCommand object, specifying the transaction through
'the constructor (along with the SQL string and SqlConnection)
Dim myCommand as New SqlCommand(sql, myConnection, myTrans)
For Each dept as ListItem in CheckBoxListID.Items
'Add the dept.Value to the Parameters collection
myCommand.Parameters.Clear()
myCommand.Parameters.Add("@DepartmentID", dept.Value)
myCommand.ExecuteNonQuery()
Next
'If we reach here, all command succeeded, so commit the transaction
myTrans.Commit
Catch ex as Exception
'Something went wrong, so rollback the transaction
myTrans.Rollback()
Throw 'Bubble up the exception
Finally
myConnection.Close()'Finally, close the connection
End Try
结语:
在前面的文章我们探讨了通过ADO.NET来使用transaction;在本文我们考察了如何直接通过存储过程的T-SQL commands来管理transaction,这2种方法都有自己的优点.对我来说,我在ADO.NET层对transaction进行管理是在这些这种情况下:当需要在编程时反复构建多个transaction,或需要用到其它高级别的逻辑处理(high-level logic)来确定改动哪些实体的时候。而当发出一个简单的、简单明了的请求来改动多个表——比如当从父表删除一条记录时其子记录也需要删除时,我就在存储过程里管理transaction逻辑.当然,你的处理办法可能跟我的不大一样.
祝编程愉快!