[英]Unit of Work + Repository Pattern: The Fall of the Business Transaction Concept
结合Unit of Work
和Repository 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
来存储更改。 此外,因为每个存储库都有一个UoW
的共享实例, 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:我知道我可以告诉我的开发人员不要在Repository
调用Commit
但可信架构比可信开发人员更可靠!
我同意你的担忧。 我更喜欢有一个环境工作单元,其中打开工作单元的最外层函数是决定是提交还是中止的函数。 被调用的函数可以打开一个工作范围单元,如果有,它会自动加入环境 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/
使您的存储库成为 UoW 的成员。 不要让您的存储库“看到”您的 UoW。 让 UoW 处理交易。
不要传入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
被描述为“工作单元和存储库模式的组合”。
我认为最好将两者分开,这意味着我需要两个围绕DbContext
包装器,一个用于工作单元位,一个用于存储库位。 我将存储库包装在RepositoryBase
。
主要区别在于我没有将UnitOfWork
传递给存储库,而是传递DbContext
。 这是否意味着BaseRepository
有权访问SaveChanges
上DbContext
。 由于意图是自定义存储库应该继承BaseRepository
,因此它们也可以访问DbContext
。 因此,开发人员可以在使用该DbContext
的自定义存储库中添加代码。 所以我想我的“包装器”有点漏水......
那么是否值得为DbContext
创建另一个包装器,该包装器可以传递给存储库构造函数以关闭它? 不确定它是...
传递 DbContext 的示例:
意识到自从提出这个问题以来已经有一段时间了,人们可能会因年老而死亡,转移到管理层等,但这里是。
从数据库、事务控制器和两阶段提交协议中汲取灵感,对模式的以下更改应该适合您。
完成此操作后,您可以支持多种不同的配置,具体取决于您实现存储库和 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 执行实际的数据库提交。
在 .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.
}
我最近也在研究这种设计模式,通过利用工作单元和通用存储库模式,我能够为存储库实现提取工作单元“保存更改”。 我的代码如下:
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。
如果您担心具有单一上下文的父/子更新,您可以将存储过程用于更新、插入和删除功能,并为您的访问方法使用实体框架。
是的,这个问题是我关心的问题,这是我的处理方式。
首先,根据我的理解,域模型不应该了解工作单元。 领域模型由接口(或抽象类)组成,并不暗示事务存储的存在。 事实上,它根本不知道任何存储的存在。 因此,术语领域模型。
工作单元存在于领域模型实现层中。 我想这是我的术语,我的意思是通过合并数据访问层来实现域模型接口的层。 通常,我使用 ORM 作为 DAL,因此它带有内置的 UoW(实体框架 SaveChanges 或 SubmitChanges 方法来提交挂起的更改)。 但是,那是属于 DAL 的,不需要任何发明家的魔法。
另一方面,您指的是域模型实现层中需要的 UoW,因为您需要抽象出“向 DAL 提交更改”的部分。 为此,我会采用 Anders Abel 的解决方案(递归范围),因为它解决了您需要一次性解决的两件事:
在一些应用中,领域模型和数据库实体是完全相同的,它们之间不需要做任何数据映射。 我们称它们为“域实体”。 在此类应用程序中, 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
实现的工作单元模式, 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 时发生,因为这需要您定义一些聚合,这些聚合是可以被视为单个单元的域对象集群。 聚合的结构并不总是完美地映射到您的关系数据库模式,因为它可以根据您正在处理的用例提供多级抽象。
例如,聚合可能允许用户管理多个地址,但在另一个业务环境中,您可能希望扁平模型并将人员地址的建模限制为仅最新值:
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 应用程序的方式。 存储库是了解持久层实际实现的类,它将在事务结束时将所有数据库操作编排为提交/回滚。 调用保存更改时从另一个存储库保存更改也是工作单元模式的预期行为。 工作单元会累积所有存储库所做的所有更改,直到有人调用提交或回滚。 如果存储库对上下文进行了不希望在数据库中持久化的更改,那么问题不在于持久化这些更改的工作单元,而是执行这些更改的存储库。
但是,如果您的应用程序执行一次原子保存更改,从而持久保存来自多个存储库的更改操作,则可能违反了 DDD 设计原则之一。 存储库是与聚合的一对一映射,聚合是可被视为单个单元的域对象集群。 如果您使用多个存储库,那么您正在尝试在单个事务中修改多个数据单元。
要么你的聚合设计得太小,你需要做一个更大的来保存你的单个事务的所有数据,一个存储库将在单个事务中处理所有数据; 要么您试图进行跨越模型大部分的复杂事务,并且您将需要以最终的一致性来实现该事务。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.