简体   繁体   中英

Repository pattern: Implementation and lazy loading of model relationships

I have an application which deals with products and product categories. For each of these I have models defined using POCO.

// Represents a product.
class Product {
  public virtual int ID { get; set; }
  public virtual string Name { get; set; }
  public virtual ProductCategory Category { get; set; }
}

// Represents a product category.
class ProductCategory {
  public virtual int ID { get; set; }
  public virtual string Name { get; set; }
  public virtual IEnumerable<Product> Products { get; set; }
}

The application uses a repository to access these models

// The interface implemented by the application's repository
interface IProductRepository {
  IEnumerable<Product> GetAllProducts();

  void Add(Product product);
  void Remove(Product product);
  void Save(Product product);
}

In the Product class, the property named Category of type ProductCategory should be loaded only when it is needed/accessed (lazy-loading). I want my models to remain POCO and contain only the structure of the model.

Am I taking the right approach ?
Should I have only the ID of the category in the Product class and use a separate repository for product categories to load the category ?

Implementing lazy-loading and relationships
For now my implementation of the repository interface returns an object of a type which extends the Product type and has support for lazy-loading through the repository instance.

Who should be responsible for loading the product category ?

I'm interested in how the product and category repositories should interact to achieve the lazy-loading ? Should they reference each other or should I have a main repository with the two sub repositories and pass that to my extended model types ?

What approach would you take ? (any suggestions and criticism is welcomed)


I should note that I want the application to be extensible and all the interfaces for the repositories and the models themselves will be in a separate asembly. This means that the extender will not have direct access to the model class definition.

A few remarks and my opinions:

1)

Should I have only the ID of the category in the Product class and use a separate repository for product categories to load the category ?

No. You are using an ORM (at least I assume you do) to be able to model relationships by references between class instances and not by IDs you are using then to query in a relational fashion. Taking your idea to the last consequence would mean that you remove all navigation properties at all from the model classes and have only scalar properties and some of them act as keys between objects. That's only the "R" in ORM.

2)

For now my implementation of the repository interface returns an object of a type which extends the Product type and has support for lazy-loading through the repository instance.

Not sure what this means exactly. (I would like to see a code-snippet how you do that.) But my guess is that in your derived Product class you inject somehow a reference to the repository, like so:

public class ProductProxy : Product
{
    private IProductRepository _productRepo;

    public ProductProxy(IProductRepository productRepo)
    {
        _productRepo = productRepo;
    }

    // now you use _productRepo to lazily load something on request, do you?
}

Well, it's obviously a problem now to load the categories since IProductRepository doesn't have methods to access them.

3)

I'm interested in how the product and category repositories should interact to achieve the lazy-loading ? Should they reference each other or should I have a main repository with the two sub repositories and pass that to my extended model types ?

Your ProductRepository and CategoryRepository look like instances of a generic repository which is only responsible for a single entity type (in EF 4.1 this would be similar to DbSet<T> where T is Product or Category respectively).

I would avoid to have references between those repositories as this may end up in a hell of complex repo-references whenever you add new entities or navigation properties.

I see two other options:

  • (Basically what you already mentioned) Having a repository which is responsible for Product and Category together. You could still have your generic repositories but I would consider them more as internal helper repos and would only use them as private members inside of the main repository. This way you can have a group of repositories, each of them is responsible for some closely related entities.

  • Introduce a Unit of Work which is able to create all of your generic repositories (again in EF 4.1 this would be something like the factory method DbContext.Set<T>() where DbContext is the unit of work) and then inject this Unit of Work into your derived instances:

     public class ProductProxy : Product { private IUnitOfWork _unitOfWork; public ProductProxy(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public Category Category { get { // ... var productRepo = _unitOfWork.CreateGenericRepo<Product>(); var categoryRepo = _unitOfWork.CreateGenericRepo<Category>(); // you can pull out the repos you need and work with them } set { ... } } } 

I would prefer the second option because in the first option you may end up in huge repositories to support loading of all possible relationships. Think of: Order has OrderItems, OrderItem has Product, Product has Category, Order has Customer, Customer has list of Addresses, Address has list of Contact Persons and so on and so forth...

4) (because you were also asking for criticism)

Are you writing your own ORM or are your writing an application? Your design goes into a direction which may become very complex and you are reinventing the wheel in my opinion. If you planning to use EF or NHibernate (or other ORM's) then you are creating functions which are already available out of the box, you only put abstractions on top of it which add no value. Lazy loading through dynamic proxies happens transparently in the sense that you never work explicitely with those proxies in your code, you always work with your POCO entities. They are invisible and exist only at runtime. Why do you want to develop your own lazy loading infrastructure?

NHibernate supports lazy loading through proxy objects (using Castle DynamicProxy by default) that subclass the classes that are manipulated by the repository. NHibernate requires you to mark members as virtual to support this scenario. I'm unsure as to what component initiates the load call when required, but suspect it's the proxy instance.

I personally think that marking members as virtual is a small price for lazy loading functionality

I'm doing something very similar and in my case I have repositories for both product and product category. No one outside of the repository knows how things are loaded - lazy or whatever - so you can change it later if you want. In my case, I eagerly-load the categories since they're accessed so much, and I load-on-demand the products and cache them for an hour or so.

I want my models to remain POCO and contain only the structure of the model.

I would ask, why is the shape of the data the important about these classes?

Generally, this means you are simply exposing our database schema directly to your entire application, with a very thin veneer of a "model" on top of it.

I'd suggest that the data an entity contains is private and should be encapsulated, and that behavior is what should be the focus of the entities.

Furthermore, it is simpler and more clear to load all the data you will need at once. If the potential amount of data is too much, then I would suggest that you have too many responsibilities assigned to one entity and you will need to either further refine your model or add a mechanism to retrieve an entity based on the context in which you are using it.

What I mean is, if you are querying to display data, you need some set of data exposed. You may query a similar but different set of data in another context. Going further, if you need to perform an action or modification, your entity needs to expose a particular behavior. But in a different context - when you are retrieving the entity to perform a different action or modification, then you need a different particular behavior exposed.

Trying to combine all these different uses into one single contract or interface leads to, more or less, god object entities.

You only need that collection of Categories for certain displays and certain actions. It simply causes pain trying to have it exposed all the time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM