C# 工作单元 + 存储库模式:业务交易概念的堕落
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/19548531/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Unit of Work + Repository Pattern: The Fall of the Business Transaction Concept
提问by Alireza
Combining Unit of Work
and Repository Pattern
is something used fairly widely nowadays. As Martin Fowler saysa purpose of using UoW
is to form a Business Transactionwhile being ignorant of how repositories actually work (being persistent ignorant). I've reviewed many implementations; and ignoring specific details (concrete/abstract class, interface,...) they are more or less similar to what follows:
结合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.
}
}
Now my problem:
现在我的问题:
UoW
exposes public method Commit
to store the changes. Also, because each repository has a shared instance of UoW
, each Repository
can access method Commit
on UoW. Calling it by one repository makes all other repositories store their changes too; hence the result the whole concept of transaction collapses:
UoW
公开公共方法Commit
来存储更改。此外,因为每个存储库都有一个共享实例UoW
,每个都Repository
可以访问Commit
UoW 上的方法。通过一个存储库调用它会使所有其他存储库也存储它们的更改;因此,交易的整个概念崩溃了:
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
}
}
I think this must be not allowed. Considering the purpose of the UoW
(business transaction), the method Commit
should be exposed only to the one who started a Business Transactionfor example Business Layer. What surprised me is that I couldn't find any article addressing this issue. In all of them Commit
can be called by any repo being injected.
我想这一定是不允许的。考虑到UoW
(业务事务)的目的,该方法Commit
应该只公开给发起业务事务的人,例如业务层。令我惊讶的是,我找不到任何解决此问题的文章。所有这些Commit
都可以被任何被注入的 repo 调用。
PS:I know I can tell my developers not to call Commit
in a Repository
but a trusted Architecture is more reliable than trusted developers!
PS:我知道我可以告诉我的开发人员不要调用Commit
一个Repository
但受信任的架构比受信任的开发人员更可靠!
回答by lightbricko
In .NET, data access components typically automatically enlist to ambient transactions. Hence, saving changes intra-transactionallybecomes separated from comitting the transaction to persist the changes.
在 .NET 中,数据访问组件通常会自动加入环境事务。因此,在事务内保存更改与提交事务以持久化更改是分开的。
Put differently - if you create a transaction scope you can let the developers save as much as they want. Not until the transaction is committed the observable state of the database(s) will be updated (well, what is observable depends on the transaction isolation level).
换句话说 - 如果您创建一个事务范围,您可以让开发人员尽可能多地保存。直到事务被提交,数据库的可观察状态才会被更新(好吧,什么是可观察的取决于事务隔离级别)。
This shows how to create a transaction scope in c#:
这显示了如何在 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.
}
回答by Anders Abel
I do agree with your concerns. I prefer to have an ambient unit of work, where the outermost function opening a unit of work is the one that decides whether to commit or abort. Functions called can open a unit of work scope which automatically enlists in the ambient UoW if there is one, or creates a new one if there is none.
我同意你的担忧。我更喜欢有一个环境工作单元,其中打开工作单元的最外层函数是决定是提交还是中止的函数。被调用的函数可以打开一个工作范围单元,如果有,它会自动加入环境 UoW,如果没有,则创建一个新的。
The implementation of the UnitOfWorkScope
that I used is heavily inspired by how TransactionScope
works. Using an ambient/scoped approach also removes the need for dependency injection.
UnitOfWorkScope
我使用的实现很大程度上受到TransactionScope
工作原理的启发。使用环境/范围方法还消除了依赖注入的需要。
A method that performs a query looks like this:
执行查询的方法如下所示:
public static Entities.Car GetCar(int id)
{
using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
{
return uow.DbContext.Cars.Single(c => c.CarId == id);
}
}
A method that writes looks like this:
写入的方法如下所示:
using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
Car c = SharedQueries.GetCar(carId);
c.Color = "White";
uow.SaveChanges();
}
Note that the uow.SaveChanges()
call will only do an actual save to the database if this is the root (otermost) scope. Otherwise it is interpreted as an "okay vote" that the root scope will be allowed to save the changes.
请注意,uow.SaveChanges()
如果这是根(最远的)范围,则调用只会对数据库进行实际保存。否则,它被解释为“同意投票”,即允许根范围保存更改。
The entire implementation of the UnitOfWorkScope
is available at: http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/
的整个实现UnitOfWorkScope
可在:http: //coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/
回答by Colin
Don't pass in the UnitOfWork
, pass in an interface that has the methods you need. You can still implement that interface in the original concrete UnitOfWork
implementation if you want:
不要传入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)
}
}
EDIT
编辑
After posting this I had a rethink. Exposing the Add method in the UnitOfWork
implementation means it is a combination of the two patterns.
发完这篇文章后,我重新思考。在UnitOfWork
实现中公开 Add 方法意味着它是两种模式的组合。
I use Entity Framework in my own code and the DbContext
used there is described as "a combination of the Unit-Of-Work and Repository pattern".
我在自己的代码中使用实体框架,并且在DbContext
那里使用的被描述为“工作单元和存储库模式的组合”。
I think it is better to split the two, and that means I need two wrappers around DbContext
one for the Unit Of Work bit and one for the Repository bit. And I do the repository wrapping in RepositoryBase
.
我认为最好将两者分开,这意味着我需要两个包装器,DbContext
一个用于工作单元位,一个用于存储库位。我将存储库包装在RepositoryBase
.
The key difference is that I do not pass the UnitOfWork
to the Repositories, I pass the DbContext
. That does mean that the BaseRepository
has access to a SaveChanges
on the DbContext
. And since the intention is that custom repositories should inherit BaseRepository
, they get access to a DbContext
too. It is therefore possible that a developer couldadd code in a custom repository that uses that DbContext
. So I guess my "wrapper" is a bit leaky...
关键的区别在于我没有将 传递UnitOfWork
给存储库,而是传递DbContext
. 这是否意味着BaseRepository
有权访问SaveChanges
的DbContext
。由于意图是自定义存储库应该继承BaseRepository
,因此它们也可以访问DbContext
。因此,开发人员可以在使用该DbContext
. 所以我想我的“包装器”有点泄漏......
So is it worth creating another wrapper for the DbContext
that can be passed to the repository constructors to close that off? Not sure that it is...
那么是否值得为DbContext
可以传递给存储库构造函数以关闭它的另一个包装器?不确定它是...
Examples of passing the DbContext:
传递 DbContext 的示例:
Implementing the Repository and Unit of Work
Repository and Unit of Work in Entity Framework
回答by Tengiz
Yes, this question is a concern to me, and here's how I handle it.
是的,这个问题是我关心的问题,这是我的处理方式。
First of all, in my understanding Domain Model should not know about Unit of Work. Domain Model consists of interfaces (or abstract classes) that don't imply the existence of the transactional storage. In fact, it does not know about the existence of anystorage at all. Hence the term Domain Model.
首先,根据我的理解,域模型不应该知道工作单元。域模型由接口(或抽象类)组成,并不暗示事务存储的存在。事实上,它根本不知道任何存储的存在。因此,术语领域模型。
Unit of Work is present in the Domain Model Implementationlayer. I guess this is my term, and by that I mean a layer that implements Domain Model interfaces by incorporating Data Access Layer. Usually, I use ORM as DAL and therefore it comes with built-in UoW in it (Entity Framework SaveChanges or SubmitChanges method to commit the pending changes). However, that one belongs to DAL and does not need any inventor's magic.
工作单元存在于领域模型实现层中。我想这是我的术语,我的意思是通过合并数据访问层来实现域模型接口的层。通常,我使用 ORM 作为 DAL,因此它带有内置的 UoW(实体框架 SaveChanges 或 SubmitChanges 方法来提交挂起的更改)。但是,那是属于 DAL 的,不需要任何发明家的魔法。
On the other hand, you are referring to the UoW that you need to have in Domain Model Implementation layer because you need to abstract away the part of "committing changes to DAL". For that, I would go with Anders Abel's solution (recursive scropes), because that addresses two things you need to solve in one shot:
另一方面,您指的是域模型实现层中需要的 UoW,因为您需要抽象出“向 DAL 提交更改”的部分。为此,我会采用 Anders Abel 的解决方案(递归范围),因为它解决了您需要一次性解决的两件事:
- You need to support saving of aggregates as one transaction, if the aggregate is an initiator of the scope.
- You need to support saving of aggregates as part of the parenttransaction, if the aggregate is not the initiator of the scope, but is part of it.
- 如果聚合是范围的发起者,您需要支持将聚合保存为一个事务。
- 您需要支持将聚合保存为父事务的一部分,如果聚合不是作用域的发起者,而是它的一部分。
回答by Chalky
Make your repositories members of your UoW. Don't let your repositories 'see' your UoW. Let UoW handle the transaction.
使您的存储库成为 UoW 的成员。不要让您的存储库“看到”您的 UoW。让 UoW 处理交易。
回答by logan gilley
I too have been recently researching this design pattern and by utilizing the Unit Of Work and Generic Repository Pattern I was able to extract the Unit of Work "Save Changes" for the Repository implementation. My code is as follows:
我最近也在研究这种设计模式,通过利用工作单元和通用存储库模式,我能够为存储库实现提取工作单元“保存更改”。我的代码如下:
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);
}
...
...
}
Essentially all we are doing is passing in the data context and utilizing the entity framework's dbSet methods for basic Get, GetAll, Add, AddRange, Remove, RemoveRange, and Where.
本质上,我们所做的只是传入数据上下文并利用实体框架的 dbSet 方法进行基本的 Get、GetAll、Add、AddRange、Remove、RemoveRange 和 Where。
Now we will create a generic interface to expose these methods.
现在我们将创建一个通用接口来公开这些方法。
public interface <IGenericRepository<T> where T : class
{
T Get(int id);
IEnumerable<T> GetAll();
IEnumerabel<T> Where(Expression<Func<T, bool>> predicate);
...
...
}
Now we would want to create an interface for each entity in entity Framework and inherit from IGenericRepository so that the interface will expect to have the method signatures implemented within the inherited repositories.
现在我们希望为实体框架中的每个实体创建一个接口并从 IGenericRepository 继承,以便接口期望在继承的存储库中实现方法签名。
Example:
例子:
public interface ITable1 : IGenericRepository<table1>
{
}
You will follow this same pattern with all of your entities. You will also add any function signatures in these interfaces that are specific to the entities. This would result in the repositories needing to implement the GenericRepository methods and any custom methods defined in the interfaces.
您将对所有实体遵循相同的模式。您还将在这些接口中添加特定于实体的任何函数签名。这将导致存储库需要实现 GenericRepository 方法和接口中定义的任何自定义方法。
For the Repositories we will implement them like this.
对于存储库,我们将像这样实现它们。
public class Table1Repository : GenericRepository<table1>, ITable1
{
private MyDatabase _context;
public Table1Repository(MyDatabase context) : base(context)
{
_context = context;
}
}
In the example repository above I am creating the table1 repository and inheriting the GenericRepository with a type of "table1" then I inherit from the ITable1 interface. This will automatically implement the generic dbSet methods for me, thus allowing me to only focus on my custom repository methods if any. As I pass the dbContext to the constructor I must also pass the dbContext to the base Generic Repository as well.
在上面的示例存储库中,我创建了 table1 存储库并继承了类型为“table1”的 GenericRepository,然后我从 ITable1 接口继承。这将自动为我实现通用的 dbSet 方法,从而让我只关注我的自定义存储库方法(如果有的话)。当我将 dbContext 传递给构造函数时,我还必须将 dbContext 传递给基本的通用存储库。
Now from here I will go and create the Unit of Work repository and Interface.
现在,我将开始创建工作单元存储库和接口。
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();
}
}
I handle my transaction scope on a custom controller that all other controllers in my system inherit from. This controller inherits from the default MVC controller.
我在自定义控制器上处理我的事务范围,系统中的所有其他控制器都继承自该控制器。这个控制器继承自默认的 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();
}
}
By implementing your code this way. Every time a request is made to the server at the beginning of an action a new UnitOfWork will be created and will automatically create all the repositories and make them accessible to the UoW variable in your controller or classes. This will also remove your SaveChanges() from your repositories and place it within the UnitOfWork repository. And last this pattern is able to utilize only a single dbContext throughout the system via dependency injection.
通过这种方式实现你的代码。每次在操作开始时向服务器发出请求时,都会创建一个新的 UnitOfWork,并将自动创建所有存储库,并使它们可被控制器或类中的 UoW 变量访问。这还将从您的存储库中删除您的 SaveChanges() 并将其放置在 UnitOfWork 存储库中。最后,这种模式只能通过依赖注入在整个系统中使用单个 dbContext。
If you are concerned about parent/child updates with a singular context you could utilize stored procedures for your update, insert, and delete functions and utilize entity framework for your access methods.
如果您担心具有单一上下文的父/子更新,您可以将存储过程用于更新、插入和删除功能,并为您的访问方法使用实体框架。
回答by Patrick Farry
Realize it has been a while since this was asked, and people may have died of old age, transferred to management etc. but here goes.
意识到自从提出这个问题以来已经有一段时间了,人们可能会因年老而死亡,转移到管理层等,但这里是。
Taking inspiration from databases, transaction controllers and the two phase commit protocol, the following changes to the patterns should work for you.
从数据库、事务控制器和两阶段提交协议中汲取灵感,对模式的以下更改应该适合您。
- Implement the unit of work interface described in Fowler's P of EAA book, but inject the repository into each UoW method.
- Inject the unit of work into each repository operation.
- Each repository operation calls the appropriate UoW operation and injects itself.
- Implement the two phase commit methods CanCommit(), Commit() and Rollback() in the repositories.
- If required, commit on the UoW can run Commit on each repository or it can commit to the data store itself. It can also implement a 2 phase commit if that is what you want.
- 实现 Fowler's P of EAA book 中描述的工作单元接口,但将存储库注入到每个 UoW 方法中。
- 将工作单元注入每个存储库操作。
- 每个存储库操作调用适当的 UoW 操作并注入自身。
- 在存储库中实现两阶段提交方法 CanCommit()、Commit() 和 Rollback()。
- 如果需要,在 UoW 上提交可以在每个存储库上运行提交,也可以提交到数据存储本身。如果这是您想要的,它还可以实现两阶段提交。
Having done this, you can support a number of different configurations depending on how you implement the repositories and the UoW. e.g. from simple data store without transactions, single RDBMs, multiple heterogeneous data stores etc. The data stores and their interactions can be either in the repositories or in the UoW, as the situation requires.
完成此操作后,您可以支持多种不同的配置,具体取决于您实现存储库和 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);
}
User code is always the same regardless of the DB implementations and looks like this:
无论数据库实现如何,用户代码始终相同,如下所示:
// ...
var uow = new MyUnitOfWork();
repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();
Back to the original post. Because we are method injecting the UoW into each repo operation the UoW does not need to be stored by each repository, meaning Commit() on the Repository can be stubbed out, with Commit on the UoW doing the actual DB commit.
回到原来的帖子。因为我们是将 UoW 注入每个 repo 操作的方法,所以 UoW 不需要由每个存储库存储,这意味着 Repository 上的 Commit() 可以被删除,UoW 上的 Commit 执行实际的数据库提交。