在EF Code First中过滤导航属性
我在EF中使用Code First。 假设我有两个实体:
public class Farm { .... public virtual ICollection Fruits {get; set;} } public class Fruit { ... }
我的DbContext是这样的:
public class MyDbContext : DbSet { .... private DbSet FarmSet{get; set;} public IQueryable Farms { get { return (from farm in FarmSet where farm.owner == myowner select farm); } } }
我这样做,以便每个用户只能看到他的农场,我不必调用每个查询的位置到数据库。
现在,我想过滤掉一个农场的所有水果,我尝试了这个(在Farm类中):
from fruit in Fruits where fruit .... select fruit
但是生成的查询不包含where子句,这非常重要,因为我有数十万行,加载它们并将它们作为对象时过滤它们效率不高。
我读到懒惰加载的属性在第一次被访问时被填充,但是他们读取了所有数据,没有filter可以应用,除非你做这样的事情:
from fruits in db.Fruits where fruit .... select fruit
但我不能这样做,因为Farm不知道DbContext(我不认为它应该(?))而且对我来说它只是失去了使用导航属性的全部目的,如果我必须处理所有数据而不只是属于我的农场的那个。
所以,
- 我做错什么/做错了假设?
- 有没有什么办法可以将filter应用到生成真实查询的导航属性? (我正在处理大量数据)
谢谢你的阅读!
不幸的是,我认为你可能采取的任何方法都必须涉及摆弄上下文,而不仅仅是实体。 正如您所见,您无法直接过滤导航属性,因为它是ICollection
而不是IQueryable
,因此在您有机会应用任何filter之前,它会立即加载。
您可以做的一件事是在Farm
实体中创建一个未映射的属性来保存过滤后的水果列表:
public class Farm { .... public virtual ICollection Fruits { get; set; } [NotMapped] public IList 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
,而不适用于您在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
实体本身添加额外数据。
刚想到我会为此添加另一个解决方案花了一些时间尝试将DDD原则附加到代码第一个模型。 经过一段时间的搜索,我找到了一个类似下面的解决方案,对我有用。
public class FruitFarmContext : DbContext { public DbSet Farms { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity ().HasMany(Farm.FruitsExpression).WithMany(); } } public class Farm { public int Id { get; set; } protected virtual ICollection Fruits { get; set; } public static Expression>> FruitsExpression = x => x.Fruits; public IEnumerable 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; } }
这个想法是农场水果收集不是直接可以访问,而是通过预过滤它的属性暴露。 这里的妥协是在设置映射时能够解决水果集合所需的静态表达式。 我已经开始在许多项目中使用这种方法,我希望控制对象子集合的访问。
延迟加载不支持过滤; 使用过滤显式加载 :
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实体加载到上下文中,如使用预测和存储库伪造过滤的Eager Load中所述 。)