34

我有一个问题,我发现的所有文章或示例似乎都不关心它。

我想在事务中执行一些数据库操作。我想做的与大多​​数示例非常相似:

using (SqlConnection Conn = new SqlConnection(_ConnectionString))
{
    try
    {
        Conn.Open();
        SqlTransaction Trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn))
        {
            /* DB work */
        }
    }
    catch (Exception Ex)
    {
        Trans.Rollback();
        return -1;
    }
}

但问题是在块SqlTransaction Trans内声明了。所以它在块try中是不可访问的。catch()大多数示例只是在块之前执行Conn.Open(),但我认为这有点冒险,因为两者都可能引发多个异常。Conn.BeginTransaction()try

我错了,还是大多数人只是忽略了这种风险?如果发生异常,能够回滚的最佳解决方案是什么?

4

7 回答 7

57
using (var Conn = new SqlConnection(_ConnectionString))
{
    SqlTransaction trans = null;
    try
    {
        Conn.Open();
        trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn, trans))
        {
            /* DB work */
        }
        trans.Commit();
    }
    catch (Exception Ex)
    {
        if (trans != null) trans.Rollback();
        return -1;
    }
}

或者您可以更清洁、更轻松地使用它:

using (var Conn = new SqlConnection(_ConnectionString))
{
    try
    {
        Conn.Open();
        using (var ts = new System.Transactions.TransactionScope())
        {
            using (SqlCommand Com = new SqlCommand(ComText, Conn))
            {
                /* DB work */
            }
            ts.Complete();
        }
    }
    catch (Exception Ex)
    {     
        return -1;
    }
}
于 2010-05-26T10:39:22.467 回答
8

我不喜欢键入类型并将变量设置为 null,所以:

try
{
    using (var conn = new SqlConnection(/* connection string or whatever */))
    {
        conn.Open();

        using (var trans = conn.BeginTransaction())
        {
            try
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.Transaction = trans;
                    /* setup command type, text */
                    /* execute command */
                }

                trans.Commit();
            }
            catch (Exception ex)
            {
                trans.Rollback();
                /* log exception and the fact that rollback succeeded */
            }
        }
    }
}
catch (Exception ex)
{
    /* log or whatever */
}

如果您想切换到 MySql 或其他提供程序,您只需修改 1 行。

于 2013-07-10T05:00:36.723 回答
6

用这个

using (SqlConnection Conn = new SqlConnection(_ConnectionString))
{
    SqlTransaction Trans = null;
    try
    {
        Conn.Open();
        Trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn))
        {
            /* DB work */
        }
    }
    catch (Exception Ex)
    {
        if (Trans != null)
            Trans.Rollback();
        return -1;
    }
}

顺便说一句 - 如果处理成功,您没有提交它

于 2010-05-26T10:39:16.373 回答
3
using (SqlConnection Conn = new SqlConnection(_ConnectionString))
{
    try
    {
        Conn.Open();
        SqlTransaction Trans = Conn.BeginTransaction();

        try 
        {
            using (SqlCommand Com = new SqlCommand(ComText, Conn))
            {
                /* DB work */
            }
        }
        catch (Exception TransEx)
        {
            Trans.Rollback();
            return -1;
        }
    }
    catch (Exception Ex)
    {
        return -1;
    }
}
于 2010-05-26T10:44:00.087 回答
2

当我在 2018 年底第一次发现这个问题时,我并不认为当时投票最多的答案中可能存在错误,但事实就是如此。我首先考虑简单地评论答案,但后来我想用我自己的参考来支持我的主张。以及我所做的测试(基于 .Net Framework 4.6.1 和 .Net Core 2.1。)

鉴于 OP 的约束,事务应在连接中声明,这使我们只能使用其他答案中已经提到的 2 个不同的实现:

使用事务范围

using (SqlConnection conn = new SqlConnection(conn2))
{
    try
    {
        conn.Open();
        using (TransactionScope ts = new TransactionScope())
        {
            conn.EnlistTransaction(Transaction.Current);
            using (SqlCommand command = new SqlCommand(query, conn))
            {
                command.ExecuteNonQuery();
                //TESTING: throw new System.InvalidOperationException("Something bad happened.");
            }
            ts.Complete();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

使用 SqlTransaction

using (SqlConnection conn = new SqlConnection(conn3))
{
    try
    {
        conn.Open();
        using (SqlTransaction ts = conn.BeginTransaction())
        {
            using (SqlCommand command = new SqlCommand(query, conn, ts))
            {
                command.ExecuteNonQuery();
                //TESTING: throw new System.InvalidOperationException("Something bad happened.");
            }
            ts.Commit();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

您应该知道,在 SqlConnection 中声明 TransactionScope 时,连接对象不会自动登记到 Transaction 中,而是必须使用显式登记它conn.EnlistTransaction(Transaction.Current);

测试并证明
我在 SQL Server 数据库中准备了一个简单的表:

SELECT * FROM [staging].[TestTable]

Column1
-----------
1

.NET 中的更新查询如下:

string query = @"UPDATE staging.TestTable
                    SET Column1 = 2";

在 command.ExecuteNonQuery() 之后立即引发异常:

command.ExecuteNonQuery();
throw new System.InvalidOperationException("Something bad happened.");

这是供您参考的完整示例:

string query = @"UPDATE staging.TestTable
                    SET Column1 = 2";

using (SqlConnection conn = new SqlConnection(conn2))
{
    try
    {
        conn.Open();
        using (TransactionScope ts = new TransactionScope())
        {
            conn.EnlistTransaction(Transaction.Current);
            using (SqlCommand command = new SqlCommand(query, conn))
            {
                command.ExecuteNonQuery();
                throw new System.InvalidOperationException("Something bad happened.");
            }
            ts.Complete();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

如果执行测试,它会在 TransactionScope 完成之前引发异常,并且更新不会应用于表(事务回滚)并且值保持不变。这是每个人都期望的预期行为。

Column1
-----------
1

如果我们忘记在事务中登记连接会发生什么conn.EnlistTransaction(Transaction.Current);

重新运行示例再次引发异常,执行流程立即跳转到 catch 块。尽管ts.Complete();从未被称为表值已更改:

Column1
-----------
2

由于事务范围是在 SqlConnection 之后声明的,因此连接不知道范围并且不会隐式登记在所谓的环境事务中。

对数据库书呆子的深入分析

为了更深入地挖掘,如果command.ExecuteNonQuery();在引发异常之后和之前执行暂停,我们可以查询数据库(SQL Server)上的事务,如下所示:

SELECT tst.session_id, tat.transaction_id, is_local, open_transaction_count, transaction_begin_time, dtc_state, dtc_status
  FROM sys.dm_tran_session_transactions tst
  LEFT JOIN sys.dm_tran_active_transactions tat
  ON tst.transaction_id = tat.transaction_id
  WHERE tst.session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE program_name = 'TransactionScopeTest')

请注意,可以通过连接字符串中的应用程序名称属性设置会话程序名称: Application Name=TransactionScopeTest;

目前现有的交易正在以下展开:

session_id  transaction_id       is_local open_transaction_count transaction_begin_time  dtc_state   dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------
113         6321722              1        1                      2018-11-30 09:09:06.013 0           0

如果conn.EnlistTransaction(Transaction.Current);没有事务绑定到活动连接,因此更改不会在事务上下文下发生:

session_id  transaction_id       is_local open_transaction_count transaction_begin_time  dtc_state   dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------

备注 .NET Framework 与 .NET Core
在我使用 .NET Core 进行测试期间,我遇到了以下异常:

System.NotSupportedException: 'Enlisting in Ambient transactions is not supported.'

看来.NET Core (2.1.0) 目前不支持 TransactionScope 方法,无论 Scope 是在 SqlConnection 之前还是之后初始化的

于 2018-11-30T09:49:32.977 回答
1

Microsoft 示例,将 begin trans 放在 try/catch 之外,请参阅此 msdn 链接。我假设 BeginTransaction 方法应该要么抛出异常,要么开始事务,但永远不要同时发生(尽管文档没有说这是不可能的)。

但是,您最好使用TransactionScope来为您管理很多(不是那么)繁重的工作:此链接

于 2010-05-26T10:41:58.233 回答
1
SqlConnection conn = null;
SqlTransaction trans = null;

try
{
   conn = new SqlConnection(_ConnectionString);
   conn.Open();
   trans = conn.BeginTransaction();
   /*
    * DB WORK
    */
   trans.Commit();
}
catch (Exception ex)
{
   if (trans != null)
   {
      trans.Rollback();
   }
   return -1;
}
finally
{
   if (conn != null)
   {
      conn.Close();
   }
}
于 2014-09-15T10:08:17.387 回答