渴望加载和存储库模式

发布于 2024-08-09 06:29:33 字数 1102 浏览 7 评论 0原文

我想知道使用存储库模式时如何正确处理复杂对象图的急切加载问题。我猜这不是 ORM 特定的问题。

第一次尝试:

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

这会很好地工作,但这会涉及到一直重复自己(在任何地方的存储库实现中编写自定义“With”方法)。

下一种方法:

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

With 方法会将一个项目添加到私有集合中,稍后将使用该项目来找出在检索必要的实体时应该急切加载哪些道具。

这种方法有效并且很好。但我不喜欢使用:

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

基本上 - 问题是没有链接。我希望它是这样的:

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

我无法实现这一点。即使我可以 - 我不确定该解决方案是否优雅。

这导致我认为我错过了一些基本的东西(任何地方都缺乏例子)。有不同的方法来处理这个问题吗?什么是最佳实践?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

梦在深巷 2024-08-16 06:29:33

有趣的问题,我相信你不是第一个遇到这个问题的人(我绝对有)。

对我来说,真正的问题是:您想将急切加载逻辑放在哪里?

在客户端代码的存储库之外,

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

我认为这不是好的软件设计:如果这样的构造分散在整个应用程序中,看起来这可能会导致“千刀万剐”。

或者在存储库中。示例:

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

因此,您的客户端代码将如下所示:

var product = productRepository.GetByIdWithCustomers(id);

通常我会创建一个 BaseRepository,其中只定义了基本的 CRUD 操作:

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
}

然后我扩展此基类/接口,以便提供用于获取域对象的特定方法。您的方法似乎朝着有些相似的方向发展。

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

好处是:所有 ORM 内容(预加载配置、获取深度等)都封装在 Repository 类中,客户端代码只获取结果集。

我尝试使用非常通用的存储库,就像您尝试做的那样,但我最终大多为我的域对象编写特定的查询和存储库。

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.

鹿港小镇 2024-08-16 06:29:33

这是一个老问题,但也许它可以帮助某人。我花了一些时间找到一个好的方法,这是我在 C# 中找到的:

IRepository.cs:

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , params Expression<Func<TEntity, object>>[] properties);
}

Repository.cs

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{    
    private readonly DbSet<TEntity> _dbset;

    public Repository(DbSet<TEntity> dbset)
    {
        _dbset = dbset;
    }

    public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , Expression<Func<TEntity, object>>[] properties)
    {
        if (where == null) 
            throw new ArgumentNullException(nameof(where));    
        if (properties == null) 
            throw new ArgumentNullException(nameof(properties));

        var query = _dbset as IQueryable<TEntity>; // _dbSet = dbContext.Set<TEntity>()

        query = properties
                   .Aggregate(query, (current, property) => current.Include(property));

        return query.AsNoTracking().Where(where).ToList();
    }
}

如何使用:

var repository = new Repository<User>();
var users = repository.GetAll(p => p.Id == 1, d => d.Address, d => d.Carts);

参考:链接

It's a old question but perhaps it can help someone. I've spent sometime to find a good aproach, here is what I've found in C#:

IRepository.cs:

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , params Expression<Func<TEntity, object>>[] properties);
}

Repository.cs

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{    
    private readonly DbSet<TEntity> _dbset;

    public Repository(DbSet<TEntity> dbset)
    {
        _dbset = dbset;
    }

    public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , Expression<Func<TEntity, object>>[] properties)
    {
        if (where == null) 
            throw new ArgumentNullException(nameof(where));    
        if (properties == null) 
            throw new ArgumentNullException(nameof(properties));

        var query = _dbset as IQueryable<TEntity>; // _dbSet = dbContext.Set<TEntity>()

        query = properties
                   .Aggregate(query, (current, property) => current.Include(property));

        return query.AsNoTracking().Where(where).ToList();
    }
}

How to use:

var repository = new Repository<User>();
var users = repository.GetAll(p => p.Id == 1, d => d.Address, d => d.Carts);

Ref: Link

素衣风尘叹 2024-08-16 06:29:33
var product = productRepository
 .With(x=>x.Customer)
 .With(x=>x.Price)
 .With(x=>x.Manufacturer)
 .GetById(id);

我可以理解您希望像上面那样确定对象图的查询深度,但我认为可能有一种更简单的方法来做到这一点。与其选择通过 ID 退回产品(包含客户、价格和制造商),不如直接退回产品,而所有其他内容都是产品的延迟加载属性?

我通过数据访问层中的 POCO 对象模型“链接”来实现这种“完整的图形可访问性”。这样我就不需要知道在任何时候要提取多少急切加载的数据,我只需从对象图中询问我需要什么,并且模型知道加载了什么以及需要从 DAL 中额外恢复什么。看看 这些 三个 答案 - 我尝试在那里解释我的方法。如果您需要更多说明,请告诉我,我将编辑此答案。

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.

爱本泡沫多脆弱 2024-08-16 06:29:33

我可以理解您正在尝试做的事情,但您在某种程度上超出了基本存储库模式。

最小的存储库接口可能包括以下方法:

  • GetById
  • 添加
  • 删除

如果您在此基础上添加其他方法,您就会开始遇到该接口不一定对所有聚合根都有意义的情况。

有时拥有一个完全漂亮的 API 是不可行的。如果你所拥有的对你来说“足够好”,我就会接受。如果您需要摆脱存储库模式以提供更好的 API 来进行编程,那就去做吧!

存储库模式不是万能的/最终的解决方案。有时您需要不同的解决方案。

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.

陈甜 2024-08-16 06:29:33

如果您想指示存储库之外所需的所有包含,您可以为每个通用方法列出可选参数(C#):

TEntity Find(Func<TEntity, bool> expression, params string[] eagerLoads);

然后在您的客户端层:

IProductRepository.Find(x => x.Id == id, "Customer", "Price")

如果您想类型安全,请枚举您的实体:

public enum BusinessEntities { Customer, Price, Manufacturer }

IProductRepository.Find(x => x.Id == id, BusinessEntities.Customer.ToString(), BusinessEntities.Price.ToString())

我认为客户有责任具体询问其想要的东西。通用存储库应该只处理基本的 CRUD。

If you want to indicate all of the Includes you need outside of your repository, you can list optional params (C#) for each generic method:

TEntity Find(Func<TEntity, bool> expression, params string[] eagerLoads);

Then on your client tier:

IProductRepository.Find(x => x.Id == id, "Customer", "Price")

If you want to be type safe, enumerate your entities:

public enum BusinessEntities { Customer, Price, Manufacturer }

IProductRepository.Find(x => x.Id == id, BusinessEntities.Customer.ToString(), BusinessEntities.Price.ToString())

I think that it is the client's responsibility to ask specifically for what it wants. Generic Repository should just handle basic CRUD.

つ低調成傷 2024-08-16 06:29:33

BaseRepository.cs 中,您可以创建此方法:

public async Task<IEnumerable<T>> GetWithChild(string child)
{
    return await _entities.Include(child).ToListAsync();
}

在我的 API 中,我还实现了一个服务层,但从 API 中,我只需调用此方法并向其传递要加载的变量的名称。

显然,在您的情况下,您需要包含更多字符串。

In the BaseRepository.cs you can create this method:

public async Task<IEnumerable<T>> GetWithChild(string child)
{
    return await _entities.Include(child).ToListAsync();
}

In my API I have also implemented a service layer but from the API I simply call this method and pass it the name of the variable to load.

Obviously, in your situation, you'll need to include a few more strings.

允世 2024-08-16 06:29:33

我之前发布了一个答案,但我仍然对解决方案不满意。所以这里有一个更好的解决方案。

在 BaseRepository.cs 中

public async Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] properties)
{
      IQueryable<T> query = _entities;

      query = properties.Aggregate(query, (current, property) => current.Include(property));

      return await query.AsNoTracking().ToListAsync();
}

,您可以简单地使用如下方法

await _service.GetAll(x => x.Customer, x => x.Price, x => x.Manufacturer); 

I posted an answer earlier but I still wasn't happy with the solution. So here is a better solution.

in the BaseRepository.cs

public async Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] properties)
{
      IQueryable<T> query = _entities;

      query = properties.Aggregate(query, (current, property) => current.Include(property));

      return await query.AsNoTracking().ToListAsync();
}

and you can simply use the method as follows

await _service.GetAll(x => x.Customer, x => x.Price, x => x.Manufacturer); 
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文