public class Student
{
public int StudentId;
public string StudentName;
public int CourseId;
public virtual Course Courses { get; set; }
}
public class Course
{
public int CourseId;
public string CourseName;
public string Description;
public ICollection<Student> Students {get;set;}
public ICollection<Lecture> Lectures { get; set; }
}
public class Lecture
{
public int LectureId;
public string LectureName;
public int CourseId;
public virtual Course Courses { get; set; }
}
这里使用的关键字
virtual
是什么?
有人告诉我虚拟是为了延迟加载,但我不明白为什么。 因为当我们这样做时
_context.Lecture.FirstOrDefault()
结果返回第一个
Lecture
,并且不包含属性Course
。
要使用
Lecture
获得 Course
,我们必须使用:
_context.Lecture.Include("Courses").FirstOrDefault()
不使用
virtual
关键字,它已经是延迟加载了。
那我们为什么需要关键字?
通过声明它
virtual
,您允许 EF 用代理替换 value 属性以启用延迟加载。使用 Include()
告诉 EF 查询 eager 加载相关数据。
在 EF6 及更早版本中,默认启用延迟加载。对于 EF Core,它默认处于禁用状态。 (或者最早的版本不支持)
进行以下查询:
var lecture = _context.Lecture.Single(x => x.LectureId == lectureId);
加载一堂课。
如果省略
virtual
,则访问 lecture.Course
将执行以下两件事之一。如果 DbContext (_context) 尚未跟踪 Lecture.CourseId 所指向的课程实例,Lecture.Course 将返回 #null。如果 DbContext 已经在跟踪该实例,则 Lecture.Course 将返回该实例。因此,如果没有延迟加载,您可能会或可能不会获得参考,不要指望它在那里。
在同一场景中使用
virtual
和延迟加载,代理会检查 DbContext 是否已提供课程,如果是则返回。如果尚未加载,那么它将自动转到 DbContext(如果它仍在范围内)并尝试查询它。通过这种方式,如果您访问 lecture.Course
,如果数据库中有记录,您可以指望它会被返回。
将延迟加载视为安全网。如果依赖它,可能会带来巨大的性能成本,但有人可能会说,与数据不一致的运行时错误相比,性能损失是两害相权取其轻。这对于相关实体的集合来说非常明显。在上面的示例中,
ICollection<Student>
等也应标记为virtual
,以确保它们可以延迟加载。如果没有它,您将返回当时可能跟踪的所有学生,这在运行时可能会出现非常不一致的数据状态。
例如,您有 2 门课程,课程#1 和课程#2。有 4 名学生,A、B、C 和 D。所有 4 名学生都注册到课程 #1,只有 A 和 B 注册到课程 B。如果我们通过删除 virtual
来忽略延迟加载,那么行为将会改变取决于我们首先加载的课程,如果我们碰巧在一种情况下急切加载而在第二种情况下忘记了......
using (var context = new MyAppDbContext())
{
var course1 = context.Courses
.Include(x => x.Students)
.Single(x => x.CourseId == 1);
var course2 = context.Courses
.Single(x => x.CourseId == 2);
var studentCount = course2.Students.Count();
}
免责声明:对于实体中的集合,您应该确保它们始终被初始化,以便它们准备就绪。这可以在构造函数或自动属性中完成:
public ICollection<Student> Students { get; set; } = new List<Student>();
在上面的示例中,studentCount 将返回为“2”,因为在加载课程 #1 时,学生 A 和 B 都是通过
Include(x => x.Students)
加载的。这是一个非常明显的示例,依次加载两门课程,但这种情况当加载共享数据的多个记录(例如搜索结果等)时,很容易发生这种情况。它还受到 DbContext 存活时间的影响。此示例使用
using
块作为新的 DbContext 实例范围,范围仅限于 Web 请求,或者可以跟踪调用早期的相关实例。现在反转场景:
using (var context = new MyAppDbContext())
{
var course2 = context.Courses
.Include(x => x.Students)
.Single(x => x.CourseId == 2);
var course1 = context.Courses
.Single(x => x.CourseId == 1);
var studentCount = course1.Students.Count();
}
在这种情况下,只有学生 A 和 B 被急切加载。虽然课程 1 实际上引用了 4 名学生,但此处的 StudentCount 将为与课程 1 关联的两名学生返回“2”,在加载课程 1 时 DbContext 正在跟踪这两名学生。您可能期望 4 或 0,因为您知道您没有急切地加载学生。由此产生的相关数据是不可靠的,您可能会或可能不会得到什么将视具体情况而定。
延迟加载会变得昂贵的地方是加载数据集时。假设我们加载 100 名学生的列表,在与这些学生合作时,我们访问 Student.Course。热切加载将生成 1 个 SQL 语句来加载 100 名学生及其相关课程。延迟加载最终将为学生执行 1 个查询,然后为每个学生执行 100 个查询来加载课程。 (即 SELECT * FROM Courses WHERE StudentId = 1; SELECT * FROM Courses WHERE StudentId = 2; ...)如果学生有多个延迟加载属性,那么每个延迟加载还需要 100 个查询。