EF Core原理揭秘
EF Core原理揭秘
既生IEnumerable,何生IQueryable
可以使用LINQ中的Where等方法对普通集合进行处理。比如下面的C#代码可以把int数组中大于10的数据取出来。
1 | int[] nums={3,5,933,2,69,69,11}; |
Where方法中,转到定义下,可以看到,这里调用的Where方法是Enumerable类中的扩展方法。
1 | IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,Func<TSoure,bool> predicate); |
也可以在EF Core的DbSet类型上调用Where之类的方法进行数据的筛选。
1 | IQueryable<Book> books=ctx.Books.Where(b=>b.Price>1.1); |
查看这里调用的Where方法的声明,我们会发现它是定义在Queryble类中的扩展方法
1 | IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source,EXpression<Func<TSource,bool>> predicate); |
这个Where方法是一个IQueryable<TSource>类型的扩展方法,返回值是IQueryable
1 | public interface IQuryable<out T>:IEnumerable<T>,IEnumerable,IQueryable{} |
IQuryable接口就是继承自IEnumerable接口的;IQueryable类中的Where方法除了参数和返回值的类型是IQueryable,其他用法和I/Enumerable类型的Where方法没有什么不同。
Where方法会在内存中对每条数据进行过滤,而EF Core如果也把全部数据都在内存中进行过滤,如果数据量非常大,就会有性能问题。因此EF Core中的Where实现必须有一套“把Where条件转换为SQL语句”的机制,让数据的筛选在数据库服务器上执行。
Enumerable类中定义的供普通集合用的Where等方法都是“客户端评估”,因此微软创造了IQueryable类型,并且在Queryable等类中定义了和Enumerable类中类似的Where等方法。Queryable中定义的Where方法支持把LINQ查询转化为SQL语句。我们尽量调用IQueryable版本的方法。
IQueryable的延迟执行
IQueryable不仅可以带来“服务器段评估”这个功能,而且提供了延迟执行的功能。
没有遍历的查询
1
2IQueryable<Article> articles = ctx.Articles.Include(a => a.Comments);
Console.WriteLine(articles);
我们对TestDbContext启用了日志输出代码执行的SQL语句,上面程序的日志输出结果

从日志结果输出可以看出,上面的代码竟然没有执行SQL语句,而我们明明执行了Where方法进行数据的过滤查询。
遍历查询
1 | Console.WriteLine("1.Where之前"); |

仔细观察上面的程序的日志输出结果的SQL语句、“2.遍历Where之前”和“3.遍历Where之后”的输出顺序。按照C#中的代码,Where调用的代码在“2.遍历Where之前”的前面执行,但是在执行结果中,SQL语句反而在“2.遍历Where之前”的后面执行,这是为什么?
IQueryable只是代表“可以放到数据库服务器中执行的查询”,他没有立即执行,只是“可以被执行”而已。其实可以从IQueryable类型的英文含义看出来,“IQueryable”的意思是“可查询的”,可以查询,但是没有执行查询,查询的执行被延迟了。
那么IQueryable什么时候才会执行查询呢?一个原则就是:对于IQueryable接口,调用“非立即执行”方法的时候不会执行查询,而调用“立即查询”方法的时候则会立即执行查询。判断一个方法是否是立即执行方法的简单方式是:一个方法的返回值类型如果是IQueryable类型,这个方法一般就是非立即执行查询方法,否则这个方法就是立即执行方法。
分布构建IQueryable
EF Core为什么要实现“IQueryable延迟执行”这样复杂的机制呢?因为我们可以先使用IQueryable拼接出复杂的查询条件,再去执行查询。
比如:定义一个方法根据给定的关键字srarchWords查询匹配的书;如果searchAll参数是true,则书名或者作者名中含有给定的searchWords都匹配,否则只匹配书名;如果orderByPrice参数为true,则按照价格排序,否者就自然排序;upperPrice参数代表价格上限。
1 | void QueryBooks(string searchWords,bool searchAll,bool orderByPirce,double upperPrice) |
1 | QueryBooks("爱",true,true,30); |
EF Core分页查询
如果数据库表中国的数据比较多,再把查询结果展现到前端的时候,我们通常要对数据进行分页展示。
在学习LINQ的时候,我们知道可以使用Skip(n)方法实现“跳过n条数据”,可以使用Take(n)方法实现“取最多n条数据”,这两个方法配合起来就可以分页获取数据,比如Skip(3).Take(8)就是“获取从第3条开始的最多8条数据”。在EF Core中也同样支持这两个方法。
在实现分页的时候,为了显示页码条,我们需要直到满足条件的数据的总条数是多少。可以使用IQueryable的复用,分别实现数据的分页查询和获取满足条件数据总条数这两个查询操作。
封装一个方法,用来输出标题不包含“张三”的第n页(页码从1开始)的内容,并且输出总页数,每页最多显示5条数据。
1 | OutputPage(1,5) |
我知道,ADO.NET中有DataReader和DataTable两种读取数据库查询结果的方式。如果查询结果有很多条数据,DataReader则会分批从数据库服务器读取数据。DataReader的优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力;DataTable的优点是数据库被快速地加载到了客户端内存中,因此不会较长时间地占用数据库连接,缺点是如果数据量大的话,客户端的内存占用会比较大。
IQueryable遍历读取数据的时候,用的是类似DataReader的方式还是类似DataTable的方式呢?
在遍历执行的过程中,如果我们关闭SQL Server服务器或者断开服务器的网络,程序就会出错,这说明IQueryable的方式读取查询结果的。其实IQueryable内部的遍历就是在调用DataReader进行数据读取。因此,在遍历IQueryable的过程中,它需要占用一个数据库连接。
如果需要一次性把所有数据都读取到客户端内存中,可以用IQueryable的ToArray、ToArrayAsync、ToList、ToListAsync等方法。
在遍历数据的过程中,如果我们关闭SQL Server服务器或断开服务器的网络,程序是可以正常运行的,这说明ToListAsync方法把查询结果加载到客户端内存中了。
何时需要一次性加载
1.遍历IQueryable并且进行数据处理的过程很耗时。
2.如果方法需要返回查询结果,并且在方法里销毁DbContext的话,是不能返回IQueryable的。必须一次性加载返回。
3.多个IQueryable的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。把连接字符串中的MultipleActiveResultSets=true删掉,其他数据库不支持这个。
EF Core中的异步方法
异步编程通常能狗提升系统的吞吐量,因此如果实现某个功能的方法即有同步方法又有异步方法,我们一般应该优先使用异步方法。
IQueryable的异步方法有AllAsync、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAsync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync、MaxAsync、MinAsync、SingleAsync、SingleOrDefaultAsync、SumAsync等。
IQueryable的这些异步方法的扩展方法都是立即执行方法,而GroupBy、OrderBy、Join、Where等非立即执行方法则没有对应的异步方法。因为这些非立即执行方法并没有实际执行SQL语句,并不是消耗I/O的操作,因此不需要定义这些方法的异步版本。
如何执行原生SQL语句
尽管EF Core已经非常强大,但是仍然存在无法被写成标准EF Core调用方法的SQL语句,因此在少数场景下,我们依然需要在EF Core中执行原生SQL语句。
执行SQL非查询语句
我们可以通过dbCtx.Database.ExecuteSqlInterpolated或者异步的dbCtx.Database.ExecuteSqlInterpolatedAsync方法执行原生的SQL非查询语句。
insert into…select于法是一种“先查询出数据,再把查询结果插入数据库表”的语法。
1 | Console.WriteLine("Hello, World!"); |
这样的字符串不会有SQL注入攻击漏洞。
执行实体类SQL查询语句
如果我们要执行的SQL语句是一个查询语句,并且查询的结果也能对应一个实体类,就可以调用对应实体类的DbSet的FromSqlInterpolated方法执行一个SQL查询语句,方法的参数是FormattbleString类型,因此同样可以使用字符串内插入传递参数。
编写一个程序要求用户输入一个年份,然后使用SQL预计获取出版年份大于指定年份的书,并且使用order by newid()这个SQL Server的特有用法进行随机排序。
1 | Console.WriteLine("请输入年份"); |
FromSqlInterpolated方法的返回值是IQueryable类型的,因此我们可以在实际执行IQueryable之前,对IQueryable进行进一步的处理。
1 | Console.WriteLine("请输入年份"); |
FromSqlInterpolated的使用局限性
1.SQL查询必须返回实体类型对应数据库表的所有列。
2.查询结果集中的列名必须与属性映射到的列名匹配。
3.SQL语句只能进行单表查询,不能使用Join语句进行关联查询,但是可以在查询后面使用Include方法进行关联数据的获取。
执行任意SQL查询语句
通过ctx.Databse.GetDbConnection获得一个数据库连接,然后就可以直接调用ADO.NET的相关方法执行任意的SQL语句了。
1 | var items=ctx.Databse.GetDbConnection().Query<GroupArticleByPrice>("select Price,Count(*) PCount from T_Articles group by Price"); |