工作单位+存储库模式:商业交易概念的下降
Unit of Work
和Repository Pattern
是当今相当普遍的用法。 正如马丁·福勒( Martin Fowler) 所言 ,使用物UoW
的目的是形成一个商业交易,同时UoW
知道存储库实际上是如何工作的(持续的无知)。 我已经回顾了许多实现; 并忽略具体的细节(具体/抽象类,接口……),它们或多或less类似于下面的内容:
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
可以被任何正在被注入的回购调用。
PS:我知道我可以告诉我的开发人员不要在Repository
调用Commit
,但可信的架构比可信任的开发人员更可靠!
我同意你的顾虑。 我更喜欢有一个工作单位,最外层的function是打开一个工作单元,决定是否提交或中止。 被调用的函数可以打开一个工作范围单元,如果有的话可以自动join到环境中,否则创build一个新的工作单元。
我使用的UnitOfWorkScope
的实现受到TransactionScope
如何工作的启发。 使用环境/范围的方法也消除了dependency injection的需要。
执行查询的方法如下所示:
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
的整个实现可在以下urlfind: 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
包装,一个用于Unit Of Work位,一个用于Repository位。 我在RepositoryBase
进行仓库封装。
关键的区别是我没有将UnitOfWork
传递给存储库,我传递了DbContext
。 这意味着BaseRepository
可以访问BaseRepository
上的SaveChanges
。 由于意图是自定义存储库应该inheritanceBaseRepository
,他们也可以访问一个DbContext
。 因此,开发人员可能会在使用该DbContext
的自定义存储库中添加代码。 所以我想我的“包装”是有点漏…
那么值得为DbContext
创build另一个包装器,它可以传递给存储库构造器来closures这个包装器呢? 不确定是…
传递DbContext的示例:
实施知识库和工作单位
entity framework中的存储库和工作单元
John Papa的原始源代码
在.NET中,数据访问组件通常会自动login到环境事务。 因此, 事务内部的保存更改与事务的保存分离以保持更改 。
换句话说,如果你创build一个事务范围,你可以让开发人员保存尽可能多的。 直到事务提交,数据库的可观察状态才会被更新(当然,可观察到的取决于事务隔离级别)。
这显示了如何在c#中创build事务作用域:
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. }
最近我也在研究这种devise模式,并且通过使用工作单元和通用存储库模式,我能够为存储库实现提取工作单元“保存更改”。 我的代码如下:
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); } ... ... }
基本上我们所做的就是传递数据上下文,并使用entity framework的dbSet方法来获取基本的Get,GetAll,Add,AddRange,Remove,RemoveRange和Where。
现在我们将创build一个通用接口来公开这些方法。
public interface <IGenericRepository<T> where T : class { T Get(int id); IEnumerable<T> GetAll(); IEnumerabel<T> Where(Expression<Func<T, bool>> predicate); ... ... }
现在我们要为entity framework中的每个实体创build一个接口,并从IGenericRepositoryinheritance,以便接口将期望在inheritance的存储库中实现方法签名。
例:
public interface ITable1 : IGenericRepository<table1> { }
你将遵循与你的所有实体相同的模式。 您还将在这些接口中添加特定于实体的任何function签名。 这将导致存储库需要实现GenericRepository方法和接口中定义的任何自定义方法。
对于知识库,我们将像这样实施它们。
public class Table1Repository : GenericRepository<table1>, ITable1 { private MyDatabase _context; public Table1Repository(MyDatabase context) : base(context) { _context = context; } }
在上面的示例库中,我创build了table1存储库,并inheritance了“table1”types的GenericRepository,然后从ITable1接口inheritance。 这将自动为我实现通用的dbSet方法,因此只允许我关注自定义存储库方法(如果有的话)。 当我将dbContext传递给构造函数时,我也必须将dbContext传递给基类Generic Repository。
现在,我将从这里开始创build工作单元库和接口。
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(); } }
我在自定义控制器上处理我的事务范围,使我的系统中的所有其他控制器都inheritance自己 这个控制器inheritance了默认的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(); } }
通过这种方式实现你的代码。 每次在动作开始时向服务器发出请求,都会创build一个新的UnitOfWork,并自动创build所有的存储库,并使其可以在控制器或类中的UoWvariables中访问。 这也将从您的存储库中删除您的SaveChanges(),并将其放置在UnitOfWork存储库中。 最后这个模式能够通过dependency injection在整个系统中只使用一个dbContext。
如果您担心单一上下文的父/子更新,则可以使用存储过程来更新,插入和删除函数,并为您的访问方法使用entity framework。
意识到这个问题已经有一段时间了,人们可能已经晚年死去,转到pipe理层等等,但是这里却是如此。
从数据库,事务控制器和两阶段提交协议中获取灵感,下列模式更改应该适用于您。
- 实现EAA书中Fowler's P中描述的工作单元界面,但是将资源库注入到每个UoW方法中。
- 将工作单元注入到每个存储库操作中。
- 每个存储库操作都会调用相应的UoW操作并注入自身。
- 在存储库中实现两个阶段提交方法CanCommit(),Commit()和Rollback()。
- 如果需要的话,在UoW上提交可以在每个存储库上运行Commit,或者它可以提交给数据存储本身。 它也可以实现一个2阶段提交,如果这是你想要的。
完成这些后,您可以根据您如何实现存储库和UoW来支持多种不同的configuration。 例如从没有事务的简单数据存储,单个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();
回到原来的post。 因为我们是将UoW注入每个repo操作的方法,所以UoW不需要存储在每个存储库中,这意味着Repository上的Commit()可以被删除,UoW上的Commit做实际的DB提交。
是的,这个问题是我关心的,这是我如何处理它。
首先,在我的理解领域模型不应该知道工作单位。 域模型由接口(或抽象类)组成,并不意味着事务存储的存在。 事实上,它根本不知道任何存储的存在。 因此,术语领域模型 。
工作单元出现在域模型实现层中。 我想这是我的术语,我的意思是通过合并数据访问层实现域模型接口的一个层。 通常,我使用ORM作为DAL,因此它内置了UoW(entity frameworkSaveChanges或SubmitChanges方法来提交挂起的更改)。 但是,那个属于DAL,并不需要任何发明者的魔力。
另一方面,你指的是你需要在域模型实现层中使用的UoW,因为你需要抽象掉“提交到DAL的变更”部分。 为此,我将与Anders Abel的解决scheme(recursionscropes)一起,因为这解决了两件事情需要一次性解决:
- 如果聚合是范围的发起者,则需要支持将聚合保存为一个事务。
- 如果聚合不是范围的发起者,但是它是其中的一部分,则需要支持将聚合保存为父事务的一部分。