.NET 事务技术-System.Transactions.TransactionScope 隐式事务(方便)

作者:vkvi 来源:ITPOW(原创) 日期:2017-2-28

前一节说了 SqlConnection显式事务,现在谈谈隐式事务,相对于显式事务,隐式事务不再有 CommitRollback 方法。

准备工作

  • 添加 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。并且它不管什么 CommitRollback,只管 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 有什么区别?

相关文章