如何回滚所有已使用 SqlTransaction 执行的 SqlCommand?

how to rollback all SqlCommands which already executed with SqlTransaction?

我有以下代码:

public void Execute(string Query, params SqlParameter[] Parameters)
{
    using (var Connection = new SqlConnection(Configuration.ConnectionString))
    {
        Connection.Open();

        using (var Command = new SqlCommand(Query, Connection))
        {
            if (Parameters.Length > 0)
            {
                Command.Parameters.Clear();
                Command.Parameters.AddRange(Parameters);
            }

            Command.ExecuteNonQuery();
        }
    }
}

对于不同的查询,该方法可能被调用 2 或 3 次,但方式相同。

例如:

  1. 插入员工
  2. 插入员工证书
  3. 在另一个 table 上更新员工的学历 table [ 此处可能会导致失败。例如]

如果第 [3] 点失败,所有已提交的命令都不应执行,必须回滚。

我知道我可以把 SqlTransaction 放在上面并使用 Commit() 方法。但是如果失败了第三点呢?我认为第 3 点只会回滚而其他第 1,2 点不会?如何解决这个问题,我应该采取什么方法??

我应该使用 SqlCommand[] 数组吗?我该怎么办?

我只在 CodeProject 中找到类似的问题:

See Here

using (var connection = new SqlConnection(Configuration.ConnectionString))
            {
                SqlCommand command = connection.CreateCommand();
                SqlTransaction transaction;
                connection.Open();
                transaction = connection.BeginTransaction("Transaction");

                command.Connection = connection;
                command.Transaction = transaction;

                try
                {
                    if (Parameters.Length > 0)
                    {
                        command.Parameters.Clear();
                        command.Parameters.AddRange(Parameters);
                    }
                    command.ExecuteNonQuery();
                    transaction.Commit();
                }
                catch (Exception e)
                {
                    try
                    {
                        transaction.Rollback();
                    }
                    catch (Exception ex2)
                    {
                       //trace
                    }
                }

            }

有几种方法可以做到。

可能涉及更改最少代码和涉及最少复杂性的方法是将多个 SQL 语句链接到一个查询中。为运行多个语句的 Query 参数构建一个字符串非常好,包括 BEGIN TRANSACTIONCOMMIT 和(如果需要)ROLLBACK。基本上,在您的 C# 代码中保留整个存储过程。这还有一个好处,就是可以更轻松地在您的过程中使用版本控制。

但感觉还是有点老套。

减少这种影响的一种方法是将 Execute() 方法标记为 private。然后,在 class 中为每个查询添加一个方法。这样一来,长的SQL字符串就被隔离了,在使用数据库的时候感觉更像是在使用本地的API。对于更复杂的应用程序,这可能是一个完整的独立程序集,其中包含一些管理逻辑功能区域的类型,其中像 Exectue() 这样的核心方法是 internal。不管您最终如何支持交易,这都是一个好主意。

说到过程,存储过程也是处理此问题的完美方式。有一个存储过程来完成所有工作,并在准备就绪时调用它。

另一种选择是重载接受多个查询和参数集合的方法:

public void Execute(string TransactionName, string[] Queries, params SqlParameter[][] Parameters)
{
    using (var Connection = new SqlConnection(Configuration.ConnectionString))
    using (var Transaction = new SqlTransaction(TransactionName))
    {
        connection.Transaction = Transaction;
        Connection.Open();
        try 
        {
            for (int i = 0; i < Queries.Length; i++)
            {
                using (var Command = new SqlCommand(Queries[i], Connection))
                {
                    command.Transaction = Transaction;
                    if (Parameters[i].Length > 0)
                    {
                        Command.Parameters.Clear();
                        Command.Parameters.AddRange(Parameters);
                    }                
                    Command.ExecuteNonQuery();
                }
            }
            Transaction.Commit();
        }
        catch(Exception ex)
        {
            Transaction.Rollback();
            throw; //I'm assuming you're handling exceptions at a higher level in the code
        }
    }
}

虽然我不确定 params 关键字如何与数组的数组一起使用...我只是没有尝试过该选项,但是按照这些思路进行操作会起作用。这里的弱点还在于,稍后的查询依赖于先前查询的结果并不是微不足道的,即使没有参数的查询仍然需要一个参数数组作为占位符。

最后一个选项是扩展持有 Execute() 方法的类型以支持交易。这里的诀窍是让这种类型成为 static 是常见的(并且是可取的),但是支持事务需要重新使用公共连接和事务对象。鉴于交易的隐含长 运行 性质,您必须一次支持多个,这意味着实例和实现 IDisposable

无需更改您的 Execute 方法,您就可以做到这一点

    var tranOpts = new TransactionOptions()
    {
         IsolationLevel = IsolationLevel.ReadCommitted,
         Timeout = TransactionManager.MaximumTimeout
    };

    using (var tran = new TransactionScope(TransactionScopeOption.Required, tranOpts)
    {
       Execute("INSERT ...");
       Execute("INSERT ...");
       Execute("UPDATE ...");

       tran.Complete();
    }

SqlClient 将缓存事务中登记的内部 SqlConnection,并在每次调用 Execute 时重用它。所以你甚至会得到一个本地(非分布式)交易。

这一切都在此处的文档中进行了解释:System.Transactions Integration with SQL Server