C#数据库事务原理及实践(上)
本文将涉及到在 .net 框架下使用 C# 语言操纵数据库事务的各个方面。
体验 SQL 语言的事务机制 作为大型的企业级数据库, SQL Server2000 对事务提供了很好的支持。我们可以使用 SQL 语句来定义、提交以及回滚一个事务。 如下所示的 SQL 代码定义了一个事务,并且命名为 "MyTransaction" (限于篇幅,本文并不讨论如何编写 SQL 语言程序,请读者自行参考相关书籍):
DECLARE @TranName VARCHAR(20) SELECT @TranName = 'MyTransaction' BEGIN TRANSACTION @TranNameGOUSE pubs GO UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%' GO COMMIT TRANSACTION MyTransaction GO
这里用到了 SQL Server2000 自带的示例数据库 pubs ,提交事务后,将为所有畅销计算机书籍支付的版税增加 10% 。 打开 SQL Server2000 的查询分析器,选择 pubs 数据库,然后运行这段程序,结果显而易见。 可是如何在 C# 程序中运行呢?我们记得在普通的 SQL 查询中,一般需要把查询语句赋值给 SalCommand.CommandText 属性,这里也就像普通的 SQL 查询语句一样,将这些语句赋给 SqlCommand.CommandText 属性即可。要注意的一点是,其中的 "GO" 语句标志着 SQL 批处理的结束,编写 SQL 脚本是需要的,但是在这里是不必要的。我们可以编写如下的程序来验证这个想法:
//TranSql.csusing System; using System.Data; using System.Data.SqlClient; namespace Aspcn { public class DbTranSql { file:// 将事务放到 SQL Server 中执行 public void DoTran() { file:// 建立连接并打开 SqlConnection myConn=GetConn();myConn.Open(); SqlCommand myComm=new SqlCommand(); try { myComm.Connection=myConn; myComm.CommandText="DECLARE @TranName VARCHAR(20) "; myComm.CommandText+="SELECT @TranName = 'MyTransaction' "; myComm.CommandText+="BEGIN TRANSACTION @TranName "; myComm.CommandText+="USE pubs "; myComm.CommandText+="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%' "; myComm.CommandText+="COMMIT TRANSACTION MyTransaction "; myComm.ExecuteNonQuery(); } catch(Exception err) { throw new ApplicationException(" 事务操作出错,系统信息: "+err.Message); } finally { myConn.Close(); } } file:// 获取数据连接 private SqlConnection GetConn() { string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password="; SqlConnection myConn=new SqlConnection(strSql); return myConn; } } public class Test { public static void Main() { DbTranSql tranTest=new DbTranSql(); tranTest.DoTran(); Console.WriteLine(" 事务处理已经成功完成。 "); Console.ReadLine(); } } }
注意到其中的 SqlCommand 对象 myComm ,它的 CommandText 属性仅仅是前面 SQL 代码字符串连接起来即可,当然,其中的 "GO" 语句已经全部去掉了。这个语句就像普通的查询一样,程序将 SQL 文本事实上提交给 DBMS 去处理了,然后接收返回的结果(如果有结果返回的话)。 很自然,我们最后看到了输出 " 事务处理已经成功完成 " ,再用企业管理器查看 pubs 数据库的 roysched 表,所有 title_id 字段以 "PC" 开头的书籍的 royalty 字段的值都增加了 0.1 倍。 这里,我们并没有使用 ADO.net 的事务处理机制,而是简单地将执行事务的 SQL 语句当作普通的查询来执行,因此,事实上该事务完全没有用到 .net 的相关特性。 了解 .net 中的事务机制 如你所知,在 .net 框架中主要有两个命名空间 (namespace) 用于应用程序同数据库系统的交互: System.Data.SqlClient 和 System.Data.OleDb 。前者专门用于连接 Microsoft 公司自己的 SQL Server 数据库,而后者可以适应多种不同的数据库。这两个命名空间中都包含有专门用于管理数据库事务的类,分别是 System.Data.SqlClient.SqlTranscation 类和 System.Data.OleDb.OleDbTranscation 类。 就像它们的名字一样,这两个类大部分功能是一样的,二者之间的主要差别在于它们的连接机制,前者提供一组直接调用 SQL Server 的对象,而后者使用本机 OLE DB 启用数据访问。 事实上, ADO.net 事务完全在数据库的内部处理,且不受 Microsoft 分布式事务处理协调器 (DTC) 或任何其他事务性机制的支持。本文将主要介绍 System.Data.SqlClient.SqlTranscation 类,下面的段落中,除了特别注明,都将使用 System.Data.SqlClient.SqlTranscation 类。
事务的开启和提交 现在我们对事务的概念和原理都了然于心了,并且作为已经有一些基础的 C# 开发者,我们已经熟知编写数据库交互程序的一些要点,即使用 SqlConnection 类的对象的 Open() 方法建立与数据库服务器的连接,然后将该连接赋给 SqlCommand 对象的 Connection 属性,将欲执行的 SQL 语句赋给它的 CommandText 属性,于是就可以通过 SqlCommand 对象进行数据库操作了。对于我们将要编写的事务处理程序,当然还需要定义一个 SqlTransaction 类型的对象。并且看到 SqlCommand 对象的 Transcation 属性,我们很容易想到新建的 SqlTransaction 对象应该与它关联起来。 基于以上认识,下面我们就开始动手写我们的第一个事务处理程序。我们可以很熟练地写出下面这一段程序:
//DoTran.csusing System; using System.Data; using System.Data.SqlClient; namespace Aspcn { public class DbTran { file:// 执行事务处理 public void DoTran() { file:// 建立连接并打开 SqlConnection myConn=GetConn(); myConn.Open(); SqlCommand myComm=new SqlCommand(); SqlTransaction myTran=new SqlTransaction(); try { myComm.Connection=myConn; myComm.Transaction=myTran; file:// 定位到 pubs 数据库 myComm.CommandText="USE pubs"; myComm.ExecuteNonQuery(); file:// 更新数据 file:// 将所有的计算机类图书 myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'"; myComm.ExecuteNonQuery();// 提交事务 myTran.Commit(); } catch(Exception err) { throw new ApplicationException(" 事务操作出错,系统信息: "+err.Message); } finally { myConn.Close(); } } file:// 获取数据连接 private SqlConnection GetConn() { string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password="; SqlConnection myConn=new SqlConnection(strSql); return myConn; } } public class Test{public static void Main() { DbTran tranTest=new DbTran(); tranTest.DoTran(); Console.WriteLine(" 事务处理已经成功完成。 "); Console.ReadLine(); } } }
显然,这个程序非常简单,我们非常自信地编译它,但是,出乎意料的结果使我们的成就感顿时烟消云散: error CS1501: 重载 "SqlTransaction" 方法未获取 "0" 参数 是什么原因呢?注意到我们初始化的代码:
SqlTransaction myTran=new SqlTransaction();
显然,问题出在这里,事实上, SqlTransaction 类并没有公共的构造函数,我们不能这样新建一个 SqlTrancaction 类型的变量。在事务处理之前确实需要有一个 SqlTransaction 类型的变量,将该变量关联到 SqlCommand 类的 Transcation 属性也是必要的,但是初始化方法却比较特别一点。在初始化 SqlTransaction 类时,你需要使用 SqlConnection 类的 BeginTranscation() 方法:
SqlTransaction myTran; myTran=myConn.BeginTransaction();
该方法返回一个 SqlTransaction 类型的变量。在调用 BeginTransaction() 方法以后,所有基于该数据连接对象的 SQL 语句执行动作都将被认为是事务 MyTran 的一部分。同时,你也可以在该方法的参数中指定事务隔离级别和事务名称,如:
SqlTransaction myTran; myTran=myConn.BeginTransaction(IsolationLevel.ReadCommitted,"SampleTransaction");
关于隔离级别的概念我们将在随后的内容中探讨,在这里我们只需牢记一个事务是如何被启动,并且关联到特定的数据链接的。 先不要急着去搞懂我们的事务都干了些什么,看到这一行:
是的,这就是事务的提交方式。该语句执行后,事务的所有数据库操作将生效,并且为数据库事务的持久性机制所保持 -- 即使系统在这以后发生致命错误,该事务对数据库的影响也不会消失。 对上面的程序做了修改之后我们可以得到如下代码(为了节约篇幅,重复之处已省略,请参照前文):
//DoTran.cs…… } file://执行事务处理 public void DoTran() { file:// 建立连接并打开 SqlConnection myConn=GetConn(); myConn.Open(); SqlCommand myComm=new SqlCommand(); file://SqlTransaction myTran=new SqlTransaction(); file:// 注意, SqlTransaction 类无公开的构造函数 SqlTransaction myTran; file:// 创建一个事务 myTran=myConn.BeginTransaction(); try { file:// 从此开始,基于该连接的数据操作都被认为是事务的一部分 file:// 下面绑定连接和事务对象 myComm.Connection=myConn; myComm.Transaction=myTran; file:// 定位到 pubs 数据库 myComm.CommandText="USE pubs"; myComm.ExecuteNonQuery();// 更新数据 file:// 将所有的计算机类图书 myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'"; myComm.ExecuteNonQuery(); file:// 提交事务 myTran.Commit(); } catch(Exception err) { throw new ApplicationException(" 事务操作出错,系统信息: "+err.Message); } finally { myConn.Close(); } } ……
到此为止,我们仅仅掌握了如何开始和提交事务。下一步我们必须考虑的是在事务中可以干什么和不可以干什么。