views:

412

answers:

3

I have seen unit of work pattern implemented with something like a following code:

    private HashSet<object> _newEntities = new HashSet<object>();
    private HashSet<object> _updatedEntities = new HashSet<object>();
    private HashSet<object> _deletedEntities = new HashSet<object>();

and then there are methods for adding entities to each of these HashSets.

On Commit UnitOfWork creates some Mapper instance for each entity and calls Insert, Update, Delete methods from some imaginary Mapper.

The problem for me with this approach is: the names of Insert, Update, Delete methods are hard-coded, so it seems such a UnitOfWork is capable only of doing simple CRUD operations. But what if I need the following usage:

UnitOfWork ouw = new UnitOfWork();
uow.Start();

ARepository arep = new ARepository();
BRepository brep = new BRepository(); 

arep.DoSomeNonSimpleUpdateHere();
brep.DoSomeNonSimpleDeleteHere();

uow.Commit();

Now the three-HashSet approach fails because I then I could register A and B entities only for Insert, Update, Delete operations but I need those custom operations now.

So it seems that I cannot always stack the Repository operations and then perform them all with UnitOfWork.Commit();

How to solve this problem? The first idea is - I could store addresses of methods arep.DoSomeNonSimpleUpdateHere(); brep.DoSomeNonSimpleDeleteHere(); in UoW instance and execute them on uow.Commit() but then I have also to store all the method parameters. That sounds complicated.

The other idea is to make Repositories completely UoW-aware: In DoSomeNonSimpleUpdateHere I can detect that there is a UoW running and so I do not perform DoSomeNonSimpleUpdateHere but save the operation parameters and 'pending' status in some stack of the Repository instance (obviously I cannot save everything in UoW because UoW shouldn't depend on concrete Repository implementations). And then I register the involved Repository in the UoW instance. When UoW calls Commit, it opens a transaction, and calls some thing like Flush() for each pending Repository. Now every method of Repository needs some stuff for UoW detection and operation postponing for later Commit().

So the short question is - what is the easiest way to register all the pending changes in multiple repositories in UoW and then Commit() them all in a single transaction?

+1  A: 

The fact that you have this problem suggests that you aren't using the Repository pattern as such, but something more like multiple table data gateways. Generally, a repository is for loading and saving an aggregate root. As such, when you save an entity, your persistence layer saves all the changes in that aggregate root entity instance's object graph.

If, in your code, you have roughly one "repository" per one table (or Entity), you're probably actually using a table data gateway or a data transfer object. In that case, you probably need to have a means of passing in a reference to the active transaction (or the Unit of Work) in each Save() method.

In Evans DDD book, he recommends leaving transaction control to the client of a repository, and I would agree that it's not a good practice, though it may be harder to avoid if you're actually using a table data gateway pattern.

JasonTrue
I guess you are right - it seems I have mixed together Repository and Table Data Gateway. Do you suggest then to have a Repository for processing identifiable domain object roots and a separate TableDataGateway for some tricky queries which may modify many records in a time? And so TableDataGateway cannot particiapte in the UnitOfWork.
Martin
I recommend finding a way to use the repository as intended: to model the complicated domain behavior that you appear to have. If you have multiple affected entities for a common type of change, figure out which one is the aggregate root and build in methods to support those changes. (Consider looking at the DDD chapter on "Bounded Context" if that seems insufficient). Generally, when I start to see complicated, error-prone code emerge in a system, I assume I'm modeling the problem incorrectly until I can prove otherwise.
JasonTrue
+2  A: 

It would seem that even complicated updates can be broken down into a series of modifications to one or more DomainObjects. Calling DoSomeNonSimpleUpdateHere() may modify several different DomainObjects, which would trigger corresponding calls to UnitOfWork.registerDirty(DomainObject) for each object. In the sample code below, I have replaced the call to DoSomeNonSimpleUpdateHere with code that removes inactive users from the system.

UnitOfWork uow = GetSession().GetUnitOfWork();
uow.Start();

UserRepository repository = new UserRespository();
UserList users = repository.GetAllUsers();

foreach (User user in users)
{
  if (!user.IsActive())
    users.Remove( user );
}

uow.Commit();

If you are concerned about having to iterate over all users, here is an alternative approach that uses a Criteria object to limit the number of users pulled from the database.

UnitOfWork uow = GetSession().GetUnitOfWork();
uow.Start();

Repository repository = new UserRespository();
Criteria inactiveUsersCriteria = new Criteria();
inactiveUsersCriteria.equal( User.ACTIVATED, 0 );
UserList inactiveUsers = repository.GetMatching( inactiveUsersCriteria );
inactiveUsers.RemoveAll();

uow.Commit();

The UserList.Remove and UserList.RemoveAll methods will notify the UnitOfWork of each removed User. When UnitOfWork.Commit() is called, it will delete each User found in its _deletedEntities. This approach allows you to create arbitrarily complex code without having to write SQL queries for each special case. Using batched updates will be useful here, since the UnitOfWork will have to execute multiple delete statements instead of only one statement for all inactive users.

Michael Venable
The problem may be related to actions which run queries like this:DELETE FROM users WHERE activated = 0So now to use UoW I should first "SELECT id FROM users WHERE activated = 0", and then add those IDs to _deletedEntities which would get deleted on uow.Commit();But that SELECT seems to be a waste of resources.I wanted to avoid having two objects (Repository and TableDataGateway) for operating on the same table that is why I thought - repository might be enough. It looks like I was wrong.
Martin
I updated the post to include the example of deleting inactive users. The idea of using delegates is interesting too. If anything, I hope I provided an alternative viewpoint. Good luck.
Michael Venable
Thanks, it was a good example. The only thing that bothers me is that I still have to read first and then delete later - two queries instead of one. From the other hand - if I just delete then my identity map gets screwed up and I have many invalid entities in my domain model until I force reloading all of them.But I guess that is the price of OR/M and UnitOfWork if not using some more complex approach which I would like to avoid (like delegates).
Martin
A: 

I finally found this one:

http://www.goeleven.com/Blog/82

The author solves the problem using three Lists for update/insert/delete, but he does not store entities there. Instead repository delegates and their parameters are stored. So on Commit the author calls each registered delegate. With this approach I could register even some complex repository methods and so avoid using a separate TableDataGateway.

Martin