62

结合Unit of WorkRepository Pattern是当今相当广泛使用的东西。正如 Martin Fowler所说,使用的目的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存储更改的公共方法。此外,由于每个存储库都有一个共享实例,因此每个存储库UoWRepository可以访问CommitUoW 上的方法。由一个存储库调用它会使所有其他存储库也存储它们的更改;因此结果整个事务的概念崩溃了:

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受信任的架构比受信任的开发人员更可靠!

4

8 回答 8

31

我同意你的担忧。我更喜欢有一个环境工作单元,其中打开一个工作单元的最外层函数是决定是提交还是中止的函数。被调用的函数可以打开一个工作单元范围,如果有一个,它会自动加入环境 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()如果这是根(最远)范围,则调用只会对数据库进行实际保存。否则,它被解释为允许根范围保存更改的“好的投票”。

的完整实现UnitOfWorkScope可在以下网址获得: http: //coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

于 2013-10-28T09:07:16.167 回答
13

使您的存储库成为您的 UoW 的成员。不要让您的存储库“看到”您的 UoW。让 UoW 处理交易。

于 2014-07-16T21:53:39.880 回答
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)
    }
}

编辑

发完这个之后,我重新思考了一下。在实现中公开 Add 方法UnitOfWork意味着它是两种模式的组合。

我在自己的代码中使用实体框架,并且在DbContext那里使用的被描述为“工作单元和存储库模式的组合”。

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

主要区别在于我没有将 传递UnitOfWork给存储库,而是传递DbContext. 这确实意味着 可以BaseRepository访问SaveChanges. DbContext并且由于意图是自定义存储库应该继承BaseRepository,因此它们也可以访问 a DbContext。因此,开发人员可以在使用该DbContext. 所以我想我的“包装”有点漏水......

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

传递 DbContext 的示例:

实施存储库和工作单元

实体框架中的存储库和工作单元

约翰爸爸的原始源代码

于 2013-10-29T13:16:54.833 回答
3

意识到自从有人问这个问题已经有一段时间了,人们可能已经因年老而死,被转移到管理层等,但这里就可以了。

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

  1. 实现 Fowler's P of EAA book 中描述的工作单元接口,但将存储库注入到每个 UoW 方法中。
  2. 将工作单元注入到每个存储库操作中。
  3. 每个存储库操作调用适当的 UoW 操作并注入自身。
  4. 在存储库中实现两阶段提交方法 CanCommit()、Commit() 和 Rollback()。
  5. 如果需要,UoW 上的提交可以在每个存储库上运行 Commit,也可以提交到数据存储本身。如果这是您想要的,它还可以实现 2 阶段提交。

完成此操作后,您可以根据实现存储库和 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 不需要由每个存储库存储,这意味着存储库上的 Commit() 可以被存根,UoW 上的 Commit 执行实际的数据库提交。

于 2017-10-25T20:44:24.617 回答
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.
}
于 2013-10-23T17:49:16.403 回答
2

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

public class GenericRepository<T> where T : class
{
  private MyDatabase _Context;
  private DbSet<T> dbset;

  public GenericRepository(MyDatabase context)
  {
    _Context = context;
    dbSet = context.Set<T>();
  }

  public T Get(int id)
  {
    return dbSet.Find(id);
  }

  public IEnumerable<T> GetAll()
  {
    return dbSet<T>.ToList();
  }

  public IEnumerable<T> Where(Expression<Func<T>, bool>> predicate)
  {
    return dbSet.Where(predicate);
  }
  ...
  ...
}

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

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

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

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

例子:

public interface ITable1 : IGenericRepository<table1>
{
}

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

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

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

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

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

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

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

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

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

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

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

于 2016-09-12T11:11:27.957 回答
1

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

在某些应用程序中,领域模型和数据库实体是相同的,不需要在它们之间做任何数据映射。我们称它们为“域实体”。在此类应用程序中,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它所做的,并且将其用作工作单元实际上是 Microsoft 建议您使用 DDD 构建 .NET 应用程序的方式

实现存储库模式

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

存储库在域和数据映射层之间进行调解,就像内存中的域对象集合一样。

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

尽管此实现可能在多种情况下都有效,但它过分强调了集合部分,而对定义的中介部分则不够重视。

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

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

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

public class Person
{
    public void EnsureEnrollable(IPersonRepository repository)
    {
        if(!repository.IsEnrollable(this))
        {
            throw new BusinessException<PersonError>(PersonError.CannotEnroll);
        }
    }
}
public class PersonRepository : IPersonRepository
{
    private readonly PersonDbContext context;

    public IEnumerable<Person> GetAll()
    {
        return context.Persons.AsNoTracking()
            .Where(person => person.Active)
            .ProjectTo<Person>().ToList();
    }

    public Person Enroll(Person person)
    {
        person.EnsureEnrollable(this);
        context.Persons.Find(person.Id).Active = true;
        context.SaveChanges(); // UPDATE statement
        return person;
    }

    public bool IsEnrollable(Person person)
    {
        return context.Persons.Any(entity => entity.Id == person.Id && !entity.Active);
    }
}

商业交易

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

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

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

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

于 2021-01-13T01:44:08.537 回答
0

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

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

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

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

  • 如果聚合是范围的发起者,您需要支持将聚合保存为一个事务。
  • 如果聚合不是作用域的发起者,而是它的一部分,您需要支持将聚合保存为父事务的一部分。
于 2013-10-30T21:03:14.020 回答