前一节说了 SqlConnection 的显式事务,现在谈谈隐式事务,相对于显式事务,隐式事务不再有 Commit、Rollback 方法。
准备工作
添加 System.Transactions 引用(在解决方案的项目/网站上右键);
引用 System.Transactions 名称空间。
示例讲解
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MySqlServer"].ConnectionString)) { using (TransactionScope ts = new TransactionScope()) { conn.Open(); using (SqlCommand cmd = new SqlCommand()) { cmd.Connection = conn; cmd.CommandType = CommandType.Text; try { cmd.CommandText = "insert into TranTable(Priority) values(1)"; cmd.ExecuteNonQuery(); cmd.CommandText = "insert into TranTable(Priority) values(256)"; cmd.ExecuteNonQuery(); ts.Complete(); Response.Write("Ok"); } catch (SqlException ex) { Response.Write("Error:" + ex.Message); } } conn.Close(); } }
可以看出 TransactionScope 并不来自于 SqlConnection,所以它可以管理多个 SqlConnection。并且它不管什么 Commit、Rollback,只管 Complete。
注意:conn.Open() 一定要在 new TransactionScope() 后面,否则不起作用,conn.Close() 则是否位于 TransactionScope 范围都无所谓。
TransactionScope 和数据库操作并没有直接关联,是什么造成了数据库操作以事务的形式执行的呢?
现在我们在数据库连接字符串中加上 Enlist=false;,再执行上面的代码,然后观察数据库,可以发现数据库中多了一条记录 1,说明上述代码不是按事务的方式执行的。原来,Enlist 默认为 true,表示 SqlClient 将自动检测是否有事务存在,若存在,则自动登记到事务中,也就形成了事务,这也是为什么 conn.Open() 要在 new TransactionScope() 后面的原因。
enlist [in'list]
v. 徵募,参与,支持
再看一个复杂点的
public void Do() { using (SqlConnection conn = new SqlConnection("Data Source=.; Initial Catalog=TestDb; Integrated Security=SSPI;")) { using (TransactionScope ts = new TransactionScope()) { conn.Open(); try { Do1(conn); Do2(conn); } catch(SqlException) { } ts.Complete(); // 注意,在 SQL Server 中,这个位置不正确,后面会说明 conn.Close(); } } } private void Do1(SqlConnection conn) { // 正确的插入语句 using (SqlCommand cmd = new SqlCommand("insert into TestTbl(fld1) values(2)", conn)) { cmd.ExecuteNonQuery(); } } private void Do2(SqlConnection conn) { // 错误的插入语句 using (SqlCommand cmd = new SqlCommand("insert into TestTbl(fld1) values(j)", conn)) { cmd.ExecuteNonQuery(); } }
有两个 SqlCommand,第一个能够插入记录,第二个不能插入记录。
理论上来说,由于使用了事务,最终结果是没有插入任何数据到数据库的,但是我们发现第一个竟然插入成功了。难道是不支持两个 SqlCommand?其实并不是,而是我们的 ts.Complete() 位置不对,应该改成如下:
using (SqlConnection conn = new SqlConnection("Data Source=.; Initial Catalog=TestDb; Integrated Security=SSPI;")) { using (TransactionScope ts = new TransactionScope()) { conn.Open(); try { Do1(conn); Do2(conn); ts.Complete(); } catch(SqlException) { } conn.Close(); } }
将 ts.Complete() 放在 try 中和 Do1()、Do2() 一起,就体现出事务的特性了,为什么会这样呢?
ts.Complete() 表示告知数据库可以写入了。
ts.Complete() 放在 try 外时:
Do1() 可以写入,那就写入吧。
Do2() 不能写入,那就不写入吧。
而再看看将 ts.Complete() 放在 try 中和 Do1()、Do2() 一起有什么神奇的事情:
Do1() 没发生异常。
Do2() 发生了异常,跳出 try。
所以 ts.Complete() 根本就没有机会执行到。所以 Do1() 虽然可以写入,但是由于 ts.Complete() 没执行,就没给他写入的机会,所以 Do1()、Do2() 最终都没有写入数据库。
说一句,事务执行时,虽然 Do1() 没有写入,但是它仍然占用了一个 Id,也就是说当我们数据库中使用了标识列(通常说的自增长 Id 列),这个 Id 会出现不连续,经测试发现就是由于 Do1()(不是 Do2)写入后又撤销了造成的。
如果我们改一下这几句代码的顺序,改成如下:
ts.Complete();
Do1();
Do2();
此时不会出错,Do1() 会写入成功,然后在 Do2() 产生异常。
再改一下:
ts.Complete();
Do1();
ts.Complete();
Do2();
同样,Do1() 还是会写入成功,不过在第二个 ts.Complete() 时就会出错,说明 ts.Complete() 不能重复调用。
再看看三种 SQL 语句位置有什么区别
// 第一种,把两个 SQL 语句赋给两个 SqlCommand using (SqlCommand cmd = new SqlCommand("insert into TestTbl(fld1) values(2);", conn)) { cmd.ExecuteNonQuery(); } using (SqlCommand cmd = new SqlCommand("insert into TestTbl(fld1) values(j)", conn)) { cmd.ExecuteNonQuery(); } // 第二种,把两个 SQL 语句赋给一个 SqlCommand,分两次执行 using (SqlCommand cmd = new SqlCommand()) { cmd.Connection = conn; cmd.CommandText = "insert into TestTbl(fld1) values(2)"; cmd.ExecuteNonQuery(); cmd.CommandText = "insert into TestTbl(fld1) values(j)"; cmd.ExecuteNonQuery(); } // 第三种,把两个 SQL 语句合并后赋给一个 SqlCommand,一次执行 using (SqlCommand cmd = new SqlCommand("insert into TestTbl(fld1) values(2);insert into TestTbl(fld1) values(j)", conn)) { cmd.ExecuteNonQuery(); }
(以上代码受用途限制,有些是通过构造函数赋的值,有些是通过属性赋的值,这不影响,因为通过构造函数赋值同样是对属性赋值。)
第一种,把两个 SQL 语句赋给两个 SqlCommand
第二种,把两个 SQL 语句赋给一个 SqlCommand,分两次执行
第三种,把两个 SQL 语句合并后赋给一个 SqlCommand,一次执行
结论:
这三种都可以实现事务。
第一种和第二种结果一样,都会出现 Id 不连续的情况(前面有说)。而第三种不会出现 Id 不连续的情况。
如果在 SQL 语句中又使用 SQL Server 的事务,有没有问题?
比如我们把上面代码的 SQL 语句改成:
begin tran insert into TestTbl(fld1) values(2); insert into TestTbl(fld1) values(j); commit tran
不会有问题,照样按事务执行。
特别说明:.NET 操作 PostgreSQL-事务怎么用?与 SQL Server 有什么区别?