可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Combining Unit of Work
and Repository Pattern
is something used fairly widely nowadays. As Martin Fowler says a purpose of using UoW
is to form a Business Transaction while 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:
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:
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 Transaction for 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.
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!
回答1:
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.
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.
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.
The entire implementation of the UnitOfWorkScope
is available at: http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/
回答2:
Make your repositories members of your UoW. Don't let your repositories 'see' your UoW. Let UoW handle the transaction.
回答3:
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:
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.
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".
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
.
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 could add code in a custom repository that uses that DbContext
. So I guess my "wrapper" is a bit leaky...
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...
Examples of passing the DbContext:
Implementing the Repository and Unit of Work
Repository and Unit of Work in Entity Framework
John Papa's original source code
回答4:
In .NET, data access components typically automatically enlist to ambient transactions. Hence, saving changes intra-transactionally becomes separated from comitting the transaction to persist the changes.
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#:
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.
}
回答5:
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.
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.
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.
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.
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.
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.
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.
回答6:
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.
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.
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.
回答7:
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 any storage at all. Hence the term Domain Model.
Unit of Work is present in the Domain Model Implementation layer. 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.
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:
- 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 parent transaction, if the aggregate is not the initiator of the scope, but is part of it.