02020501 EF Core高级01-IEnumerable和IQuerable、客户端评估、服务端评估、对数据存在逻辑处理的客户端评估
02020501 EF Core高级01-IEnumerable和IQuerable、客户端评估、服务端评估、对数据存在逻辑处理的客户端评估
1. IEnumerable和IQuerable对比(视频3-23)
1.1 IQuerable示例
- 在02020409章5.2节基础上继续
// Program.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
IQueryable<Comment> comments01 = ctx.Comments.Where(c => c.Message.Contains("微软")); // @1 返回值类型为IQueryable<T>
foreach (var item in comments01)
{
Console.WriteLine(item.Message);
}
Console.WriteLine("********************");
IEnumerable<Comment> comments02 = ctx.Comments.Where(c => c.Message.Contains("微软")); // @2 返回值类型为IEnumerable<T>
foreach(var item in comments02)
{
Console.WriteLine(item.Message);
}
}
Console.ReadLine();
}
}
}
控制台输出:
微软不过如此
微软真牛
微软真水
********************
微软不过如此
微软真牛
微软真水
说明:在@1处和@2处输出结果完全相同。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 查看CLR中IQueryable接口
using System.Collections;
using System.Collections.Generic;
namespace System.Linq
{
//
// 摘要:
// Provides functionality to evaluate queries against a specific data source wherein
// the type of the data is known.
//
// 类型参数:
// T:
// The type of the data in the data source.
public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable
{
}
}
抛出问题:既生IEnumerable<T>,何生IQueryable<T>?
1.2 同List集合来观察
// 观察1:
List<Comment> list = new List<Comment>();
list.Where(c => c.Message.Contains("微软")).Max(); // 可以用
list.Where(c => c.Message.Contains("微软")).GroupBy(...); // 可以用
...
// 观察这里Where的定义
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
说明:
1. 普通的LINQ方法在List<T>中都可以用。
2. 参数为泛型的IEnumerable<TSource>,返回值也为IEnumerable<TSource>。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 观察2:
IQueryable<Comment> comments01 = ctx.Comments.Where(c => c.Message.Contains("微软"));
// 观察这里Where的定义
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
说明:参数为泛型的IQueryable<TSource>,返回值也为IQueryable<TSource>。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:
1、对普通集合和DbSet调用的Where方法,虽然用起来一样,但是“转到定义”后看到的是不同的方法。
2、普通集合的版本(IEnumerable)是在内存中过滤(客户端评估),而IQueryable版本则是EF Core把查询操作翻译成SQL语句(服务器端评估)到服务器端评估。
3、通常来讲,如果数据量比较大(十万条,百万条),那么服务器端评估性能更高。
1.3 IEnumerable和IQueryable比较
// 强制用客户端评估来查询数据库的形式:
1、IQueryable<Book> books = ctx.Books;
books.Where(b => b.Price > 1.1)
2、IEnumerable<Book> books = ctx.Books;
books.Where(b => b.Price > 1.1)
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// step1:继续以在02020409章5.2节来示例,查看默认的服务器端评估
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
// 默认的服务器端评估
IQueryable<Comment> cmts = ctx.Comments.Where(c => c.Message.Contains("微软")); // Comments是DbSet<Comment>属性,DbSet实现了IQueryable<TEntity>接口,默认最匹配的是IQueryable<TEntity>类型。
foreach (var item in cmts)
{
Console.WriteLine(item.Message);
}
}
Console.ReadLine();
}
}
}
控制台输出:
微软不过如此
微软真牛
微软真水
// 查看SQL语句1
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
WHERE [t].[Message] LIKE N'%微软%'
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// step2:继续以在02020409章5.2节来示例,查看强制使用客户端评估
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
// 强制使用客户端评估
IEnumerable<Comment> comments = ctx.Comments; // Comments是DbSet<Comment>属性,DbSet实现了IEnumerable<TEntity>接口,强制使用这个接口
IEnumerable<Comment> cmts = comments.Where(c => c.Message.Contains("微软")); // 强制返回IEnumerable<TEntity>类型
foreach (var item in cmts)
{
Console.WriteLine(item.Message);
}
}
Console.ReadLine();
}
}
}
控制台输出:
微软不过如此
微软真牛
微软真水
// 查看SQL语句2
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 对比step1和step2中的SQL语句
// step1:默认服务端
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
WHERE [t].[Message] LIKE N'%微软%'
// step2:强制客户端
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
说明:
1. step1在服务器端直接获取匹配的结果。
2. step2在服务端将所有数据取出来放到客户端内存,然后在客户端内存中一条条比较获取匹配的结果。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// step3:继续以在02020409章5.2节来示例,查看强制使用服务端评估
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
IQueryable<Comment> comments = ctx.Comments;
IEnumerable<Comment> cmts = comments.Where(c => c.Message.Contains("微软")); // 强制返回IEnumerable<TEntity>类型
foreach (var item in cmts)
{
Console.WriteLine(item.Message);
}
}
Console.ReadLine();
}
}
}
控制台输出:
微软不过如此
微软真牛
微软真水
// 查看SQL语句3
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
WHERE [t].[Message] LIKE N'%微软%'
说明:这种和默认服务端效果相同,推荐使用step1中的写法。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:能用IQueryable用IQueryable,即服务端评估。
2. 客户端评估也有需要(视频3-24)
2.1 服务器端评估和客户端评估使用场景
// 服务器端评估使用场景
IQueryable<Book> books = ctx.Books.Where(b=>b.Price>1.1);
var items = books.Select(b=>new {TitlePre=b.Title.Substring(0,2),PubYear=b.PubTime.Year});
查看生成的SQL语句。
// 客户端评估使用场景
IEnumerable<Book> books = ctx.Books.Where(b => b.Price > 1.1);
var items = books.Select(b=>new {TitlePre=b.Title.Substring(0,2),PubYear=b.PubTime.Year});
查看生成的SQL语句。
2.2 取出评论的前两个字
- 继续以在02020409章5.2节来示例
// step1:服务器端评估
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
// 服务器端评估:只查询Id和Message两列,同时Message只取前两个字,然后在两个字后加“...”。
var cmts = ctx.Comments.Select(c => new { Id = c.Id, Pre = c.Message.Substring(0, 2) + "..." });
foreach (var item in cmts)
{
Console.WriteLine(item.Id + item.Pre);
}
}
Console.ReadLine();
}
}
}
控制台输出:
1太牛...
2微软...
4微软...
5微软...
// 查看SQL语句
SELECT [t].[Id], COALESCE(SUBSTRING([t].[Message], 0 + 1, 2), N'') + N'...' AS [Pre]
FROM [T_Comments] AS [t]
说明:
1. 字符串的拼接是在服务器端实现的。
2. 这种操作对服务器的影响是很小的,但是必进在服务器端进行了运算。
3. 首先这种字符串拼接的写法是可行的,对服务器端性能影响微乎其微。假设我们有十分复杂的字符串或者类似的运算,此时可以考虑用客户端评估来实现。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// step2:服务器端评估
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
// 服务器端评估:在服务器端获取数据,然后在客户端完成字符串相关操作。
var cmts = ((IEnumerable<Comment>)ctx.Comments).Select(c => new { Id = c.Id, Pre = c.Message.Substring(0, 2) + "..." }); // @1
foreach (var item in cmts)
{
Console.WriteLine(item.Id + item.Pre);
}
}
Console.ReadLine();
}
}
}
控制台输出:
1太牛...
2微软...
4微软...
5微软...
// 查看SQL语句
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
说明:
1. 把数据快速的取到客户端来,然后在客户端完成字符串相关操作。
2. 在@1处,((IEnumerable<Comment>)ctx.Comments)表示将IQueryable显式转换为IEnumerable类型。
3. 通过查看SQL语句,是先在服务器端获取数据,然后在客户端完成字符串操作。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
综上:这个示例只是为了演示客户端匹配的作用,实际上还是推荐用step1中的用法。
2.3 对评论做逻辑上的处理
- 比如要查出评论,此时在客户端需要一个单独的方法来处理个性化的需求。
- 继续以在02020409章5.2节来示例
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
// 服务端匹配
var cmts = ctx.Comments.Where(c => IsOk(c.Message));
foreach (var item in cmts)
{
Console.WriteLine(item.Id + item.Message);
}
}
Console.ReadLine();
}
static bool IsOk(string s)
{
// 如下逻辑不重要,主要演示需要对数据进行逻辑上的处理。
if(s.StartsWith("微"))
{
return true;
}
else
{
return false;
}
}
}
}
异常信息:System.InvalidOperationException:“The LINQ expression 'DbSet<Comment>()
.Where(c => Program.IsOk(c.Message))' could not be translated. Additional information: Translation of method 'OneToMany.Program.IsOk' failed. If this method can be mapped to your custom function, see https://go.microsoft.com/fwlink/?linkid=2132413 for more information. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.”
说明:此时EF Core无法翻译成SQL语句,此时只能做客户端评估。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OneToMany
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
// 客户端匹配
var cmts = ((IEnumerable<Comment>)ctx.Comments).Where(c => IsOk(c.Message));
foreach (var item in cmts)
{
Console.WriteLine(item.Id + item.Message);
}
}
Console.ReadLine();
}
static bool IsOk(string s)
{
// 如下逻辑不重要,主要演示需要对数据进行逻辑上的处理。
if (s.StartsWith("微"))
{
return true;
}
else
{
return false;
}
}
}
}
控制台输出:
2微软不过如此
4微软真牛
5微软真水
// 查看SQL语句
SELECT [t].[Id], [t].[Message], [t].[TheArticleId]
FROM [T_Comments] AS [t]
说明:虽然我们要尽量减少客户端评估,但是如上对数据存在逻辑处理的情形下,服务器端无法实现的时候可以先获取数据,然后在客户端进行逻辑处理。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

浙公网安备 33010602011771号