51

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

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

4

7 回答 7

20

今天早上启动我的电脑时,我遇到了我正在从事的项目的确切问题。我有一些想法导致了以下设计 - 并且评论会非常棒。不幸的是,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

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

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

/// <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();
        }
    }
}

评论表示赞赏!

于 2009-02-23T11:51:36.997 回答
11

我会考虑使用某种类型的事务范围/上下文系统。因此,您可能拥有以下大致基于 .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 事务。

于 2009-02-22T16:20:30.463 回答
6

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

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

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

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

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

于 2009-02-22T18:14:30.877 回答
5

使用 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的文档。

于 2009-02-22T16:36:16.183 回答
3

你想看看实现工作单元模式。NHibernate 有一些实现。一个是在 Rhino Commons 项目中,还有 Machine.UoW。

于 2009-02-22T16:22:53.047 回答
1

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

在这些方法中,您可以检查参数是否为 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);
}
于 2019-06-01T17:08:51.833 回答
0

来自 Eric Evans,DDD Book,CH 6,存储库:

将事务控制权留给客户端。虽然 REPOSITORY 会插入和删除数据库,但它通常不会提交任何内容。例如,在保存后提交是很诱人的,但客户端可能具有正确启动和提交工作单元的上下文。如果 REPOSITORY 不动手,事务管理会更简单。

让更高层进行事务管理很好:

  • 在处理两个或多个聚合根时,您必须确保两者处于一致状态。
  • 当存储库操作出现错误时,客户端代码通常有更多的上下文来操作。
  • 存储库始终专注于检索/更新特定实体的任务,通常是聚合根。
于 2021-10-07T03:14:24.247 回答