【问题标题】:How can Dispose() know it was called because of an exception?Dispose() 怎么知道它是因为异常而被调用的?
【发布时间】:2011-07-12 11:44:59
【问题描述】:

我想编写一个简单的工作单元类,其行为如下:

using (var unitOfWork = new UnitOfWork())
{
   // Call the data access module and don't worry about transactions.
   // Let the Unit of Work open a session, begin a transaction and then commit it.
}

这是我目前所拥有的(如果您认为我的设计有误,欢迎任何 cmets):

class UnitOfWork : IDisposable
{
   ISession _session;
   ITransation _transaction;
   .
   .
   .
   void Dispose()
   {
      _transaction.Commit();
      _session.Dispose();
   }
}

我想做的是回滚事务,以防数据访问代码抛出一些异常。所以Dispose() 方法看起来像:

   void Dispose()
   {
      if (Dispose was called because an exception was thrown) 
      {
         _transaction.Commit();
      }
      else
      {
         _transaction.RollBack();
      }
      _session.Dispose();
   }

这有意义吗?如果可以,怎么做?

【问题讨论】:

    标签: c# nhibernate exception-handling data-access-layer unit-of-work


    【解决方案1】:

    “Dispose()”应该与事务提交或回滚无关。您应该在 Dispose() 方法中处理您的事务。从长远来看,更改 Dispose() 方法的语义只会给您和其他使用您的类的人增加混乱。

    事务的 Commit() 和 RollBack() 方法与 Dispose() 方法没有任何关系,因为这两个方法和 Dispose() 之间没有关联,因为无论最终结果如何,您都必须处理一个事务结果是。

    这是用于连接和事务的正确模式。请注意,Roolback(0 与异常相关(而不是处置)

    connection.Open();
    var trasnaction = null;
    try
    {
      transaction = connection.BeginTransaction(); 
      ///Do Some work
      transaction.Commit();
    }
    catch
    {
      transaction.Rollback();
    }
    finally
    {
      if (transaction != null)
        transaction.Dispose();
      connection.Close();
    }
    

    所以在你的 UnitOfWork 中模仿这种模式,使用 Commit()、Roolback() 和 Dispose() 方法。

    【讨论】:

    • 这正是我想要避免的。我不希望用户记得在每个块的末尾调用 Commit()。我希望 Commit() 自行发生。但没关系,我想我已经找到了一个很好的解决方案。我会在下周发布。
    • @llya,不知道为什么“用户”除了启动流程之外还必须打电话或做任何事情。我展示的代码是方法的一部分。用户只需启动流程(调用方法)。
    • Dispose 中的回滚是完全正确和惯用的。只要有可能,应该设计一个类,以便在调用 IDisposable.Dispose 后可以安全地放弃任何不会再次使用的对象,而无需执行任何其他操作。如果数据库要求任何最终不会被提交的事务都应该被回滚,那么以允许通过 IDisposable.Dispose 处理其责任的方式封装连接对象会更干净而不是需要一些非标准的机制来确保它处于合理的状态。
    • 您能解释一下为什么在 catch 块中调用了 transaction.RollBack(),但为什么在 finally 块中处理了事务?
    【解决方案2】:

    这里的游戏有点晚了,但请查看this post from Ayende 以获得(有点疯狂的)解决方案:

    在 Dispose 方法中,您只需要确定它是否“干净”地到达那里 - 即在提交事务之前确定是否存在未处理的异常:

    public class ExceptionDetector : IDisposable
    {
        public void Dispose()
        {
            if (Marshal.GetExceptionCode()==0)
                Console.WriteLine("Completed Successfully!");
            else
                Console.WriteLine("Exception!");
        }
    }
    

    【讨论】:

    • 它的确切语义是什么?如果一个Dispose 方法链接到try/finally 块中的另一个方法会发生什么?
    • if (Marshal.GetExceptionCode() == 0) _repository.CommitUnitOfWork();
    • 如果finally 块中的一个Dispose 方法链接到try/finally 块中的另一个,Marshal.GetExceptionCode 会报告最内层块还是外层块的状态,或者什么?
    • 对于 .NET Core docs.microsoft.com/en-us/dotnet/api/… 已过时
    【解决方案3】:

    您需要在using 块的末尾添加UnitOfWork.Commit()。在UnitOfWork 中,您有一个committed 标志,您可以在UnitOfWork.Dispose 中签入。如果那里的标志是false,那么你是UnitOfWork.Rollback()

    【讨论】:

      【解决方案4】:

      Dispose 的意义在于它始终在运行。通过使用这种提交-回滚习语,您无需知道其中的区别。

      using (var unitOfWork = new UnitOfWork())
      {
          // use unitOfWork here - No need to worry about transactions for this code.
          unitOfWork.Commit();
      }
      

      这里我们看到要么抛出异常或提交unitOfWork。然后我们可以在UnitOfWork 中有一个布尔值来跟踪提交是否被执行。然后Dispose 可以回滚未提交。这样,工作单元总是回滚或提交。

      无论如何我都会避免在 Dispose 中使用 Commit。对于初学者来说,ITransaction.Commit 方法通常可能会在错误时抛出异常——这是完全正常的。但是,Dispose 方法不应该抛出异常。请参阅this link 并在 Stackoverflow 上搜索以了解有关为什么的更多详细信息。

      我在想这样的事情

      class UnitOfWork : IDisposable
      {
         ISession _session;
         ITransation _transaction;
         bool _commitTried;
      
         // stuff goes here
      
         void Commit()
         {
            _commitTried = true;
            _transaction.Commit();
         }
      
         void Dispose()
         {
            if (!_commitTried) _transaction.Rollback();
            _transaction.Dispose();
            _session.Dispose();
         }
      }
      

      我想说完全忘记调用 Commit 的问题并没有那么大,因为除非提交,否则客户端代码将不起作用,因为事务是 Rollback'ed 并且未应用更改在夹具内或手动执行代码时会发现。

      我实际上尝试在一个项目中通过使用像这样的 lambda 语法来处理这个问题

      _repository.InTransactionDo(ThisMethodIsRunInsideATransaction);
      

      这样客户就不需要担心提交任何东西。实际上我最终后悔了,因为它让事情变得太复杂了,希望我能采用上述方法。

      【讨论】:

      • 这正是我想要避免的。我不希望用户记得在每个块的末尾调用 Commit()。我希望 Commit() 自行发生。但没关系,我想我已经找到了一个很好的解决方案。我会在下周发布。
      • 好的,我要稍微编辑一下我的帖子来指导它。期待你的帖子!
      • @Ilya Kogan 稍微编辑了我的帖子以解决用户忘记调用 Commit() 的问题。
      • @vidstige 这正是我打算做的,伟大的思想都一样:)
      • @vidstige:共识问题意味着在处理远程系统时,提交总是有可能以这种方式失败,以至于尝试执行它的代码不知道它是成功还是除非或直到它可以查询远程系统。如果导致提交失败的因素使远程系统不可用(例如通信中断),这可能暂时无法实现。如果通信不可靠,则很容易发生无法证明已经发生的回滚。真正失败的回滚很少见,但应该非常令人担忧。
      【解决方案5】:

      毕竟我实现了一个方法,所有的操作都应该通过它来执行:

      class UnitOfWork : IDisposable
      {
         ...
         public void DoInTransaction(Action<ISession> method)
         {
             Open session, begin transaction, call method, and then commit. Roll back if there was an exception.
         }
      }
      

      【讨论】:

        猜你喜欢
        • 2021-04-30
        • 2011-05-01
        • 2019-07-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多