EF Core 既生,何生

目录

一,既生 IEnumerable,何生 IQueryable

二,IQueryable 的延迟执行

三,lQueryable 的复用

四,EF Core 分页查询

五,IQueryable 的底层运行

1.场景一:方法需要返回查询结果

2.场景二:多个 IQueryable 的遍历嵌套


一,既生 IEnumerable,何生 IQueryable

        我们已经知道,可以使用 LINQ 中的 Where 等方法对普通集合选行处理。比如下面的C#代码可以把 int 数组中大于10的数据取出来:

int[] nums = { 3,5,933,2,69,69,11 };
IEnumerable<int> items = nums.where(n => n > 10):

        在 Where 方法上右击,单击[转到定义] 按钮,可以看到,这里调用的 Where 方法Enumerable 类中的扩展方法,方法的声明如下;

IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,Func<TSource,bool> predicate);

        我们也可以在 EF Core 的 DbSet 类型上调用 Where 之类的方法进行数据的筛选,比如下面的代码可以把价格高于 1.1 元的书筛选出来;

IQueryable<Book> books ctx.Books.Where(b => b.Price > 1.1 );

        查看这里调用的 Where 方法的声明,我们会发现它是定义在 Queryable 类中的扩展方法方法的声明如下;

IQueryable<TSource> Where<TSource>(this IQueryable<TSource> sourceExpression<Func<TSource, bool>> predicate);

        这个 Where 方法是一个IQueryable<TSource>类型的扩展方法,返回值IQueryabl<TSource类型。IQueryable 其实就是一个继承了 IEnumerable 接口的接口,如下所示;

public interface IOuervable<out T> : IEnumerable<T>, IEnumerable, IQueryablel{}

        IQueryable 接口就是继承自 IEnumerable 接口的; Queryable 类中的 Where方法除了参数和返回值的类型是 IQueryable,其他用法和 Enumerable 类中的 Where 方法没有什么不同。那微软为什么还要推出一个IQueryable 接口以及一个新的 Where 方法

        对于普通集合,Where 方法会在内存中对每条数据进行过滤,而EF Core 如果也把全夏据都在内存中进行过滤的话,我们就需要把一张数据库表中的所有数据都加我到内存中然后通过条件判断逐条进行过法,如果数据量非带大,就会有性能问题。因此EF Core中where实现必须有一套“把 Where 条件转换为 SOL 语句”的机制,让数据的筛选在数据库服务器上执行。使用 SOL语句在数据库服务器上完成数据筛选的过程叫作“服务器评估”把数据首先加裁到应用程序的内存中,然后在内存中进行数据筛选的过程叫作“客户端评估”很显然,对于大部分情况来讲,“客户端评估”性能比较低,我们要尽量避免“客户端评估”。

        Enumerable 类中定义的供普通集合的 Where 等方法都是“客户端评估”,因此微软创造了IQueryable 类型,并且在Queryable等类中定义了和Enumerable类中类似的Where等方法。Queryable 中定义的 Where 方法则支持把 LINQ 查询转换为 SQL 语句。因此,在使用 EF Core的时候,为了避免“客户端评估”,我们要尽量调用 IQueryable 版本的方法,而不是直接调用IEnumerable 版本的方法。

        举个例子说明。使用如下代码获取价格高于1.1元的书。

IQueryable<Book> books = ctx.Books.Where(b => b.Price > 1.1);
foreach (var b in books.Where(b => b.price > 1.1))
{
Console.WriteLine($"Id={b.Id),Title={b.Title}"); 
}

        上面的代码生成的 SQL 语句如下

SELECT [t].[Id],[t].[AuthorName],[t].[Price],[t].[PubTime], [t].[Title) FROM[T_Books] AS [t] WHERE [t].[Price] >1.1000000000000001E0

        这里是 EF Core 在数据库服务器上用 SQL 语句进行的“服务器端评估”,因为 books 变量是IQueryable<Book>类型的,所以这里调用的是 IQueryable 版本的 Where 方法。

        接下来,我们对代码稍微进行改变,把 books 变量的类型从 IQueryable<Book>改为 IEnumerable<Book>,其他代码不做任何改变,如代码所示。

IEnumerablc<Book> books = ctx.Books;
foreach (var b in books,where(b => b.Price > 1.1))
{
Console.WriteLine($"Id={b.Id},Title={b.Title}"); 
}

我们再查看生成的对应的 SQL 语句,如下:

SELECT[t].[Id],[t].[AuthorName],[t].[Price],[t].[PubTime], [t].[Title] FR[T_Books] AS [t]

        这次程序把 T_Books 表中所有的数据都加载到应用程序内存中,然后在内存进行数据的过滤,变成了“客户端评估”。因为 books 是IEnumerable <Book>类型的,所以这里调用的是 IEnumerable 版本的 Where 方法。

        Queryable 类中不仅定义了 Where 方法,还定义了 Select、OrderBy、GroupBy、Min等方法,这些方法和Enumerable 类中定义的同名方法的用法几乎一模一样。唯一不同的就都'它们都是“服务器端评估”的版本。

        总之,在使用 EF Core 的时候,我们要尽量避免“客户端评估”,能用 IQueryable<>m
方就不要直接用IEnumerable<T>。

二,IQueryable 的延迟执行

        IQueryable 不仅可以带来“服务器端评估”这个功能,而且提供了延迟执行的能力。本小节将会对 IQueryable 的延迟执行特性进行介绍。

        编写代码 执行数据查询。

IQueryable<Book> books = ctx.Books.Where(b => b.Price > 1.1); 
Console.WriteLine(books) 

        这段代码只是查询价格大于 11 元的书,但是对于返回值没有遍历输出,我们对 TestDbContext启用了日志输出代码执行的 SQL语句,上面程序的日志输出结果。

        从日志结果输出可以看出,上面的代码竟然没有执行 SQL 语句,而我们明明执行了 Where方法进行数据的过滤查询。

        把代码修改一下,遍历查询结果,如代下所示。

Console.writeLine("1,Where 之前");
IQueryable<Book> books = ctx.Books,Where(b->b,Price>1.1); 
Console.WriteLine("2, 遍历 IQueryable之前");
foreach (var b in books)
{
Console.WriteLine(b.Title + ":" + b.PubTime); 
}
Console.WriteLine("3,遍历IQueryable之后");

       观察上面程序的日志输出结果。请仔细观察上面输出结果的截图中的 SQL 语句、“2.遍历 IQueryable 之前”和“3.遍历 IQueryable 之后”的输出顺序。按照 C#中的代码,Where 调用的代码在“2.遍历 IQueryabl之前”的前面执行,但是在执行结果中,SQL 语句反而在“2.遍历 IQueryable 之前”的后面执行,这是为什么呢?

其实,IQueryable 只是代表“可以放到数据库服务器中执行的查询”,它没有立即执行只是“可以被执行”而已。这一点其实可以从 IQueryable 类型名的英文含义看出来,“IQueryabla”的意思是“可查询的”,可以查询,但是没有执行查询,查询的执行被延迟了。

        那么IQueryable 什么时候才会执行查询。一个原则就是:对于 IQueryable 接口,调用“非立即执行”方法的时候不会执行查询,而调用“立即执行”方法的时候则会立即执行查询。除遍历IQueryable操作之外,还有ToArray、ToList、Min、Max、Count等立即执行方法;GroupE OrderBy、Include、Skip、Take 等方法是非立即执行方法。判断一个方法是否是立即执行方法的简单方式是:一个方法的返回值类型如果是IQueryable类型,这个方法一般就是非立即执行方法,否则这个方法就是立即执行方法。

        EF Core 为什么要实现“IQueryable 延迟执行”这样复杂的机制呢?因为我们可以先使用 IQueryable拼接出复杂的查询条件,再去执行查询。比如,下面的代码中定义了一个方法,这个方法用来根据给定的关键字searchWords 查询匹配的书;如果 searchAll参数是true,则书名或者作者名中含有给定的searchWords 的都匹配,否则只匹配书名;如果 orderByPrice 参数为true则把查询结果按照价格排序,否则就自然排序;upperPrice 参数代表价格上限,如代码下所示

void QueryBooks(string searchWords, bool searchAll, bool orderByPrice, double upperPrice
{
using TestDbContext ctx = new TestDbContext(); 
IQueryable<Book> books = ctx.Books.Where(b => b.Price <= upperPrice); 
if (searchAll) //匹配书名或作者名 
{
books = books.Where(b => b.Title.Contains(searchWords)II 
b.AuthorName.Contains(searchWords));
} 
else //只匹配书名 
{ 
books = books.Where(b => b.Title.Contains(searchWords));
}
if (orderlyPrice) //按照价格排序 
{
books = books.OrderBy(b => b.Price); 
}
foreach (Book b in books) 
{
Console.WriteLine($"ib.Id),{b.Titlo},(b.Price},(b.AuthorName}");
}
}

        我们根据用户传递的参数对 ctx.Books.Where(b=>b.Price<=upperPrice)返回的 IQueryable<Book>对象进一步使用 Where、OrderBy等方法进行过滤,只有到了使用foreach遍历 books 的时候才会执行查询

        我们编写如下代码调用 QueryBooks 方法: 

QueryBooks("爱",true,true,30);

查看上面的代码执行的 SQL 语句的程序的日志输出结果;

        可以看到,我们对 IQueryable 的拼接过程中并没有执行 SQL 语句,只有在最后遍历 IQueryable 的时候才执行 SQL 语句,而且这个 SQL 语句把我们设定的两个 Where 过滤条件合并成了一个 Where 条件,SQL 语句中也包含了我们设置的 Order By 语句。

        我们再尝试调用 QueryBooks 方法

QueryBooks("爱",false,false,18);

查看上面代码执行的 SQL 语句的程序的日志输出结果。

        可以看到,由于传递的参数不同,我们拼接完成的 IQueryable 不同,因此最后执行查询的时候生成的 SQL 语句也不同。

        如果不使用 EF Core 而使用 SQL 语句实现“根据参数不同执行不同 SQL 语句”的逻辑,我们就需要手动拼接 SQL 语句,这个过程是很麻烦的,而 EF Core 把“动态拼接生成查询逻辑”变得非常简单。

        总之,IOueryable 代表一个对数据库中的数据进行查询的逻辑,这个查询是一个延迟查询。我们可以调用非立即执行方法向 IQueryable 中添加查询逻辑,当执行立即执行方法的时候才真正生成 SQL 语句执行查询。

三,lQueryable 的复用

        IQueryable 是一个待查询的逻辑,因此它是可以被重复使用的,如下代码所示。

IQueryable<Book> books = ctx.Books.Where(b => b.Price >=8);
Console.WriteLine(books.Count());
Console.WriteLine(books.Max(b=> b.Price));
foreach(Book b in books.Where(b => b.PubTime.Year >2000))
{
Console.WriteLine(b.Title)i 
}

        上面的代码首先创建了一个获取价格大于等于 8元的书的 IQueryable 对象,然后调用 Count方法执行IQueryable对象获取满足条件的数据条数,接下来调用Max方法执行IQueryable对象获取满足条件的最高的价格,最后对于 books 变量调用 Where 方法进一步过滤获取 2000年之后发布的书。

        上面的代码会生成如下 SQL 语句:

SELECT COUNT(*) FROM [T_Books] AS [t] WHERE [t].[Price] >= 8.0E0;
SELECTMAX([t].[Price]) FROM [T_Books] AS [t] WHERE [t].[Price] >= 8.0E0;
SELECT[t].[Id],[t].[AuthorName],[t].[Price],[t].[PubTime], [t].[Title] FROM
[T_Books] AS [t]
WHERE([t].[Price] >= 8.0EO) AND (DATEPART(year, [t].[PubTime]) > 2000);

        可以看到,由于Count、Max和foreach 都是立即执行操作,因此对IQueryable的这3个操作都各自执行了相应的查询逻辑。IQueryable 让我们可以复用之前生成的查询逻辑,这在下面介绍的分页查询中会用到。

四,EF Core 分页查询

        如果数据库表中的数据比较多,在把查询结果展现到前端的时候,我们通常要对查询结果进行分页展示。

        在实现分页展示效果时,程序需要实现从数据库表中分页获取数据的方法,比如每页显示10条数据,如果要显示第3页(页码从1开始)的数据,我们就要获取从第20条开始的 10 条数据。

        我们知道可以使用Skip(n)方法实现“跳过n条数据”,可以使用 Take(n)方法实现“取最多n条数据”,这两个方法配合起来就可以分页获取数据,比如 Skip(3).Take(8)就是“获取从第3条开始的最多8条数据”。在 EF Core 中也同样支持这两个方法。

        在实现分页的时候,为了显示页码条,我们需要知道满足条件的数据的总条数是多少。可以使用IQueryable的复用,分别实现数据的分页查询和获取满足条件数据总条数这两个查询操作。

        下面封装一个方法,用来输出标题不包含“张三”的第n页(页码从1开始)的内容,并且输出总页数,每页最多显示5条数据,如下代码所示。

void OutputPage(int pageIndex, int pageSize)
{
using TestDbContext ctx = new TestDbContext(); 
IQueryable<Book> books =ctx.Books.Where(b =>lb.Title.Contains("张三")); 
long count = books.LongCount(); 
long pageCount = (long)Math.Ceiling(count *1.0 /pageSize); //页数//总条数
Console.WriteLine("页数:"+ pageCount);
var pagedBooks = books.Skip((pageIndex - 1) * pageSize).Take(pageSize) 
foreach (var b in pagedBooks)
{
Console.WriteLine(b.Id + "," + b.Title);
}
}

        OutputPage 方法的 pageIndex 参数代表页码,pageSize 参数代表页大小。在 OutputPage法中,我们首先把查询规则 books 创建出来,然后使用 LongCount方法获取满足条件的数据总条数。使用 countx1÷pageSize 可以计算出数据总页数,考虑到有可能最后一页不满,因此我们用 Ceiling 方法获得整数类型的总页数。由于 pageIndex 的序号是从I开始的,因此我们更使用 Skip 方法跳过(pageIndex-1)x pageSize 条数据,再获取最多 pageSize 条数据就可以获取正确的分页数据了。

        我们用代码测试输出第1页和第2页的数据。

OutputPage(1,5);
Console.WriteLine("******"); 
OutputPage(2,5)

五,IQueryable 的底层运行

        ADO.NET 中有DataReader 和DataTable 两种读取数据库查询结果的方式。果查询结果有很多条数据,DataTable 会把所有数据一次性地从数据库服务器加载到客户端存中,而DataReader 则会分批从数据库服务器读取数据。DataReader的优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力;DataTable 的优点是数据被快速地加载到客户端内存中,因此不会较长时间地占用数据库连接,缺点是如果数据量大的话,客户端的内存占用会比较大。

        IQueryable 遍历读取数据的时候,用的是类似DataReader 的方式还是类似 DataTable 的式呢?我们在 T_Books 表中插入几十万条数据,然后使用代码遍历 IQueryable。

IQueryable<Book> books = ctx.Books.Where(b=>b.Id>2);
foreach (var b in books) 
{
Console.WriteLine(b.Id +"," + b.Title):
}

        在遍历执行的过程中,如果我们关闭 SQL Server 服务器或者断开服务器的网络,程序就会出错,这说明IQueryable 是用类似 DataReader 的方式读取查询结果的。其实 IQueryable p部的遍历就是在调用 DataReader 进行数据读取。因此,在遍历 IQueryable 的过程中,它需要占用一个数据库连接。

        如果需要一次性把所有数据都读取到客户端内存中,可以用IQueryable 的 ToArray、 ToArrayAsync、ToList、ToListAsync 等方法。如下代码所示,读取前50万条数据,然后使用 ToListAsync 把查询结果一次性地读取到内存中,再去遍历输出数据。

var books = await ctx.Books.Take(500000).ToListAsyne();
foreach (var b in books)
{
Console.WriteLine(b.Id + "," + b.Title):
}

        在遍历数据的过程中,如果我们关闭 SQL Server 服务器或者断开服务器的网络,程序是可以正常运行的,这说明 ToListAsync 方法把查询结果加载到客户端内存中了。

        除非遍历 IQueryable 并且进行数据处理的过程很耗时,否则一般不需要一次性把查询结果读取到内存中。但是在以下场景下,一次性把查询结果读取到内存中就有必要了。

1.场景一:方法需要返回查询结果

        如果方法需要返回查询结果,并且在方法里销毁上下文的话,方法是不能返回 IQueryable的。例如实现如下代码所示的方法,用于查询 Id>5 的书,再返回查询结果。

IQueryable<Book> QueryBooks()
{
using TestDbContext ctx = new TestDbContext(); 
return ctx.Books,Where(b=>b.Id>5);
}

        然后在代码中调用这个方法。

foreach(var b in QueryBooks())
{
Console.WriteLine(b.Title)
}

        上面的代码运行后,程序会抛出“Cannot access a disposed context instance”异常,因为在 QueryBooks 方法中销毁了TestDbContext 对象,而遍历 IQueryable 的时候需要上下文从数据库中加载数据,因此程序就报错了。如果在 QueryBooks 方法中,采用 ToList 等方法把数据一次性加载到内存中就可以了

IEnumerable<Book> QueryBooks()
{
using TestDbContext ctx = new TestDbContext(); 
return ctx.Books.Where(b => b.Id > 5).ToArray();
}

2.场景二:多个 IQueryable 的遍历嵌套

        在遍历一个 IQueryable 的时候,我们可能需要同时遍历另外一个IQueryable。IQueryable底层是使用 DataReader 从数据库服务器读取查询结果的,而很多数据库是不支持多个 DataReader 同时执行的。

        我们使用 SQL Server 数据库,实现两个 IQueryable 一起遍历,如下代码所示。

var books = ctx.Books.Where(b=>b.Id>1);
foreach (var b in books)
{
Console.WriteLine (b.Id + "," + b.Title); 
foreach(var a in etx.Authors) 
{
Console.WriteLine(a,Id);
}
}

        上面的程序执行的时候会报错“There is already an open DataReader associated with this Connection which must be closed first.”,这个错误就是因为两个 foreach 循环都在遍历 IQueryable,导致同时有两个 DataReader 在执行。

        虽然可以在连接字符串中通过设置 utipleActiveResultSets=true 开启“允许多个 DataReader 执行”,但是只有SQLServer支持MultipleActiveResultSets选项,其他数据库有能不支持。因此作者建议采用“把数据一次性加载到内存”以改造其中一个循环的方式来解决,比如只要把var books=ctx.Books.Where(b=>b.Id>1)改为 var books = ctx.Books. Where
(b=>b.Id>1).ToList()就可以了。

        综上所述,在进行日常开发的时候,我们直接遍历 IQueryable 即可。但是如果方法需要返回查询结果或者需要多个查询嵌套执行,就要考虑把数据一次性加载到内存的方式,当然一次性查询的数据不能太多,以免造成过高的内存消耗。

猜你喜欢

转载自blog.csdn.net/xxxcAxx/article/details/128460582
今日推荐