11

我在 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 到数据库。

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

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

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

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

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

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

所以,

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

感谢您的阅读!

4

3 回答 3

9

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

您可以做的一件事是在您的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只填充过滤后的集合,因此您可以在实体中以您想要的方式使用它。但是,我自己从未尝试过这种方法,因此可能存在我没有想到的陷阱。一个主要的缺点是它只适用于Farm您使用该方法加载的 s,而不适用于您对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实体本身添加额外的数据。

于 2013-05-22T15:40:31.070 回答
4

只是想我会为此添加另一个解决方案,我花了一些时间尝试将 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; }

}

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

于 2014-03-06T20:48:14.717 回答
3

延迟加载不支持过滤;改用过滤后的显式加载

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 实体加载到上下文中,如使用投影和存储库来伪造过滤的急切负载中所述。)

于 2014-07-21T20:54:52.573 回答