【问题标题】:Using repository pattern to eager load entities using ThenIclude使用存储库模式使用 ThenIclude 预加载实体
【发布时间】:2016-05-26 14:06:23
【问题描述】:

我的应用程序使用 Entity Framework 7 和存储库模式。

存储库上的 GetById 方法支持子实体的预加载:

public virtual TEntity GetById(int id, params Expression<Func<TEntity, object>>[] paths)
{
    var result = this.Set.Include(paths.First());
    foreach (var path in paths.Skip(1))
    {
        result = result.Include(path);
    }
    return result.FirstOrDefault(e => e.Id == id);
}

检索产品(id 为 2)以及与该产品相关的订单和零件的用法如下:

productRepository.GetById(2, p => p.Orders, p => p.Parts);

我想增强此方法以支持急切加载嵌套比一层更深的实体。例如,假设 Order 拥有自己的 LineItem 集合。

在 EF7 之前,我相信以下方法也可以检索与每个订单关联的 LineItems:

productRepository.GetById(2, p => p.Orders.Select(o => o.LineItems), p => p.Parts);

但是,EF7 似乎不支持此功能。取而代之的是一个新的 ThenInclude 方法,可以检索嵌套实体的其他级别:

https://github.com/aspnet/EntityFramework/wiki/Design-Meeting-Notes:-January-8,-2015

我不确定如何更新我的存储库以支持使用 ThenInclude 检索多级预加载实体。

【问题讨论】:

    标签: c# entity-framework repository-pattern eager-loading entity-framework-core


    【解决方案1】:

    这是一个有点老的问题,但由于它没有公认的答案,我想我会发布我的解决方案。

    我正在使用 EF Core,并且想要做到这一点,从我的存储库类外部访问即时加载,这样我就可以指定每次调用存储库方法时要加载的导航属性。由于我有大量的表和数据,我不想要一组标准的急切加载实体,因为我的一些查询只需要父实体,而有些则需要整个树。

    我当前的实现只支持IQueryable 方法(即FirstOrDefaultWhere,基本上是标准的 lambda 函数),但我相信您可以使用它来传递到您的特定存储库方法。

    我从 EF Core 的 EntityFrameworkQueryableExtensions.cs 的源代码开始,其中定义了 IncludeThenInclude 扩展方法。不幸的是,EF 使用内部类IncludableQueryable 来保存先前属性的树,以允许以后包含强类型。但是,对此的实现只不过是 IQueryable 为前一个实体添加了一个额外的泛型类型。

    我创建了自己的版本,我称之为IncludableJoin,它将IIncludableQueryable 作为构造函数参数并将其存储在私有字段中以供以后访问:

    public interface IIncludableJoin<out TEntity, out TProperty> : IQueryable<TEntity>
    {
    }
    
    public class IncludableJoin<TEntity, TPreviousProperty> : IIncludableJoin<TEntity, TPreviousProperty>
    {
        private readonly IIncludableQueryable<TEntity, TPreviousProperty> _query;
    
        public IncludableJoin(IIncludableQueryable<TEntity, TPreviousProperty> query)
        {
            _query = query;
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    
        public IEnumerator<TEntity> GetEnumerator()
        {
            return _query.GetEnumerator();
        }
    
        public Expression Expression => _query.Expression;
        public Type ElementType => _query.ElementType;
        public IQueryProvider Provider => _query.Provider;
    
        internal IIncludableQueryable<TEntity, TPreviousProperty> GetQuery()
        {
            return _query;
        }
    }
    

    注意内部GetQuery 方法。这在以后很重要。

    接下来,在我的通用IRepository 接口中,我定义了预加载的起点:

    public interface IRepository<TEntity> where TEntity : class
    {
        IIncludableJoin<TEntity, TProperty> Join<TProperty>(Expression<Func<TEntity, TProperty>> navigationProperty);
        ...
    }
    

    TEntity 泛型类型是我的 EF 实体的接口。我的通用存储库中Join 方法的实现是这样的:

    public abstract class SecureRepository<TInterface, TEntity> : IRepository<TInterface>
        where TEntity : class, new()
        where TInterface : class
    {
        protected DbSet<TEntity> DbSet;
        protected SecureRepository(DataContext dataContext)
        {
            DbSet = dataContext.Set<TEntity>();
        }
    
        public virtual IIncludableJoin<TInterface, TProperty> Join<TProperty>(Expression<Func<TInterface, TProperty>> navigationProperty)
        {
            return ((IQueryable<TInterface>)DbSet).Join(navigationProperty);
        }
        ...
    }
    

    现在是实际允许多个IncludeThenInclude 的部分。我有几个扩展方法可以接受和返回,IIncludableJoin 允许方法链接。在其中我调用 DbSet 上的 EF IncludeThenInclude 方法:

    public static class RepositoryExtensions
    {
        public static IIncludableJoin<TEntity, TProperty> Join<TEntity, TProperty>(
            this IQueryable<TEntity> query,
            Expression<Func<TEntity, TProperty>> propToExpand)
            where TEntity : class
        {
            return new IncludableJoin<TEntity, TProperty>(query.Include(propToExpand));
        }
    
        public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
             this IIncludableJoin<TEntity, TPreviousProperty> query,
             Expression<Func<TPreviousProperty, TProperty>> propToExpand)
            where TEntity : class
        {
            IIncludableQueryable<TEntity, TPreviousProperty> queryable = ((IncludableJoin<TEntity, TPreviousProperty>)query).GetQuery();
            return new IncludableJoin<TEntity, TProperty>(queryable.ThenInclude(propToExpand));
        }
    
        public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
            this IIncludableJoin<TEntity, IEnumerable<TPreviousProperty>> query,
            Expression<Func<TPreviousProperty, TProperty>> propToExpand)
            where TEntity : class
        {
            var queryable = ((IncludableJoin<TEntity, IEnumerable<TPreviousProperty>>)query).GetQuery();
            var include = queryable.ThenInclude(propToExpand);
            return new IncludableJoin<TEntity, TProperty>(include);
        }
    }
    

    在这些方法中,我使用上述GetQuery 方法获取内部IIncludableQueryable 属性,调用相关的IncludeThenInclude 方法,然后返回一个新的IncludableJoin 对象以支持方法链接。

    就是这样。 this的用法是这样的:

    IAccount account = _accountRepository.Join(x=>x.Subscription).Join(x=>x.Addresses).ThenJoin(x=>x.Address).FirstOrDefault(x => x.UserId == userId);
    

    上面将加载基础Account实体,它是一对一的子Subscription,它是一对多的子列表Addresses,它是子Address。沿途的每个 lambda 函数都是强类型的,并受智能感知支持以显示每个实体上可用的属性。

    【讨论】:

    • 与先选择相比,先加入再选择(通过 FirstOrDefault() 方法)会有性能缺陷吗?
    • @phhbr 我不这么认为?我认为这并不重要,因为当您坐在IQueryable 中时,查询不会执行,直到您调用.ToList 或类似名称。所以操作顺序可能没关系,但我没有测试过。
    • @Steve 你能把这个放在某个地方快速浏览一下吗?
    • @Seb 我不确定你的意思?它用于不公开的内部项目中,但所有代码都在答案中。
    • @Steve 是的,我刚刚意识到,我的错。我会在我自己的项目中尝试一下,谢谢!
    【解决方案2】:

    你可以把它改成这样:

    public virtual TEntity GetById<TEntity>(int id, Func<IQueryable<TEntity>, IQueryable<TEntity>> func) 
    {
        DbSet<TEntity> result = this.Set<TEntity>();
    
        IQueryable<TEntity> resultWithEagerLoading = func(result);
    
        return resultWithEagerLoading.FirstOrDefault(e => e.Id == id);
    }
    


    你可以这样使用它:

    productRepository.GetById(2, x => x.Include(p => p.Orders)
                                       .ThenInclude(o => o.LineItems)
                                       .Include(p => p.Parts))
    

    【讨论】:

    • 感谢您抽出宝贵时间回复。我对您提出的解决方案的问题是它在存储库之外公开了 IQueryable/Entity Framework 调用。我不想这样做。
    • 我理解您的担忧。使这种“持久性无知”的一种方法是在您的数据层中创建一些对象来封装您的获取详细信息(EF 包含),然后让它们实现一些接口。然后,您可以将该接口传递给您的存储库方法,并让存储库解析已实现的对象,然后调用该对象中的包含。设置它需要一些工作,但这就是我在我的项目中实现预加载的方式。
    猜你喜欢
    • 2011-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多