【问题标题】:how to rollback all SqlCommands which already executed with SqlTransaction?如何回滚已使用 SqlTransaction 执行的所有 SqlCommand?
【发布时间】:2020-01-02 23:08:46
【问题描述】:

我有以下代码:

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. 在另一个表上更新员工学位 [ 此处可能导致失败。例如]

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

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

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

我只发现类似的问题,但在 CodeProject 中:

See Here

【问题讨论】:

    标签: c# .net sql-server transactions ado.net


    【解决方案1】:

    无需更改您的 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 将缓存 Transaction 中登记的内部 SqlConnection,并在每次调用 Execute 时重用它。因此,您甚至会得到一个本地(非分布式)事务。

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

    【讨论】:

    • 如果是这样,它是否安全且性能良好?请解释更多。
    • 只有 TransactionScope 中的查询会被提交或回滚,性能与使用 TSQL BEGIN TRAN/COMMIT 对相同。
    • 伙计,你成就了我的一天。请原谅我想知道 TransactionScope 比 Normal Connection.BeginTransaction() 和 Rollback() 更好吗?还是和性能一样?
    • 正常情况下是一样的。
    【解决方案2】:

    有几种方法可以做到这一点。

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

    但它仍然感觉有点骇人听闻。

    减少这种影响的一种方法是标记Execute() 方法私有。然后,在类中为每个查询添加一个附加方法。通过这种方式,长 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

    【讨论】:

      【解决方案3】:
      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
                          }
                      }
      
                  }
      

      【讨论】:

      • 这是一个正确的答案,但可以使用更多细节。如果有多个命令会是什么样子?
      • @MahdiAnjam Sql Server 已经使用隐式事务。没有理由只使用一个 SqlCommand 对象来使用此代码。重点是您将使用相同的事务连续运行一系列命令。
      • @JoelCoehoorn 如果(在本例中)该命令不是包含多个更新的存储过程,那是正确的。如果这是一个存储过程,这将起作用,尽管我们永远不会这样做。我们将交易放入程序中。但是由于 OP 询问了执行多个命令,因此在执行多个命令时显示答案如何应用是有意义的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-17
      • 2015-07-18
      • 2021-12-14
      • 1970-01-01
      相关资源
      最近更新 更多