views:

335

answers:

3

I'm wondering how to properly handle eager-loading problem for complex object graphs when using Repository pattern. This isn't ORM specific problem i guess.

First try:

public interface IProductRepository : IRepository<Product>
{
  Product GetById(int id);
  IProductRepository WithCustomers();
}

This would work fine, but that would involve repeating myself all the time (writing custom 'With' methods in repository implementations everywhere).

Next approach:

public interface IRepository<T> where T : IAggregateRoot
{
  ...
  void With(Expression<Func<T, object>> propToExpand);
}

With method will add an item to private collection which will be used later to find out what props should be eager loaded when retrieving necessary entity/ies.

This kind a works and is fine. But i dislike usage:

productRepository.With(x=>x.Customer);
productRepository.With(x=>x.Price);
productRepository.With(x=>x.Manufacturer);
var product = productRepository.GetById(id);

Basically - problem is that there isn't chaining. I would like it to be like this:

var product = productRepository
  .With(x=>x.Customer)
  .With(x=>x.Price)
  .With(x=>x.Manufacturer)
  .GetById(id);

I couldn't achieve this. Even if i could - i'm not sure if that solution would be elegant.

This leads to thoughts that i'm missing something fundamental (lack of examples anywhere). Are there different ways how to handle this? What are best practices?

A: 

I can appreciate what you are trying to do, but you are somewhat beyond the basic repository-pattern.

A minimal repository interface may include methods for:

  • GetById
  • Add
  • Remove

If you add additional methods on top of that, you start running into situations where the Interface doesn't necessarily make sense for all of your aggregate roots.

Sometimes it's just not feasible to have a completely beautiful API. If what you have works "good enough" for you, I would go with it. If you need to get away from the repository pattern to provide a better API to program against, do it!

The repository pattern isn't a be-all / end-all solution. Sometimes you need a different solution.

Michael Maddox
That's too controversial. DDD is all about OOP and putting everything in domain objects. How can it be that it provides zero guidance (if not from DDD directly then from those who apply it) on how to solve this easily predictable problem? I know - it's completely technical and DDD isn't about technical solutions, but it can't be that no one has been here. And i'm not saying that to handle this one must stretch the meaning of repository pattern. I just haven't anything that's "good enough".
Arnis L.
Software development isn't really about picking patterns up out of a book and applying them nor is it about always applying best practices. Software development is about using software to solve problems. The Domain-Driven Design book was published in 2003, but that doesn't mean everything there is to know about DDD is clearly known and documented now six years later. Nor does it mean that DDD and OOP are appropriate or applicable when solving all problems. I wish you luck in finding a satisfactory solution to your problem. That solution may not come from where you are expecting it.
Michael Maddox
Trust me, no one knows what software development really is. Including you. But that does not solve my problem. Just thought it's obvious that someone has faced this. I'm not trying to blindly apply one or another pattern. I'm looking for solutions just like you. And you can't deny that patterns are a good way how to restrict yourself, therefore - you can gain clarity when facing bunch of various ways how to achieve something. After all - that's why software development is hard - there's just too much options.
Arnis L.
+1  A: 

Interesting problem and I am sure you are not the first one having trouble with this (I absolutelty have).

For me, the real question is: where do you want to put your eager loading logic?

Outside of the repository in the client code

var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);

I dont think that is good software design: it looks like this could cause "death by a thousand cuts" if such constructs are scattered through your whole app.

Or within the repository. Example:

interface IProductRepository {
    Product GetById(int id);
    Product GetByIdWithCustomers(int i);
}

So your client code would look like this:

var product = productRepository.GetByIdWithCustomers(id);


Normally I make one BaseRepository which has just the basic CRUD operations defined:

public class BaseRepository<TEntity, TPrimaryKey> {
    public void Save(TEntity entity) { ... }
    public void Delete(TEntity entity) { ... }
    public TEntity Load(TPrimaryKey id) { ... } // just gets the entity by primary key
}

Then I extend this base Class / Interface in order to provide specific methods for fetching domain objects. Your approach seems to go in a somewhat similiar direction.

public class MediaRepository : BaseRepository<Media, int> {
    public long CountMediaWithCategories() { ... }
    public IList<Media> MediaInCategories(IList<Category> categories) { .... }
}

The good thing: all ORM stuff (eager loading config, fetch depth etc) is encapsulated in the Repository class, the client code just gets the result set.

I tried working with very generic repositories like you are trying to do, but I mostly ended up writing specific queries and repositories for my domain objects.

Max
Yeah. I see exposing eager loading strategy outside as quite bad too. But i don't want my method names to look like `GetByIdWithSomethingWithSomethingWithSomethingWithSomethingWithSomething...`. There are such a cases.
Arnis L.
That is understandable. I dont know what DB technology you are using, but too much eager fetching _might_ be a problem, too. Normally eager fetching in a RDBMS is represented as a LEFT OUTER JOIN which can depending on the size of the result set lead to big and slow cartesian products. But that only as a note. Sometimes it is necessary. Maybe you should rethink your aggregates? Will you really need more then GetEntityById() and GetInitializedEntityById()? An example would be interesting.
Max
There is a need to retrieve initialized entity collections too. I surely need to rethink my aggregates - they are wrong at the moment but i can't handle refactoring everything at once (particularly if changes involves need of really deep business knowledge). Anyway, thanks for some thoughts.
Arnis L.
+1  A: 
var product = productRepository
 .With(x=>x.Customer)
 .With(x=>x.Price)
 .With(x=>x.Manufacturer)
 .GetById(id);

I can understand your wish to determine the query depth of the object graph like above but i think there might be an easier way to do it. How about instead of choosing to return a Product (with Customer, Price and Manufacturer) by ID i simply return the Product - and all of those other things are lazy loaded properties of Product?

I achieve this 'complete graph accessibility' by 'chaining' by POCO object model in my data access layer. This way i don't need to know how much eager loaded data to pull out at any one time, i just ask for what i need from the object graph, and the model knows what is loaded and what needs recovering additionally from the DAL. Take a look at these three answers - i try to explain my approach there. If you need more clarification let me know and i'll edit this answer.

cottsak
Nah... i believe you got it wrong. I'm talking about eager loading. Your approach won't eliminate select n+1 problem when i'll iterate through collection of Product and do something with customer (basically - calling db in `for` cycle because of lazy loading). I'm looking for a nice way how to optionally avoid lazy loading if needed. Anyway - ty for your time. Upvote for that. :)
Arnis L.
Oh ok (i think i get it) - you really do want to explicitly define what id eager loaded with the example above? You want a Fluent Interface right?
cottsak
I can't get it working (example above) in generic way. And i don't think it's good to expose eager loading strategy outside. I'm just curious, how others handle this because i haven't find anything better than methods like GetProductWithCustomersWithAddressWithFooWithBar.
Arnis L.
Do you mind if i ask where the 'eager' part of the requirement has come from? It seems counter intuitive to me to be explicitly deciding on the eager-depth of a query at every query point (like example above). Why would you not want to put the decisions on query depth in one place and have your query framework use that every time?
cottsak
This is exactly what i'm looking for (putting decisions about query depth in one place). I just haven't seen widely accepted standard for that.
Arnis L.
That's what i'm saying. I don't think there is a widely accepted standard because the practise is counter intuitive. I'm not having a dig. I'm just interested in why there is such a requirement. Sorry i wasn't able to help more.
cottsak