【问题标题】:Filtering navigation properties in EF Code First在 EF Code First 中过滤导航属性
【发布时间】:2013-05-22 14:20:14
【问题描述】:

我在 EF 中使用 Code First。假设我有两个实体:

public class Farm
{
    ....
    public virtual ICollection<Fruit> Fruits {get; set;}
}

public class Fruit
{
    ...

}

我的 DbContext 是这样的:

public class MyDbContext : DbSet
{
    ....
    private DbSet<Farm> FarmSet{get; set;} 

    public IQueryable<Farm> Farms
    {
        get
        {
            return (from farm in FarmSet where farm.owner == myowner select farm);
        }
    }
}

我这样做是为了让每个用户只能看到他的农场,而我不必在每个查询中调用 Where 到 db。

现在,我想过滤一个农场的所有水果,我尝试了这个(在 Farm 类中):

from fruit in Fruits where fruit .... select fruit

但是生成的查询不包括 where 子句,这非常重要,因为我有成千上万的行,并且在它们是对象时将它们全部加载并过滤它们效率不高。

我了解到延迟加载的属性在第一次访问时会被填充,但它们会读取所有数据,除非您执行以下操作,否则无法应用过滤器:

from fruits in db.Fruits where fruit .... select fruit

但我不能这样做,因为 Farm 不了解 DbContext(我认为它不应该(?))而且对我来说,如果我必须使用导航属性,它只会失去使用导航属性的全部目的所有数据,而不仅仅是属于我农场的数据。

所以,

  1. 我是否做错了什么/做出了错误的假设?
  2. 有什么方法可以将过滤器应用于生成到实际查询的导航属性? (我正在处理大量数据)

感谢您的阅读!

【问题讨论】:

    标签: c# .net entity-framework entity-framework-5 code-first


    【解决方案1】:

    不幸的是,我认为您可能采取的任何方法都必须涉及摆弄上下文,而不仅仅是实体。如您所见,您不能直接过滤导航属性,因为它是 ICollection&lt;T&gt; 而不是 IQueryable&lt;T&gt;,因此在您有机会应用任何过滤器之前它会被一次性加载。

    您可以做的一件事是在您的 Farm 实体中创建一个未映射的属性来保存过滤后的水果列表:

    public class Farm
    {
      ....
      public virtual ICollection<Fruit> Fruits { get; set; }
    
      [NotMapped]
      public IList<Fruit> FilteredFruits { get; set; }
    }
    

    然后,在您的上下文/存储库中,添加一个方法来加载 Farm 实体并使用您想要的数据填充 FilteredFruits

    public class MyDbContext : DbContext
    {
      ....    
    
      public Farm LoadFarmById(int id)
      {
        Farm farm = this.Farms.Where(f => f.Id == id).Single(); // or whatever
    
        farm.FilteredFruits = this.Entry(farm)
                                  .Collection(f => f.Fruits)
                                  .Query()
                                  .Where(....)
                                  .ToList();
    
        return farm;
      }
    }
    
    ...
    
    var myFarm = myContext.LoadFarmById(1234);
    

    这应该仅使用过滤后的集合填充myFarm.FilteredFruits,因此您可以在实体中以您想要的方式使用它。但是,我自己从未尝试过这种方法,因此可能存在我没有想到的陷阱。一个主要缺点是它仅适用于您使用该方法加载的 Farms,而不适用于您在 MyDbContext.Farms 数据集上执行的任何常规 LINQ 查询。

    话虽如此,我认为您尝试这样做的事实可能表明您将过多的业务逻辑放入实体类中,而实际上它可能更适合放在不同的层中。很多时候,最好将实体基本上视为数据库记录内容的容器,并将所有过滤/处理留给存储库或您的业务/显示逻辑所在的任何地方。我不确定您正在开发哪种应用程序,因此我无法提供任何具体建议,但值得考虑。

    如果您决定将事物移出Farm 实体,一种非常常见的方法是使用投影:

    var results = (from farm in myContext.Farms
                   where ....
                   select new {
                     Farm = farm,
                     FilteredFruits = myContext.Fruits.Where(f => f.FarmId == farm.Id && ...).ToList()
                   }).ToList();
    

    ...然后将生成的匿名对象用于您想做的任何事情,而不是尝试向Farm 实体本身添加额外数据。

    【讨论】:

    • 感谢 Jeremy,我决定听从您的建议,并将过滤/处理职责留在我的上下文类中。这是有道理的,因为我只需要对我的一个实体进行过滤,但是如果我需要对多个实体进行过滤,那会很麻烦,你不觉得吗?上下文将填充用于查询和填充实体的方法。我不认为这违反了单一责任原则,但听起来很奇怪,不是吗?
    【解决方案2】:

    我花了一些时间尝试将 DDD 原则附加到代码优先模型中,我只是想为此添加另一个解决方案。在搜索了一段时间后,我找到了一个适合我的解决方案,如下所示。

    public class FruitFarmContext : DbContext
    {
        public DbSet<Farm> Farms { get; set; }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Farm>().HasMany(Farm.FruitsExpression).WithMany();
        }
    }
    
    public class Farm
    {
        public int Id { get; set; }
        protected virtual ICollection<Fruit> Fruits  { get; set; }
        public static Expression<Func<Farm, ICollection<Fruit>>> FruitsExpression = x => x.Fruits;
    
        public IEnumerable<Fruit> FilteredFruits
        {
            get
            {
                //Apply any filter you want here on the fruits collection
                return Fruits.Where(x => true);
            }
        }
    }
    
    public class Fruit
    {
        public int Id { get; set; }
    
    }
    

    这个想法是农场的水果收集不能直接访问,而是通过预过滤的属性暴露出来。 这里的折衷方案是在设置映射时能够处理水果集合所需的静态表达式。 我已经开始在一些我想控制对对象子集合的访问的项目中使用这种方法。

    【讨论】:

    • 这个解决方案有什么影响吗?
    【解决方案3】:

    Lazy loading 不支持过滤;改用filtered explicit loading

    Farm farm = dbContext.Farms.Where(farm => farm.Owner == someOwner).Single();
    
    dbContext.Entry(farm).Collection(farm => farm.Fruits).Query()
        .Where(fruit => fruit.IsRipe).Load();
    

    显式加载方法需要两次往返数据库,一次用于主数据库,一次用于详细数据库。如果坚持单个查询很重要,请改用投影:

    Farm farm = (
        from farm in dbContext.Farms
        where farm.Owner == someOwner
        select new {
            Farm = farm,
            Fruit = dbContext.Fruit.Where(fruit => fruit.IsRipe) // Causes Farm.Fruit to be eager loaded
        }).Single().Farm;
    

    EF 总是将导航属性绑定到它们加载的实体。这意味着farm.Fruit 将包含与匿名类型中的Fruit 属性相同的过滤集合。 (只要确保您没有将任何应该被过滤掉的 Fruit 实体加载到上下文中,如Use Projections and a Repository to Fake a Filtered Eager Load 中所述。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-03-16
      • 2014-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-09
      • 2016-10-10
      相关资源
      最近更新 更多