zoukankan      html  css  js  c++  java
  • EF使用延迟加载的本质原因

    EF(Entity Framework)是微软的一个ORM框架

    使用过EF的同学都知道它有一个延迟加载的功能

    那么这个延迟加载的功能到底是什么?

    为什么需要延迟加载?

    使用延迟加载的优点和缺点又各是什么?


    可以通过一个简单的小例子来阐述EF的这些问题

    首先使用到了两个很简单的数据表

    关系图如下:

    T_Product的Uid关联到T_Users的Id,形成一个外键关系

    是不是真的很简单= =

    然后在测试项目中根据数据库添加EF数据模型

    准备工作已经做好了,现在进入主题


    首先需要搞明白的是:什么是延迟加载?

    延迟加载又可以理解成 按需加载

    根据字面的意思很容易理解:按照所需的数据 加载数据

    上例子:

    很简单的代码,相信大家都看得懂

    接下来设置断点对代码的执行步骤进行分解

    首先我在如图所示的位置设置了一个断点

    打开sql server profiler监听接收到的sql语句

    运行程序并命中断点

    此时程序停留在到数据库取数据(姑且这么认为)的这行代码上

    由于代码还未执行所以数据库不可能接收到任何的sql语句

    按下F11逐语句执行

    可以看到,程序跳转,数据库接收到sql语句

    正是var user = dbEntities.T_Users.Where(u => u.Id == 2).FirstOrDefault();这行代码发送了sql语句到数据库取的数据

    那么所谓的延迟加载体现在哪里呢?

    其实这行代码是变相的即使加载,看起来像是调用where方法之后马上到数据库取出了数据,但是别忽略了后面的FirstOrDefault()

    在EF中,调用where方法只是先保存了sql语句,但是并没有提交到数据库

    但是当调用了FirstOrDefault()方法的时候就表示程序要用数据了,这时sql语句才会被提交到数据库中

    这就是延迟加载,到要使用数据的时候才取出数据

    口说无凭,下面继续用代码来证明这个说法

    将代码拆分如下,还是在调用where方法的地方设置断点并运行

    F11下一步

    这时可以看到,代码已经执行了where方法,并停在了.FirstOrDefault方法,但是此时数据库并没有接收到任何sql语句

    为了方便看清楚,在Console.ReadKey()也设置了一个断点,F5代码停留在Console.ReadKey()

    现在已经很明显了,只有程序中要使用数据的时候(调用了FirstOrDefault),sql语句才会被发送发数据库

    这就是EF的延迟加载

    但是List集合本身就有一个where方法

    难道这个where方法要是延迟加载的吗,但是这并不是到数据库取数据的呀

    在上图的智能提示中可以隐约的看到,该where方法的第一个参数是一个IEnumerable的linq表达式

    我们可以在var user = dbEntities.T_Users.Where(u => u.Id == 2)这行代码上对where方法按F12转到定义看一看

    注意看一下这个where方法的返回值类型,是IQueryable而不是IEnumerable

    这说明了两个where方法虽然名字一样,甚至连参数都差不多但是一个返回的是IEnumerable另外一个则是IQueryable

    这就从本质上区别开了这两个where方法

    同时注意一下IQueryable的where方法第一个参数的this关键字,这说明了什么?

    说明了where并不是在IQueryable接口内部的,而是在外部类通过扩展方法的方式加上去的(这个类就是Queryable,该类中为IQueryable接口提供了n多类似where的扩展方法)

    所以可以说,EF能实现延迟加载就是因为有Queryable类的存在

    但是从这行代码中我们又可以看到

    var user = dbEntities.T_Users.Where(u => u.Id == 2);

    where方法是从dbEntities这个EF上下文对象的T_Users属性中点出来的

    where方法不是在Queryable类中的吗(或者说是IQueryable接口的)

    怎么就可以从EF的上下文属性中调用呢?

    那么现在我们就找到EF上下文,看看T_Users属性到底是怎么回事

    可以很清楚的看到T_Users是一个DbSet<T_Users>类型的泛型属性

    F12转到定义看一下这个DbSet是什么东西

    是不是能够在DbSet的基类(接口中)找到IQueryable!

    这就解释了为什么能够在EF上下文的T_Users属性中点出where方法并使用

    因为T_Users的类型继承了IQueryable接口,自然能够使用IQueryable接口中的方法啦!



    上面的长篇大论简要的解释了一下EF的延迟加载机制

    说了那么多,那到底为什么需要延迟加载呢?

    直接取直接用不就好了?

    确实从这个简单的例子来看延迟加载貌似很多余

    但是通常我们操作数据库不可能只是这么简单的

    就拿很常用的分页来说

    一般是先对数据进行排序

    然后按照要求跳过几行数据

    在取几行数据

    这就不是一个简单的where方法可以实现的了

    至少需要先调用order进行排序,然后skip跳过几行数据,最后take取几行数据

    如果where/order/skip/take等等方法每次使用的时候就马上提交sql语句到数据库

    那做一个分页查询至少要发送4次请求

    也就是说要和数据库交互4次

    如果使用延迟加载的话

    上面的where/order/skip/take方法调用的时候可以看做只是在拼凑条件

    当条件满足的时候(一般就是要用数据的时候,比如说FirstOrDefault方法)

    在将整个拼凑好的sql语句一起提交到数据库

    这样一来和数据库的交互次数由4降到了1


    这就是使用延迟加载的本质原因之一:拼凑条件一起提交,降低数据库交互次数,提高数据库的吞吐量

    这也是延迟加载的优点



    在本例中,T_Products中有一个外键Uid关联到T_Users

    上代码:

    var product = dbEntities.T_Products.Where(p => p.Uid == 2).FirstOrDefault();
    Console.WriteLine(product.T_Users.UserName);

    表内容如下

    可以看到Uid为2有两行

    取出Uid为2的T_Products集合然后获得第一行的实体

    并将其关联的外键属性T_User的UserName输出

    继续在每行代码设置断点并运行


    程序停留在上图的位置,F5执行,命中下一个断点


    可以看到,因为调用了FirstOrDefault方法

    所以想数据库发送了sql语句

    但是注意观察左边的sql语句

    并没有将外键列一起查询出来

    继续F5运行

    还是观察左边的sql语句

    因为执行了Console.WriteLine(product.T_Users.UserName);这行代码

    需要用到外键列

    此时EF才向数据库提交查询外键列的sql语句


    这就是延迟加载的本质原因之二:针对外键属性,EF只有在这个外键属性用到的时候才会去查询



    那么通过上面的介绍,EF延迟加载的优点已经很明显了

    那么缺点是什么呢?

    比如说现在的需求是

    取出T_Products

    var products = dbEntities.T_Products.Where(p => true);
                foreach (var p in products)
                {
                    Console.WriteLine(p.T_Users.UserName);
                }
    
                Console.ReadKey();

    表中的所有实体

    遍历输出每一个实体的外键T_Users并输出其UserName
    我们知道

    在执行完第一行代码的时候并没有和数据库进行交互

    但是此时问题来了

    foreach每次一次循环都要用一次products集合中实体的外键

    这就造成了每循环一次就到数据库中取一次数据

    这就是EF延迟加载的缺点:每次用到外键列都会去查询一次数据库

    其实这里EF内部已经做了一些列的小优化,对于相同的实体只取一次,比如说T_Products表中Uid为2的有两行,Uid为3的有两行,本来需要查询4次,经过优化之后只需要查询两次

    但是当数据量十分巨大的时候,这些小优化起到的作用微乎其微

    那么要怎么避免这个缺陷呢?

    这时可以通过连接查询先将外键列一起查询出来在循环使用

    代码如下:

    var products = dbEntities.T_Products.Include("T_Users");//通过Include告诉EF连接查询哪个外键列,特别舒服的事情是,Include可以一直点下去~!
                foreach (var p in products)
                {
                    Console.WriteLine(p.T_Users.UserName);
                }
    
                Console.ReadKey();

    点击运行可以看到下图的效果

    注意左边的sql语句

    生成了inner join的sql语句

    只进行了一次数据库交互


  • 相关阅读:
    CentOS新系统必做的几件事
    CentOS基础指令备忘
    有向图的强连通分量
    模仿c的字符转整数函数 atoi
    N个骰子的点数和的概率分布
    c语言算法题目求职用
    copy指定目录下包括子目录中所有的文件
    js的页面交互
    js的数据类型、函数、流程控制及变量的四种声明方式
    z-index
  • 原文地址:https://www.cnblogs.com/jchubby/p/4429724.html
Copyright © 2011-2022 走看看