我在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);
}
}
}
我这样做是为了让每个用户只能看到他的农场,而且我不必调用每个查询到数据库的位置。
现在,我想过滤掉一个农场的所有水果,我尝试了这个(在Farm类中):
from fruit in Fruits where fruit .... select fruit
但生成的查询不包含where子句,这非常重要,因为我有几十万行,加载它们并将它们作为对象时过滤它们效率不高。
我读到延迟加载的属性在第一次被访问时被填充但是他们读取了所有数据,没有过滤器可以应用,除非你做这样的事情:
from fruits in db.Fruits where fruit .... select fruit
但我不能这样做,因为Farm不知道DbContext(我不认为它应该(?))而且对我来说它只是失去了使用导航属性的整个目的,如果我必须使用所有数据,而不仅仅是属于我的农场的数据。
所以,
感谢您阅读!
答案 0 :(得分:8)
不幸的是,我认为您可能采取的任何方法都必须涉及摆弄上下文,而不仅仅是实体。如您所见,您无法直接过滤导航属性,因为它是ICollection<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
,而不适用于您在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
实体本身添加额外数据。
答案 1 :(得分:3)
刚想我会为此添加另一个解决方案花了一些时间尝试将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; }
}
这个想法是农场水果收集不是直接可以访问,而是通过预过滤它的属性暴露。 这里的折衷是在设置映射时能够解决水果集合所需的静态表达式。 我已经开始在许多项目中使用这种方法,我希望控制对象子集合的访问。
答案 2 :(得分:2)
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中所述。)