EF数据延迟加载
在DAL层,我们一般都是返回IQueryable类型的数据,然后根据情况在BLL或者UI层来ToList() 【如果是在UI层ToList()其实就是foreach(var item in ...)】
当你使用Where(),Find(),First().....等等来查询数据的时候,EF仅仅是生成了SQL语句,只有当你真正要使用数据的时候,即在ToList() 或者foreach(var a in list)的时候EF才会将这条SQL语句发送给ado.net,然后ado.net发送给db,进行查询
EF的延迟加载,就是使用Lambda表达式或者Linq 从 EF实体对象中查询数据时,EF并不是直接将数据查询出来,而是在用到具体数据的时候才会加载到内存。说白了就是按需加载。
EF的延迟加载主要体现在导航属性上
例如,我有两个实体类模型 User和UserOrder 他们的关系是以对多 User对应的是一 UserOrder对应的是多
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class UserOrder
{
public int Id { get; set; }
public decimal Money { get; set; } //订单金额
public int UserId { get; set; } //订单签订者
public DateTime CreateTime { get; set; } //订单签订时间
public virtual User User { get; set; } //导航属性(导航属性需要用virtual标识)
}
public class HomeController : Controller
{
public ActionResult Index()
{
string log = null;
using (MyDbContext db = new MyDbContext())
{
db.Database.Log = (r) => log = r+log;
var uorder = db.UserOrder.FirstOrDefault(); //它查询了一次数据库
var userName = uorder.User.Name; //它也查询了一次数据库(使用到了延迟加载功能,此时用到了导航属性User,所以这次它去数据库查询了数据)
var info = db.UserOrder.Include("User").FirstOrDefault(); //这段代码实现了上面两段代码的功能,它避免了延迟加载,它使用Include,一次性将导航属性的数据以连表查询的方式查询出来,所以它只查询了一次数据库就得到了我们想要的数据
//总结:
//优点:用到的时候才加载,没用到的时候才加载,因此避免了一次性加载所有数据,提高了加载的速度
//缺点:延迟加载的功能虽然比较方便,但是它会多次查询数据库,增加了数据库的压力,
//因此:如果关联的导航属性值几乎都要读取到,那么就不要用延迟加载;如果关联的属性只有较小的概率(比如年龄大于7岁的学生显示班级名字,否则就不显示)则可以启用延迟加载。这个概率到底是多少是没有一个固定的值,和数据、业务、技术架构的特点都有关系,这是需要经验和直觉,也需要测试和平衡的。
}
return View();
}
}
总结
优点:用到的时候才加载,没用到的时候才加载,因此避免了一次性加载所有数据,提高了加载的速度
缺点:延迟加载的功能虽然比较方便,但是它会多次查询数据库,增加了数据库的压力,
因此:如果关联的导航属性值几乎都要读取到,那么就不要用延迟加载;如果关联的属性只有较小的概率(比如年龄大于7岁的学生显示班级名字,否则就不显示)则可以启用延迟加载。这个概率到底是多少是没有一个固定的值,和数据、业务、技术架构的特点都有关系,这是需要经验和直觉,也需要测试和平衡的。
延迟加载的原理
public class HomeController : Controller
{
public ActionResult Index()
{
string log = null;
using (MyDbContext db = new MyDbContext())
{
db.Database.Log = (r) => log = r+log;
var user = db.User.FirstOrDefault(); //User表中没有导航属性,所以这个查询并没有使用延迟加载,查询出来的类对象之间就是User
var userType = user.GetType();//获取user的类型,值为User类型
var userOrder = db.UserOrder.FirstOrDefault(); //UserOrder表中有导航属性,所有这里查询出来值为UserOrder的一个子类
var userOrderType = userOrder.GetType(); //得到的值为:System.Data.Entity.DynamicProxies.UserOrder_9866053E3224700A29085272301D76997A4B20B86455582BAAC81183A964CBE0
var userOrderBaseType = userOrderType.BaseType; //获取System.Data.Entity.DynamicProxies.UserOrder_9866053E3224700A29085272301D76997A4B20B86455582BAAC81183A964CBE0类的父类类型,值为UserOrder类型
}
return View();
}
}
通过以上代码可以看出,如果一个类中有导航属性存在,那么查询出来的数据类型是这个类对象的子类,这个子类的名称EF动态生成的,例如像UserOrder实体模型类中有User这个导航属性,那么通过db.UserOrder.FirstOrdefault()查询出来的数据对象类是System.Data.Entity.DynamicProxies.UserOrder_9866053E3224700A29085272301D76997A4B20B86455582BAAC81183A964CBE0,这个类继承了UserOrder类。
让我们来看看这个自动生成的类的结构类似下面这样的(类名我做了缩写)
public class System.Data.Entity.DynamicProxies.UserOrder_xxxxxxxxxxxxxxxx4CBE0:UserOrder
{
private User _user { get; set; }
public override User User //它重写了父类的导航属性User(这下明白为什么导航属性要设置为virtual了吧)
{
get
{
if (this._user == null)
{
this._user =//这里是从数据库中加载User对象的代码
}
return this._user;
}
}
}
延迟加载的一些坑
由于DbContext销毁后就不能再延迟加载了,因为数据库连接已经断开。下面的代码最后一行会报错
public class HomeController : Controller
{
public ActionResult Index()
{
UserOrder uOrderA;
UserOrder uOrderB;
using (MyDbContext db = new MyDbContext())
{
uOrderA = db.UserOrder.FirstOrDefault();
uOrderB = db.UserOrder.Include(nameof(User)).FirstOrDefault();
}
//这里会报错:The ObjectContext instance has been disposed and can no longer be used for operations that require a connection. 表示ObjectContext实例已被释放,不能再用于需要连接的操作。即与数据库的连接已经关闭,这里就无法再次进行数据库查询了,除非将这段代码写在using里面.或者用Include,不延迟加载,把数据一次性都取出来(推荐)
var userNameA = uOrderA.User.Name;
//这种写法则不会报错
var userNameB = uOrderB.User.Name;//因为uOrderB已经通过Include将数据一次性全查询出来了。所有这里即便与数据库断开连接也不会报错
return View();
}
}
禁用延迟加载
public class MyDbContext : DbContext
{
public DbSet<User> User { get; set; }
public DbSet<UserOrder> UserOrder { get; set; }
public MyDbContext() : base("name=connstr")
{
Database.SetInitializer<MyDbContext>(null);
//启用延迟加载需要配置如下两个属性(默认就是true,因此不需要去配置,如果想要禁用延迟加载将他们两设为false即可)
this.Configuration.ProxyCreationEnabled = true;
this.Configuration.LazyLoadingEnabled = true;
}
}
IEnumerable还是IQueryable的区别
IQueryable继承自IEnumerable,所以对于数据遍历来说,它们没有区别。
但是IQueryable的优势是它有表达式树,所有对于IQueryable的过滤,排序等操作,都会先缓存到表达式树中,只有当真正遍历发生的时候,才会将表达式树由IQueryProvider执行获取数据操作。
而使用IEnumerable,所有对于IEnumerable的过滤,排序等操作,都是在内存中发生的。也就是说数据已经从数据库中获取到了内存中,只是在内存中进行过滤和排序操作。
在实际检验过程中我们发现 IEnumerable和IQueryable的效率存在差别:
在数据较多的情况下或者操作比较复杂的情况下,IEnumerable的效率会比IQueryable低很多。
Skip():跳过的数据条数
Take():获取的数据条数
<pre name="code" class="csharp">using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcTest.Controllers
{
public class TestController : Controller
{
salesEntities db = new salesEntities();
//IQueryable接口与IEnumberable接口的区别: IEnumerable<T> 泛型类在调用自己的SKip 和 Take 等扩展方法之前数据就已经加载在本地内存里了,而IQueryable<T> 是将Skip ,take 这些方法表达式翻译成T-SQL语句之后再向SQL服务器发送命令,它并不是把所有数据都加载到内存里来才进行条件过滤。
//让我们先来了解一下DbQuery,IQueryable,IQueryable之间的关系,看下面两天代码
//public abstract class DbQuery : IOrderedQueryable, IQueryable, IEnumerable, IListSource
//public interface IQueryable : IEnumerable
public ActionResult Index()
{
//它执行的时候SQL语句是 :select * from T_User (这想想为什么他的SQL语句是select * from T_User ,而不是像下面例子那样?其实道理很简单,因为 我们在(from a in db.T_User select a)的后面就toList()了,我们只是如果ToList()就是执行数据库,所以此时它已经去执行数据库了。将这条语句查询到的所有数据都加载到了内存当中,然后再执行后面的.ToList().Skip(5).Take(10); 也就是说,它是在内存中的数据进行刷选了。所以它执行的sql语句就是select * from T_User了
//它会把所有的数据都查询出来,放到内存中,然后再从这些所有的数据中再跳过5条,取10条数据,即取:6-15条的数据 ;
IEnumerable<T_User> query1 = (from a in db.T_User
select a).ToList().Skip(5).Take(10);
//【注意:ToList()方法的返回类型是:IEnumerable<T>】
//---------------------------------------------------------------------------------------------
//我们知道ToList()的返回类型是:IEnumerable<T>所以在接收数据的时候我们需要用IEnumerable<T_User>去接收数据。那我们前面说到IEnumerable<T> 泛型类在调用自己的SKip 和 Take 等扩展方法之前数据就已经加载在本地内存里了 ,为什么这里却是先准备了select top 10* from....语句?之后再执行?
//因为我们是最后ToList()的,所以它是先准备select top 10* from (select * ,ROW_NUMBER() over(order by id asc) as rowNum from T_User ) as t1 where t1.rowNum>5 order by t1.Id asc 这么一条sql语句,然后再ToList()的时候去查询数据库
//它只会查询在数据库中第6-15条之间的数据 即取:6-15条的数据
IEnumerable<T_User> query2 = (from a in db.T_User
select a).OrderBy(r => r.Id).Skip(5).Take(10).ToList();
//注意:ToList()的返回类型是:IEnumerable<T>
//注意:OrderBy()与Skip()Take()方法的返回类型是IQueryable<T>
//它只会查询在数据库中第6-15条之间的数据 即取:6-15条的数据 它执行的时候SQL语句是
//select top 10* from (select * ,ROW_NUMBER() over(order by id asc) as rowNum from T_User ) as t1 where t1.rowNum>5 order by t1.Id asc
IQueryable<T_User> query3 = (from a in db.T_User
select a).OrderBy(r => r.Id).Skip(5).Take(10);
//其实这里的query3真实的类型是DbQuery<T_User> 之所以我们在这里用IQueryable<T_User>来接收它,是因为OrderBy()与Skip()Take()方法的返回类型是IQueryable<T>而DbQuery这个类又继承了IQueryable, IEnumerable这两个接口,根据李氏定理,子类可以隐式的转换成父类 所以,这里可以用IQueryable<T_User>或者用IEnumerable<T_User>类接收
//EF之所以可以实现延迟加载,就是因为有了DbQuery这个类。(所谓延迟加载就是:只有当使用到数据的时候才去查询数据库)
//DbQuery<T_User> query = ((from a in db.T_User
// select a).OrderBy(r => r.Id).Skip(5).Take(10)) as DbQuery<T_User>;
ViewBag.Data = query3;
return View();
} //就算执行到“}”这一步的时候 query3都是没有数据的。它仅仅是准备了一条sql语句。并没有执行这条语句。也就是说他没有查询数据库(它真正的去查询数据库的时候是在页面上使用的时候 比如在视图Index页面上执行@foreach(var a in viewBag.Data)的时候才会去查询数据库
}
}
Index视图
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<script src="~/Scripts/jquery-1.8.2.js"></script>
</head>
<body>
<div>
@foreach (var i in ViewBag.Data)
{
@i.UserName <br/>
}
</div>
</body>
</html>