【问题标题】:Transactions in the Repository Pattern存储库模式中的事务
【发布时间】:2010-10-09 04:13:21
【问题描述】:

如何使用存储库模式以事务方式封装多个实体的保存?例如,如果我想添加一个订单并根据该订单创建更新客户状态,但只有在订单成功完成后才这样做?请记住,对于此示例,订单不是客户内部的集合。他们是他们自己的实体。

这只是一个人为的例子,所以我并不关心订单是否应该或不应该在客户对象内,甚至在同一个有界上下文中。我并不真正关心将使用什么底层技术(nHibernate、EF、ADO.Net、Linq 等)。我只是想看看在这个公认的人为设计的全有或全无操作示例中某些调用代码可能是什么样子。

【问题讨论】:

    标签: design-patterns domain-driven-design repository-pattern architectural-patterns


    【解决方案1】:

    我会考虑使用某种类型的事务范围/上下文系统。所以你可能有以下大致基于 .Net & C# 的代码。

    public class OrderService
    {
    
    public void CreateNewOrder(Order order, Customer customer)
    {
      //Set up our transactional boundary.
      using (TransactionScope ts=new TransactionScope())
      {
        IOrderRepository orderRepos=GetOrderRespository();
        orderRepos.SaveNew(order);
        customer.Status=CustomerStatus.OrderPlaced;
    
        ICustomerRepository customerRepository=GetCustomerRepository();
        customerRepository.Save(customer)
        ts.Commit();   
       }
    }
    }
    

    TransactionScope 可以嵌套,因此假设您有一个跨越多个服务的操作,您的应用程序也会创建一个 TransactionScope。现在在当前的 .net 中,如果您使用 TransactionScope,他们就有升级为 DTC 的风险,但这将在未来得到解决。

    我们创建了自己的 TransactionScope 类,它基本上管理我们的数据库连接并使用本地 SQL 事务。

    【讨论】:

    • 我认为这不是 DDD 精神的解决方案。基本上,您已经创建了一个执行域模型工作的事务脚本。例如,服务不应改变客户状态。
    • 代码中的某些内容必须处理此业务规则,无论是在此级别还是更高级别,重点是在单个 TransactionScope 中进行更改,允许本地事务或分布式事务处理事务。如果业务规则说每次下订单时都要更新客户,那么这是处理所有订单的好地方。
    【解决方案2】:

    您想了解如何实现工作单元模式。 NHibernate 有一些实现。一个是在 Rhino Commons 项目中,还有 Machine.UoW。

    【讨论】:

      【解决方案3】:

      使用 Spring.NET AOP + NHibernate,您可以像往常一样编写存储库类并在自定义 XML 文件中配置您的事务:

      public class CustomerService : ICustomerService
      {
          private readonly ICustomerRepository _customerRepository;
          private readonly IOrderRepository _orderRepository;
      
          public CustomerService(
              ICustomerRepository customerRepository, 
              IOrderRepository orderRepository) 
          {
              _customerRepository = customerRepository;
              _orderRepository = orderRepository;
          }
      
          public int CreateOrder(Order o, Customer c) 
          {
              // Do something with _customerRepository and _orderRepository
          }
      }
      

      在 XML 文件中,您可以选择要在事务中执行的方法:

        <object id="TxProxyConfigurationTemplate" 
                abstract="true"
                type="Spring.Transaction.Interceptor.TransactionProxyFactoryObject, Spring.Data">
      
          <property name="PlatformTransactionManager" ref="HibernateTransactionManager"/>
      
          <property name="TransactionAttributes">
            <name-values>
              <add key="Create*" value="PROPAGATION_REQUIRED"/>
            </name-values>
          </property>
        </object>
      
        <object id="customerService" parent="TxProxyConfigurationTemplate">
          <property name="Target">
            <object type="MyNamespace.CustomerService, HibernateTest">
                <constructor-arg name="customerRepository" ref="customerRepository" />
                <constructor-arg name="orderRepository" ref="orderRepository" />
            </object>
          </property>
      
        </object>
      

      在您的代码中,您会获得一个 CustomerService 类的实例,如下所示:

      ICustomerService customerService = (ICustomerService)ContextRegistry
          .GetContent()
          .GetObject("customerService");
      

      Spring.NET 将返回 CustomerService 类的代理,当您调用 CreateOrder 方法时,该代理将应用事务。这样,您的服务类中就没有特定于事务的代码。 AOP 负责处理它。更多详情可以查看Spring.NET的文档。

      【讨论】:

        【解决方案4】:

        如何封装保存的 一个以上的实体 交易方式使用 存储库模式?例如,什么 如果我想添加订单并更新 基于此的客户状态 订单创建,但只有在 订单成功完成?保持在 请注意,对于此示例,订单是 不是客户内部的集合。 他们是他们自己的实体。

        它不是存储库的责任,它通常在更高级别完成。尽管您说您对特定技术不感兴趣,但我认为值得捆绑解决方案,例如在将 NHibernate 与 Web 应用程序一起使用时,您可能会考虑使用 session-per request

        因此,如果您可以在更高级别管理交易,那么我的两个选择是:

        1. 前期检查 - 例如,在协调行为的服务中,通过询问订单/客户来决定您是否要继续,如果任何一方说他们不这样做,那么甚至不要尝试更新其中任何一个他们。
        2. 回滚 - 继续更新客户/订单,如果在回滚数据库事务中途失败。

        如果您选择第二个选项,那么问题是内存中的对象会发生什么情况,您的客户可能会处于不一致的状态。如果这很重要,并且我在不为该请求加载对象的情况下工作,那么我会考虑预先检查是否可能,因为它比替代方案容易得多(回滚-内存更改或重新加载对象)。

        【讨论】:

        • 为什么不是Repository的责任?将数据库操作从域模型中抽象出来的整个想法不是吗?对我来说,存储库是放置事务支持的最佳位置。
        【解决方案5】:

        今天早上启动我的计算机时,我遇到了我正在处理的项目的确切问题。我有一些想法导致了以下设计 - 而 cmets 将非常棒。不幸的是,Josh 建议的设计是不可能的,因为我必须使用远程 SQL 服务器并且无法启用它所依赖的 Distribute Transaction Coordinator 服务。

        我的解决方案基于对现有代码的一些简单更改。

        首先,我的所有存储库都实现了一个简单的标记接口:

        /// <summary>
        /// A base interface for all repositories to implement.
        /// </summary>
        public interface IRepository
        { }
        

        其次,我让所有启用事务的存储库实现以下接口:

        /// <summary>
        /// Provides methods to enable transaction support.
        /// </summary>
        public interface IHasTransactions : IRepository
        {
            /// <summary>
            /// Initiates a transaction scope.
            /// </summary>
            void BeginTransaction();
        
            /// <summary>
            /// Executes the transaction.
            /// </summary>
            void CommitTransaction();
        }
        

        我的想法是,在我所有的存储库中,我实现了这个接口并添加了代码,这些代码直接根据实际的提供者引入了事务(对于假存储库,我制作了一个在提交时执行的委托列表)。对于 LINQ to SQL,很容易进行如下实现:

        #region IHasTransactions Members
        
        public void BeginTransaction()
        {
            _db.Transaction = _db.Connection.BeginTransaction();
        }
        
        public void CommitTransaction()
        {
            _db.Transaction.Commit();
        }
        
        #endregion
        

        这当然需要为每个线程创建一个新的存储库类,但这对我的项目来说是合理的。

        如果存储库实现IHasTransactions,则使用存储库的每个方法都需要调用BeginTransaction()EndTransaction()。为了让这个电话更容易,我想出了以下扩展:

        /// <summary>
        /// Extensions for spawning and subsequently executing a transaction.
        /// </summary>
        public static class TransactionExtensions
        {
            /// <summary>
            /// Begins a transaction if the repository implements <see cref="IHasTransactions"/>.
            /// </summary>
            /// <param name="repository"></param>
            public static void BeginTransaction(this IRepository repository)
            {
                var transactionSupport = repository as IHasTransactions;
                if (transactionSupport != null)
                {
                    transactionSupport.BeginTransaction();
                }
            }
        
            public static void CommitTransaction(this IRepository repository)
            {
                var transactionSupport = repository as IHasTransactions;
                if (transactionSupport != null)
                {
                    transactionSupport.CommitTransaction();
                }
            }
        }
        

        欢迎评论!

        【讨论】:

        • 您还可以使用变体并为每个事务创建一个存储库实例,将其放在 using 语句中并让 Dispose() 提交事务。这将抽象出需要知道调用者方法中的事务。
        • 仅作记录,我相信您可以使用事务范围,只要您只使用单个 SQL Server,它就不会升级为 DTC。我相信这在过去 4 年的某个时候发生了变化
        • 让一个事务跨越多个存储库方法,然后呢?
        【解决方案6】:

        您可以将事务参数添加到要在事务中运行的方法的末尾,并为其指定默认值 null。因此,如果您不想在现有事务中运行该方法,请不要使用 end 参数或显式传递 null。

        在这些方法中,您可以检查参数是否为空,以确定是创建新事务还是使用传入的事务。此逻辑可以推送到基类。

        这使您的方法比使用基于上下文的解决方案更纯粹,尽管后者可能更适合通用库。但是,在独立应用程序中,您知道哪些方法需要在事务中链接起来,而且不会是所有方法。

        void Update(int itemId, string text, IDbTransaction trans = null) =>
           RunInTransaction(ref trans, () =>
           {
              trans.Connection.Update("...");
           });
        
        void RunInTransaction(ref IDbTransaction transaction, Action f)
        {
            if (transaction == null)
            {
                using (var conn = DatabaseConnectionFactory.Create())
                {
                    conn.Open();
        
                    using (transaction = conn.BeginTransaction())
                    {
                        f();
        
                        transaction.Commit();
                    }
                }
            }
            else
            {
                f();
            }
        }
        
        Update(1, "Hello World!");
        Update(1, "Hello World!", transaction);
        

        然后你可以为你的服务层设置一个事务运行器...

        public class TransactionRunner : ITransactionRunner
        {
            readonly IDatabaseConnectionFactory databaseConnectionFactory;
        
            public TransactionRunner(IDatabaseConnectionFactory databaseConnectionFactory) =>
                this.databaseConnectionFactory = databaseConnectionFactory;
        
            public void RunInTransaction(Action<IDbTransaction> f)
            {
                using (var conn = databaseConnectionFactory.Create())
                {
                    conn.Open();
        
                    using (var transaction = conn.BeginTransaction())
                    {
                        f(transaction);
        
                        transaction.Commit();
                    }
                }
            }
        
            public async Task RunInTransactionAsync(Func<IDbTransaction, Task> f)
            {
                using (var conn = databaseConnectionFactory.Create())
                {
                    conn.Open();
        
                    using (var transaction = conn.BeginTransaction())
                    {
                        await f(transaction);
        
                        transaction.Commit();
                    }
                }
            }
        }
        

        服务方法可能看起来像这样......

        void MyServiceMethod(int itemId, string text1, string text2) =>
           transactionRunner.RunInTransaction(trans =>
           {
              repos.UpdateSomething(itemId, text1, trans);
              repos.UpdateSomethingElse(itemId, text2, trans);
           });
        

        这很容易模拟单元测试...

        public class MockTransactionRunner : ITransactionRunner
        {
            public void RunInTransaction(Action<IDbTransaction> f) => f(null);
            public Task RunInTransactionAsync(Func<IDbTransaction, Task> f) => f(null);
        }
        

        【讨论】:

          【解决方案7】:

          来自 Eric Evans,DDD 书籍,第 6 章,存储库:

          将事务控制权留给客户端。尽管存储库将 从数据库中插入和删除,通常不会 承诺任何事情。保存后提交是很有诱惑力的,例如, 但客户可能具有正确启动和 提交工作单元。事务管理会更简单,如果 REPOSITORY 不插手。

          让更高层做事务管理很不错:

          • 在处理两个或多个聚合根时,您必须确保两者处于一致状态。
          • 当存储库操作出错时,客户端代码通常有更多的上下文来执行操作。
          • 存储库始终专注于检索/更新特定实体(通常是聚合根)的任务。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-02-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-02-21
            • 1970-01-01
            • 1970-01-01
            • 2014-03-03
            相关资源
            最近更新 更多