实体框架:导航属性问题

7

我正在使用Entity Framework的Code-First功能,我有一个名为Course的类,其中包含一个导航属性Students:

public virtual Collection<Student> Students { get; set;}

它能正常工作,但是当我访问这个导航属性时,所有的数据都会从数据库中检索出来:

var allStudents = course.Students; // Here it retrieves the data
var activeStudents = allStudents.Where(n => n.Active); // Here it filter the data on memory
var listOfActiveStudents = activeStudents.ToList(); // It already has the data on memory.

正如您所想象的那样,我需要在执行.ToList()时执行查询,因为我不想将所有Students从数据库中提取出来,只需要活动的。

您知道我做错了什么吗?

4个回答

3

如果您使用正确的变量类型,那么您将看到正在发生的事情。整个集合通过导航属性进行惰性加载到内存中。

//user is an instance of the class User referenced by DbSet<User>
//when you lazy load a navigation property in that set, it loads the data
ICollection<Student> allStudents = user.Students;

//At this point, all of the data was lazy loaded
//But the Where creates an IEnumerable of the in memory set
IEnumerable<Student> activeStudents = allStudents.Where(n => n.Active);

//At this point, the IEnumerable is iterated, and a List is returned
List<Student> listOfActiveStudents = activeStudents.ToList();

谢谢,我知道那个问题...问题在于当你访问导航属性时,学生会在第一行被检索出来。我该如何只检索活跃的学生? - ascherman
我的意思是...查询在第一行被执行,然后在内存中应用过滤器。但我希望当我执行“ToList()”时才执行查询。 - ascherman
1
@ArielScherman - 你需要一个 IQueryable 来实现这个。你应该使用外键关系对学生表发出查询。类似于 db.Students.Where( s => s.Active && s.UserId == user.UserId ) 这样的语句。这将给你一个 IQueryable,能够利用你所寻找的延迟执行。 - Travis J
1
@Ariel - 我建议您采用Moho的建议,使用:db.Entry(user).Collection(u => u.Students).Query().Where(s => s.Active).ToList();。 这将只加载用户的活动学生。 .Collection返回DbCollectionEntry,而Query返回IQueryable,它们都不会将集合加载到内存中。 在此处使用Where也将返回IQueryable,而最终调用ToList是将集合加载到内存中的地方。 - Travis J

3

懒加载会将整个集合加载到内存中。如果您不想这样做,请通过删除virtual关键字并在DbEntry上使用查询对象来关闭懒加载:

public GetCourseWithActiveStudentsLoaded(int courseid)
{
   var course= context.Courses.Find(courseid); 

   context.Entry(course)
          .Collection(c => c.Students)
          .Query()
          .Where(s => s.Active)
          .Load();

   return user
}

“Active”标识是指您正在尝试实现软删除吗?如果是,可以在这里找到解决方案:Entity Framework中的软删除
您可以在此处投票以进行筛选包含:允许Include扩展方法的过滤
另一种方法是通过继承来实现。您可以从“Student”继承出一个“ActiveStudent”,并在“Course”类中拥有一个“ActiveStudents”导航属性和一个“AllStudents”导航属性。
public virtual Collection<Student> AllStudents { get; set;}
public virtual Collection<ActiveStudent> ActiveStudents { get; set;}

参考资料:

在显式加载相关实体时应用过滤器


注:本文介绍了如何在使用Entity Framework进行显式加载相关实体时,应用过滤器的方法。

user.Students 的类型是什么,它有一个 .Query() 方法? - Moho
“active”字段是一个例子。我必须应用更复杂的过滤器。 我尝试删除“virtual”,但它不运行查询……但在执行“ToList()”时没有返回任何内容(它不执行任何查询)。 - ascherman
@Moho 啊。我的错;-( 我在这里错了级别。我需要一个 DbCollectionEntry - Colin
@ArielScherman 当你关闭惰性加载时,你必须显式地加载。请查看我在答案中包含的链接,了解如何急切加载。 - Colin
为了避免急切加载所有项目,您可以使用.Query方法,如@Moho的答案中所述,我的编辑答案以及我放在答案中的链接。 - Colin
谢谢@Colin,太好了!但你知道我如何对多个集合执行此操作吗?这是问题:http://stackoverflow.com/questions/20524311/entity-framework-fetch-multiple-collections-with-filters-applied - ascherman

3

使用 dbContext.Entry( user ).Collection( u => u.Students ).Query() 可以获取学生集合导航属性的 IQueryable<Student>,然后您可以添加筛选条件并在准备好数据时枚举。


3
那么,导航属性没有解决方案吗?这怎么可能?我使用存储库模式,按照你说的做太麻烦了。 - ascherman
3
延迟加载导航属性是一种全或无操作,本质上是 dbContext.Entry(user).Collection(u => u.Students).Load()。 通过使用 .Query(),您可以在从数据库加载之前筛选结果。我认为仓库模式会使这变得不那么繁琐,因为您已经将实体的加载从开发人员中抽象出来了。 - Moho

1

其中一种解决方法是反转查询,尽管这意味着通常要避免使用导航属性。(实际实现会因模型而异。)

var allStudents =
   context
   .Students
   .Where(s => s.CourseID == course.ID) // depends on your model
   .Where(s => s.Active)
   .ToList();

我更喜欢使用这种方法而不是使用Entry方法,因为我使用通用接口与我的模型交互,并且不想暴露EF6类型。

避免暴露EF6类型的另一种方法是编写以下方法:

public IQueryable<TChild> Nav<TParent, TChild>(
   TParent pParent,
   Expression<Func<TParent, ICollection<TChild>>> pNavigationExpression
) where TParent : class
where TChild : class =>
   Entry(pParent)
   .Collection(pNavigationExpression)
   .Query();

使用类似于这样的东西:
var allStudents =
   context
   .Nav(course, c => c.Students)
   .Where(s => s.Active)
   .ToList()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接