【问题标题】:Eager loading and repository pattern急切加载和存储库模式
【发布时间】:2009-10-24 21:44:14
【问题描述】:

我想知道在使用存储库模式时如何正确处理复杂对象图的急切加载问题。我猜这不是 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 couldn't achieve this。即使我可以 - 我不确定该解决方案是否优雅。

这导致我认为我缺少一些基本的东西(任何地方都没有例子)。有不同的方法来处理这个吗?什么是最佳做法?

【问题讨论】:

  • 我遇到了类似的问题。你是怎么解决这个问题的?
  • @vondip 我没有使用存储库
  • 是的,我阅读了您的另一个答案,您在其中陈述了您的解决方案。听起来很公平。谢谢。
  • @vondip 在这里发布他的答案的链接?
  • @ArnisL。你用什么代替存储库?

标签: repository-pattern eager-loading


【解决方案1】:

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

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

存储库之外的客户端代码

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 类中,客户端代码只获取结果集。

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

【讨论】:

  • 是的。我认为在外面公开急切的加载策略也很糟糕。但我不希望我的方法名称看起来像GetByIdWithSomethingWithSomethingWithSomethingWithSomethingWithSomething...。有这样的情况。
  • 这是可以理解的。我不知道您使用的是什么数据库技术,但过于急切地获取 可能 也是一个问题。通常,RDBMS 中的急切获取表示为 LEFT OUTER JOIN,这取决于结果集的大小,会导致笛卡尔积大而慢。但这仅作为说明。有时这是必要的。也许你应该重新考虑你的聚合?除了 GetEntityById() 和 GetInitializedEntityById() 之外,您真的需要更多吗?一个例子会很有趣。
  • 也需要检索初始化的实体集合。我当然需要重新考虑我的聚合——它们目前是错误的,但我无法一次处理所有重构(特别是如果更改需要非常深入的业务知识)。无论如何,谢谢你的想法。
【解决方案2】:
var product = productRepository
 .With(x=>x.Customer)
 .With(x=>x.Price)
 .With(x=>x.Manufacturer)
 .GetById(id);

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

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

【讨论】:

  • 不...我相信你弄错了。我说的是急切加载。当我将遍历 Product 集合并与客户做一些事情时,您的方法不会消除选择 n+1 问题(基本上 - 由于延迟加载而在 for 循环中调用 db)。我正在寻找一种好方法,如果需要,可以选择性地避免延迟加载。无论如何 - 你的时间。为此点赞。 :)
  • 哦,好吧(我想我明白了)——你真的想明确定义上面示例中加载的 id 是什么?你想要一个流畅的界面对吧?
  • 我不能让它以通用方式工作(上面的例子)。而且我认为在外面公开急切的加载策略并不好。我只是好奇,其他人是如何处理这个问题的,因为我没有找到比 GetProductWithCustomersWithAddressWithFooWithBar 这样的方法更好的方法。
  • 你介意我问一下需求的“急切”部分是从哪里来的吗?在我看来,在每个查询点明确决定查询的热切深度(如上面的示例)似乎违反直觉。为什么你不想将查询深度的决定放在一个地方,让你的查询框架每次都使用它?
  • 这正是我正在寻找的(将有关查询深度的决定放在一个地方)。我只是还没有看到被广泛接受的标准。
【解决方案3】:

这是一个老问题,但也许它可以帮助某人。我花了一些时间找到一个好的方法,这是我在 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);

参考:Link

【讨论】:

    【解决方案4】:

    我可以理解您正在尝试做的事情,但您有点超出了基本的存储库模式。

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

    • GetById
    • 添加
    • 移除

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

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

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

    【讨论】:

    • 这太有争议了。 DDD 完全是关于 OOP 并将一切都放在域对象中。它怎么能提供关于如何解决这个容易预测的问题的零指导(如果不是直接来自 DDD,那么来自应用它的人)?我知道——这完全是技术性的,DDD 不是关于技术解决方案的,但不可能没有人来过这里。我并不是说要处理这个必须扩展存储库模式的含义。我只是没有任何“足够好”的东西。
    • 软件开发并不是真正从书中挑选模式并应用它们,也不是始终应用​​最佳实践。软件开发就是使用软件来解决问题。 《领域驱动设计》一书于 2003 年出版,但这并不意味着关于 DDD 的所有知识都在六年后的今天得到了清晰的认识和记录。也不意味着 DDD 和 OOP 在解决所有问题时都​​是合适的或适用的。我祝你好运找到一个满意的解决方案来解决你的问题。该解决方案可能不是您所期望的。
    • 相信我,没有人知道软件开发到底是什么。包括你。但这并不能解决我的问题。只是认为很明显有人遇到过这个问题。我并不是要盲目地应用一种或另一种模式。我正在寻找像你一样的解决方案。而且你不能否认模式是一种限制自己的好方法,因此 - 当你面对一堆如何实现某事的各种方法时,你可以变得清晰。毕竟 - 这就是软件开发很难的原因 - 选择太多了。
    【解决方案5】:

    如果您想在存储库之外指出您需要的所有包含,您可以列出每个通用方法的可选参数 (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。

    【讨论】:

    • 我也假设 EF,也许是天真 - 不确定其他 ORM 如何处理导航属性。 EF 使用字符串。
    【解决方案6】:

    BaseRepository.cs你可以创建这个方法:

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

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

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

    【讨论】:

      【解决方案7】:

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

      在 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); 
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多