延迟加载(LazyLoad)
如果public virtual Class Class { get; set; }(实体之间的关联属性又叫做“导航属性(Navigation
Property)”)把virtual 去掉,那么下面的代码就会报空引用异常
var s = ctx.Students.First();
Console.WriteLine(s.Class.Name);
联想为什么?凭什么!!!
改成virtual观察SQL的执行。执行了两个SQL,先查询T_Students,再到T_Classes中查到对应的行。
这叫“延迟加载”(LazyLoad),只有用到关联的对象的数据,才会再去执行select 查询。注意延迟加载只在关联对象属性上,普通属性没这个东西。
注意:启用延迟加载需要配置如下两个属性(默认就是true,因此不需要去配置,只要别手贱设置为false 即可)
context.Configuration.ProxyCreationEnabled = true;
context.Configuration.LazyLoadingEnabled = true;
分析延迟加载的原理:打印一下拿到的对象的GetType(),再打印一下GetType().BaseType;我们发现拿到的对象其实是Student子类的对象。(如果和我这里结果不一致的话,说明:类不是public,没有关联的virtual 属性)
因此EF其实是动态生成了实体类对象的子类,然后override了这些virtual属性,类似于这样的
实现:
public class StudentProxy:Student
{
private Class clz;
public override Class Class
{
get
{
if(this.clz==null)
{
this.clz= ....//这里是从数据库中加载Class 对象的代码
}
return this.clz;
}
}
}
再次强调:如果要使用延迟加载,类必须是public,关联属性必须是virtual。
延迟加载(LazyLoad)的优点:用到的时候才加载,没用到的时候才加载,因此避免了一次性加载所有数据,提高了加载的速度。缺点:如果不用延迟加载,就可以一次数据库查询就可以把所有数据都取出来(使用join实现),用了延迟加载就要多次执行数据库操作,提高了数据库服务器的压力。
因此:如果关联的属性几乎都要读取到,那么就不要用延迟加载;如果关联的属性只有较小的概率(比如年龄大于7
岁的学生显示班级名字,否则就不显示)则可以启用延迟加载。这个概率到底是多少是没有一个固定的值,和数据、业务、技术架构的特点都有关系,这是需要经验和直觉,也需要测试和平衡的。
注意:启用延迟加载的时候拿到的对象是动态生成类的对象,是不可序列化的,因此不能直接放到进程外Session、Redis 等中,解决方法?
不延迟加载,怎么样一次性加
用EF永远都要把导航属性设置为virtual。又想方便(必须是virtual)又想效率高!
使用Include()方法:
var s = ctx.Students.Include("Class").First();
观察生成的SQL语句,会发现只执行一个使用join的SQL就把所有用到的数据取出来了。当然拿到的对象还是Student 的子类对象,但是不会延迟加载。(不用研究“怎么让他返回Student 对象”)
Include(“Class”)的意思是直接加载Student 的Class 属性的数据。注意只有关联的对象属性才可以用Include,普通字段不可以直接写"Class"可能拼写错误,如果用C#6.0,可以使用nameof语法解决问这个问题:
var s = ctx.Students.Include(nameof(Student.Class)).First();
也可以using System.Data.Entity;然后var s = ctx.Students.Include(e=>e.Class).First(); 推荐这种做法。
如果有多个属性需要一次性加载,也可以写多个Include:
var s = ctx.Students.Include(e=>e.Class) .Include(e=>e.Teacher).First();
如果Class对象还有一个School属性,也想把School对象的属性也加载,就要:
var s = ctx.Students.Include("Class").Include("Class. School").First(); 或者更好的
var s = ctx.Students.Include(nameof(Student.Class)).Include(nameof(Student.Class)+"."+nameof(Class.School)).First();
延迟加载的一些坑
-
DbContext销毁后就不能再延迟加载了,因为数据库连接已经断开
下面的代码最后一行会报错:
Student s; using (MyDbContext ctx = new MyDbContext()) { s = ctx.Students.First(); } Console.WriteLine(s.Class.Name);
两种解决方法:
-
用Include,不延迟加载(推荐)
Student s; using (MyDbContext ctx = new MyDbContext()) { s = ctx.Students.Include(t=>t.Class).First(); } Console.WriteLine(s.Class.Name);
-
关闭前把要用到的数据取出来
Class c; using (MyDbContext ctx = new MyDbContext()) { Student s = ctx.Students.Include(t=>t.Class).First();\ c = s.Class; } Console.WriteLine(c.Name);
-
-
两个取数据一起使用
下面的程序会报错:已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭。
foreach(var s in ctx.Students) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
-
因为EF的查询是“延迟执行”的,只有遍历结果集的时候才执行select 查询,而由于延迟加载的存在到s.Class.Name也会再次执行查询。ADO.Net中默认是不能同时遍历两个DataReader。因此就报错。
解决方法有如下
-
允许多个DataReader 一起执行:在连接字符串上加上MultipleActiveResultSets=true,但只适用于SQL 2005以后的版本。其他数据库不支持。
-
执行一下ToList(),因为ToList()就遍历然后生成List:
foreach(var s in ctx.Students.ToList()) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
-
推荐做法:用Include预先加载:
foreach(var s in ctx.Students.Include(e=>e.Class)) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
-