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※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-10-06 11:12  qinway  阅读(12)  评论(0)    收藏  举报