工作单元+存储库模式:业务交易概念的衰落

时间:2013-10-23 17:32:55

标签: c# java architecture repository-pattern unit-of-work

结合Unit of WorkRepository Pattern是现在相当广泛使用的东西。正如Martin Fowler says一样,使用UoW的目的是形成商业交易,而不知道存储库实际上是如何工作的(持续无知)。我已经回顾了很多实现;并忽略具体细节(具体/抽象类,接口......),它们或多或少类似于以下内容:

public class RepositoryBase<T>
{
    private UoW _uow;
    public RepositoryBase(UoW uow) // injecting UoW instance via constructor
    {
       _uow = uow;
    }
    public void Add(T entity)
    {
       // Add logic here
    }
    // +other CRUD methods
}

public class UoW
{
    // Holding one repository per domain entity

    public RepositoryBase<Order> OrderRep { get; set; }
    public RepositoryBase<Customer> CustomerRep { get; set; }
    // +other repositories

    public void Commit()
    {
       // Psedudo code: 
       For all the contained repositories do:
           store repository changes.
    }
}

现在我的问题:

UoW公开了公开方法 Commit 来存储更改。此外,由于每个存储库都有UoW的共享实例,因此每个Repository都可以访问UoW上的方法Commit。通过一个存储库调用它会使所有其他存储库也存储其更改;因此,整个交易概念崩溃的结果是:

class Repository<T> : RepositoryBase<T>
{
    private UoW _uow;
    public void SomeMethod()
    {
        // some processing or data manipulations here
        _uow.Commit(); // makes other repositories also save their changes
    }
}

我认为一定不允许这样做。考虑到UoW(业务交易)的目的,方法Commit应仅暴露给启动业务交易的人,例如业务层。让我感到惊讶的是,我找不到任何解决这个问题的文章。在所有这些中Commit可以被任何注入的repo调用。

PS:我知道我可以告诉我的开发人员不要在Commit中调用Repository,但可靠的架构比可信赖的开发人员更可靠!

8 个答案:

答案 0 :(得分:26)

我同意你的担忧。我更喜欢有一个环境工作单元,其中打开工作单元的最外层函数是决定是提交还是中止的工作单元。调用的函数可以打开一个工作范围单元,如果有的话,它会自动登记在环境UoW中,或者如果没有,则创建一个新的工作范围。

我使用的UnitOfWorkScope的实施很大程度上取决于TransactionScope的工作原理。使用环境/范围方法也消除了依赖注入的需要。

执行查询的方法如下所示:

public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}

写入的方法如下所示:

using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}

请注意,uow.SaveChanges()调用仅对数据库执行实际保存(如果这是根(最))范围)。否则,它被解释为“okay vote”,允许根范围保存更改。

UnitOfWorkScope的整个实施位于:http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

答案 1 :(得分:9)

使您的存储库成为您的UoW的成员。不要让你的知识库看到&#39;你的UoW。让UoW处理交易。

答案 2 :(得分:4)

不要传递UnitOfWork,传入具有所需方法的界面。如果需要,您仍然可以在原始具体UnitOfWork实现中实现该接口:

public interface IDbContext
{
   void Add<T>(T entity);
}

public interface IUnitOfWork
{
   void Commit();
}

public class UnitOfWork : IDbContext, IUnitOfWork
{
   public void Add<T>(T entity);
   public void Commit();
}

public class RepositoryBase<T>
{
    private IDbContext _c;

    public RepositoryBase(IDbContext c) 
    {
       _c = c;
    }

    public void Add(T entity)
    {
       _c.Add(entity)
    }
}

修改

发布后我重新思考了。在UnitOfWork实现中公开Add方法意味着它是两种模式的组合。

我在自己的代码中使用Entity Framework,其中使用的DbContext被描述为“工作单元和存储库模式的组合”。

我认为最好将两者分开,这意味着我需要两个DbContext封装器,一个用于工作单元位,一个用于存储库位。我在RepositoryBase中进行了存储库包装。

关键区别在于我没有将UnitOfWork传递给存储库,我通过了DbContext。这确实意味着BaseRepository可以访问SaveChanges上的DbContext。由于自定义存储库的目的是继承BaseRepository,因此它们也可以访问DbContext。因此,开发人员可能在使用DbContext的自定义存储库中添加代码。所以我猜我的“包装”有点漏...

那么值得为DbContext创建另一个包装器,可以传递给存储库构造函数来关闭它吗?不确定它是......

传递DbContext的示例:

Implementing the Repository and Unit of Work

Repository and Unit of Work in Entity Framework

John Papa's original source code

答案 3 :(得分:2)

在.NET中,数据访问组件通常会自动登记到环境事务。因此,在事务内部保存更改分配事务以保持更改分开。。

换句话说 - 如果您创建了一个事务范围,您可以让开发人员尽可能多地保存。在提交事务之前,数据库的可观察状态将被更新(嗯,可观察的内容取决于事务隔离级别)。

这显示了如何在c#中创建事务范围:

using (TransactionScope scope = new TransactionScope())
{
    // Your logic here. Save inside the transaction as much as you want.

    scope.Complete(); // <-- This will complete the transaction and make the changes permanent.
}

答案 4 :(得分:2)

我最近也在研究这种设计模式,并利用工作单元和通用存储库模式,我能够为存储库实现提取工作单元“保存更改”。我的代码如下:

public interface <IGenericRepository<T> where T : class
{
  T Get(int id);
  IEnumerable<T> GetAll();
  IEnumerabel<T> Where(Expression<Func<T, bool>> predicate);
  ...
  ...
}

基本上我们所做的就是传递数据上下文并利用实体框架的dbSet方法来实现基本的Get,GetAll,Add,AddRange,Remove,RemoveRange和Where。

现在我们将创建一个通用接口来公开这些方法。

public interface ITable1 : IGenericRepository<table1>
{
}

现在我们想要为实体框架中的每个实体创建一个接口,并从IGenericRepository继承,以便接口期望在继承的存储库中实现方法签名。

示例:

public class Table1Repository : GenericRepository<table1>, ITable1
{
  private MyDatabase _context;

  public Table1Repository(MyDatabase context) : base(context)
  {
    _context = context;
  }
} 

您将对所有实体遵循相同的模式。您还将在这些接口中添加特定于实体的任何功能签名。这将导致存储库需要实现GenericRepository方法以及接口中定义的任何自定义方法。

对于存储库,我们将像这样实现它们。

public interface IUnitOfWork
{
  ITable1 table1 {get;}
  ...
  ...
  list all other repository interfaces here.

  void SaveChanges();
} 

public class UnitOfWork : IUnitOfWork
{
  private readonly MyDatabase _context;
  public ITable1 Table1 {get; private set;}

  public UnitOfWork(MyDatabase context)
  {
    _context = context; 

    // Initialize all of your repositories here
    Table1 = new Table1Repository(_context);
    ...
    ...
  }

  public void SaveChanges()
  {
    _context.SaveChanges();
  }
}

在上面的示例存储库中,我创建了table1存储库并继承了类型为“table1”的GenericRepository,然后我继承了ITable1接口。这将自动为我实现通用的dbSet方法,从而允许我只关注我的自定义存储库方法(如果有的话)。当我将dbContext传递给构造函数时,我还必须将dbContext传递给基本的Generic Repository。

现在我将从这里开始创建工作单元存储库和接口。

public class DefaultController : Controller
{
  protected IUnitOfWork UoW;

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    UoW = new UnitOfWork(new MyDatabase());
  }

  protected override void OnActionExecuted(ActionExecutedContext filterContext) 
  {
    UoW.SaveChanges();
  }
}

我在自定义控制器上处理我的事务范围,我的系统中的所有其他控制器都是从该控制器继承的。此控制器继承自默认的MVC控制器。

{{1}}

以这种方式实现您的代码。每次在操作开始时向服务器发出请求时,都会创建一个新的UnitOfWork,并自动创建所有存储库,并使它们可以访问控制器或类中的UoW变量。这也将从您的存储库中删除您的SaveChanges()并将其放在UnitOfWork存储库中。最后,这种模式只能通过依赖注入在整个系统中使用单个dbContext。

如果您担心具有单一上下文的父/子更新,您可以利用存储过程进行更新,插入和删除功能,并将实体框架用于您的访问方法。

答案 5 :(得分:1)

意识到已经有一段时间了,因为这被问到,人们可能已经过世,被转移到管理层等等,但这里也是如此。

从数据库,事务控制器和两阶段提交协议中获取灵感,以下对模式的更改应该适合您。

  1. 实施Fowler的EAA书中描述的工作单元接口,但将存储库注入每个UoW方法。
  2. 将工作单元注入每个存储库操作。
  3. 每个存储库操作都会调用相应的UoW操作并注入自身。
  4. 在存储库中实现两阶段提交方法CanCommit(),Commit()和Rollback()。
  5. 如果需要,提交UoW可以在每个存储库上运行Commit,或者它可以提交到数据存储本身。如果这是您想要的,它还可以实现2阶段提交。
  6. 完成此操作后,您可以根据实施存储库和UoW的方式支持许多不同的配置。例如来自没有事务的简单数据存储,单个RDBM,多个异构数据存储等。根据情况需要,数据存储及其交互可以在存储库中或在UoW中。

    interface IEntity
    {
        int Id {get;set;}
    }
    
    interface IUnitOfWork()
    {
        void RegisterNew(IRepsitory repository, IEntity entity);
        void RegisterDirty(IRepository respository, IEntity entity);
        //etc.
        bool Commit();
        bool Rollback();
    }
    
    interface IRepository<T>() : where T : IEntity;
    {
        void Add(IEntity entity, IUnitOfWork uow);
        //etc.
        bool CanCommit(IUnitOfWork uow);
        void Commit(IUnitOfWork uow);
        void Rollback(IUnitOfWork uow);
    }
    

    无论数据库实现如何,用户代码始终相同,如下所示:

    // ...
    var uow = new MyUnitOfWork();
    
    repo1.Add(entity1, uow);
    repo2.Add(entity2, uow);
    uow.Commit();
    

    回到原帖。因为我们是将UoW注入到每个repo操作中的方法,所以UoW不需要由每个存储库存储,这意味着Repository上的Commit()可以被存根,UoW上的Commit执行实际的数据库提交。

答案 6 :(得分:0)

是的,这个问题是我关注的问题,这就是我处理它的方式。

首先,在我的理解中,域模型不应该知道工作单元。域模型由接口(或抽象类)组成,并不意味着存在事务存储。事实上,它根本不知道任何存储的存在。因此,术语域模型

工作单元存在于域模型实现层中。我想这是我的术语,我的意思是通过合并数据访问层来实现域模型接口的层。通常,我使用ORM作为DAL,因此它带有内置的UoW(实体框架SaveChanges或SubmitChanges方法来提交挂起的更改)。但是,那个属于DAL并且不需要任何发明者的魔力。

另一方面,您指的是域模型实现层中需要的UoW,因为您需要抽象掉“将更改提交到DAL”的部分。为此,我会选择Anders Abel的解决方案(递归scropes),因为它解决了你需要解决的两件事一次性

  • 如果聚合是作用域的发起者,则需要支持将聚合保存为一个事务。
  • 如果聚合不是作用域的发起者,而是其中的一部分,则需要支持将聚合保存为事务的一部分。

答案 7 :(得分:0)

在一个非常简单的应用程序中

在一些应用中,领域模型和数据库实体是相同的,它们之间不需要做任何数据映射。我们称它们为“域实体”。在此类应用程序中,DbContext 可以同时充当存储库工作单元。我们可以简单地使用上下文,而不是做一些复杂的模式:

public class CustomerController : Controller
{
    private readonly CustomerContext context; // injected

    [HttpPost]
    public IActionResult Update(CustomerUpdateDetails viewmodel)
    {
        // [Repository] acting like an in-memory domain object collection
        var person = context.Person.Find(viewmodel.Id);

        // [UnitOfWork] keeps track of everything you do during a business transaction
        person.Name = viewmodel.NewName;
        person.AnotherComplexOperationWithBusinessRequirements();

        // [UnitOfWork] figures out everything that needs to be done to alter the database
        context.SaveChanges();
    }
}

大型应用上的复杂查询

如果您的应用程序变得更加复杂,您将开始编写一些大型 Linq 查询以访问您的数据。在这种情况下,您可能需要引入一个新层来处理这些查询,以防止您在控制器之间复制粘贴它们。在这种情况下,您最终将拥有两个不同的层,一个是由 DbContext 实现的工作单元模式,另一个是将简单地提供在前者之上执行的一些 Linq 结果的存储库模式。您的控制器应该调用存储库来获取实体,更改它们的状态,然后调用 DbContext 将更改持久化到数据库,但通过存储库对象代理 DbContext.SaveChanges() 是一种可接受的近似:

public class PersonRepository
{
    private readonly PersonDbContext context;
    public Person GetClosestTo(GeoCoordinate location) {} // redacted
}
public class PersonController
{
    private readonly PersonRepository repository;
    private readonly PersonDbContext context; // requires to Equals repository.context

    public IActionResult Action()
    {
        var person = repository.GetClosestTo(new GeoCoordinate());
        person.DoSomething();
        context.SaveChanges();
        // repository.SaveChanges(); would save the injection of the DbContext
    }
}

DDD 应用

当领域模型和实体是两组不同的类时,它会变得更有趣。这将在您开始实施 DDD 时发生,因为这需要您定义一些聚合,这些聚合是可以被视为单个单元的域对象集群。聚合的结构并不总是完美地映射到您的关系数据库架构,因为它可以根据您处理的用例提供多级抽象。

例如,聚合可能允许用户管理多个地址,但在其他业务环境中,您可能希望扁平化模型并将人员地址的建模限制为仅最新值:

public class PersonEntity
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsValid { get; set; }
    public ICollection<AddressEntity> Addresses { get; set; }
}

public class AddressEntity
{
    [Key]
    public int Id { get; set; }
    public string Value { get; set; }
    public DateTime Since { get; set; }
    public PersonEntity Person { get; set; }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CurrentAddressValue { get; private set; }
}

实施工作单元模式

首先让我们回到定义:

<块引用>

工作单元会跟踪您在可能影响数据库的业务事务期间所做的一切。完成后,它会计算出更改数据库所需执行的所有工作。

DbContext 会跟踪发生在实体上的每次修改,并在您调用 SaveChanges() 方法后将它们保存到数据库中。就像在更简单的示例中一样,工作单元正是 DbContext 所做的,而将其用作工作单元实际上是 how Microsoft suggest you'd structure a .NET application using DDD

实现存储库模式

再一次,让我们回到定义:

<块引用>

存储库在域和数据映射层之间起到中介作用,就像内存中的域对象集合。

DbContext 不能充当存储库。尽管它表现为实体的内存中集合,但它并不充当域对象的内存中集合。在这种情况下,我们必须为存储库实现另一个类,它将充当我们在内存中的域模型集合,并将数据从实体映射到域模型。但是,您会发现许多实现只是域模型中 DbSet 的投影,并提供类似 IList 的方法,这些方法只是将实体映射回并重现 DbSet<T> 上的操作。

虽然这种实现可能在多种情况下都有效,但它过分强调了集合部分,而在定义的中介部分还不够。强>

存储库是域层和基础设施层之间的中介,这意味着它的接口是在域层中定义的。接口中描述的方法在领域层中定义,它们都必须在程序的业务上下文中具有意义。无处不在的语言是 DDD 的核心概念,这些方法必须提供一个有意义的名称,也许“添加一个人”并不是为这个操作命名的正确商业方式。

此外,所有与持久性相关的概念都严格限于存储库的实现。该实现定义了给定的业务操作如何在基础设施层中转换为一系列实体操作,这些操作最终将通过一个原子数据库事务持久化到数据库中。另请注意,域模型上的 Add 操作不一定意味着数据库中的 INSERT 语句,并且 Remove 有时会以 UPDATE 甚至多个 { {1}} 条语句!

实际上,这是存储库模式的一个非常有效的实现:

INSERT
public class Person
{
    public void EnsureEnrollable(IPersonRepository repository)
    {
        if(!repository.IsEnrollable(this))
        {
            throw new BusinessException<PersonError>(PersonError.CannotEnroll);
        }
    }
}

业务交易

您是说使用工作单元的目的是形成业务交易,这是错误的。工作单元的目的是跟踪您在业务交易期间所做的可能影响数据库的所有事情,以由于您的工作而改变数据库原子操作中。存储库确实共享工作单元实例,但请记住,在注入 dbcontext 时,依赖项注入通常使用范围生命周期管理器。这意味着实例仅在同一个 http 请求上下文中共享,不同的请求不会共享更改跟踪。使用单例生命周期管理器将在不同的 http 请求之间共享实例,这将对您的应用程序造成严重破坏。

从存储库调用工作单元保存更改方法实际上是您实现 DDD 应用程序的方式。存储库是了解持久层实际实现的类,它将在事务结束时将所有数据库操作编排为提交/回滚。调用保存更改时从另一个存储库保存更改也是工作单元模式的预期行为。工作单元会累积所有存储库所做的所有更改,直到有人调用提交或回滚。如果存储库对上下文进行了不希望在数据库中持久化的更改,那么问题不在于持久化这些更改的工作单元,而是执行这些更改的存储库。

但是,如果您的应用程序执行一次原子保存更改以保留来自多个存储库的更改操作,则可能违反了 DDD 设计原则之一。存储库是与聚合的一对一映射,聚合是可被视为单个单元的域对象集群。如果您使用多个存储库,那么您就是在尝试在单个事务中修改多个数据单元。

要么您的聚合设计得太小,您需要制作一个更大的聚合来保存单个事务的所有数据,并使用一个存储库来处理单个事务中的所有数据;要么您正在尝试进行跨越模型大部分的复杂事务,并且您需要以最终的一致性来实现该事务。