【Entity Framework】聊聊EF中复杂查询运算符

简介: 【Entity Framework】聊聊EF中复杂查询运算符

一、概述

语言集成查询 (LINQ) 包含许多用于组合多个数据源或执行复杂处理的复杂运算符。 并非所有 LINQ 运算符都会在服务器端进行适当转换。 有时,采用一种形式的查询会转换为服务器,但如果采用另一种形式,即使结果相同,也不会转换。本文将介绍部分复杂运算符及其支持的变体。


二、联接

借助LINQ Join运算符,可根据每个源的键选择器连接两个数据源,并在键匹配时生成值的元组。该运算符在关系数据库中自然而然地转换为INNER JOIN。虽然LINQ Join具有外部和内部键选择器,但数据库只需要一个联接条件。因此EF Core通过比较外部键选择器和内部键选择器是否相等,来生成联接条件。


var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on photo.PersonPhotoId equals person.PhotoId
            select new { person, photo };

下面语句生成SQL

 [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId]
SELECT

此外,如果键选择器是匿名类型,则 EF Core 会生成一个联接条件,以比较组件是否相等。

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on new { Id = (int?)photo.PersonPhotoId, photo.Caption }
                equals new { Id = person.PhotoId, Caption = "SN" }
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON ([p0].[PersonPhotoId] = [p].[PhotoId] AND ([p0].[Caption] = N'SN'))

三、GroupJoin

借助LINQ GroupJoin运算符,可以采用与Join类似的方式连接两个数据源,但它会创建一组内部值,用于匹配外部元素。执行与以下示例类似的查询将生成Blog和IEnumerable<Post>的结果。由于数据库(特别是关系数据库)无法表示一组客户端对象,因此在许多情况下,GroupJoin 不会转换为服务器。 它需要从服务器获取所有数据来进行 GroupJoin,无需使用特殊选择器(下面的第一个查询)。 但如果选择器限制选定的数据,则从服务器提取所有数据可能会导致出现性能问题(下面的第二个查询)。 这就是 EF Core 不转换 GroupJoin 的原因。

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, grouping };
var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, Posts = grouping.Where(p => p.Content.Contains("EF")).ToList() };


四、SelectMany

借助 LINQ SelectMany 运算符,可为每个外部元素枚举集合选择器,并从每个数据源生成值的元组。 在某种程度上,它是没有任何条件的联接,因此每个外部元素都与来自集合源的元素连接。 根据集合选择器与外部数据源的关系,SelectMany 可在服务器端转换为各种不同的查询。

4.1/集合选择器不引用外部

如果集合选择器不引用外部源中的任何内容,则结果是这两个数据源的笛卡尔乘积。 它在关系数据库中转换为 CROSS JOIN

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>()
            select new { b, p };

生成SQL语句如下:

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
CROSS JOIN [Posts] AS [p]

4.2/集合选择器引用 where 子句中的外部

如果集合选择器具有引用外部元素的 where 子句,则 EF Core 会将其转换为数据库联接,并将谓词用作联接条件。 在对外部元素使用集合导航作为集合选择器时,通常会出现这种情况。 如果外部元素的集合为空,则不会为该外部元素生成任何结果。 但如果在集合选择器上应用 DefaultIfEmpty,则外部元素将与内部元素的默认值连接。 由于这种区别,应用 DefaultIfEmpty 时,如果缺少 DefaultIfEmpty 和 LEFT JOIN,此类查询会转换为 INNER JOIN。

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId).DefaultIfEmpty()
             select new { b, p };

生成SQL语句如下:

SESELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]


五、GroupBy

LINQ GroupBy运算符创建IGrouping<TKey,TElement>类型的结果,其中TKey和TElement可以是任意类型,此外,IGrouping实现了IEnumerable<IElement>,这意味着可在分组后使用任意LINQ运算符来对其进行组合。由于任何数据库结构都无法表示IGrouping,因此在大多数情况下GroupBy运算符不会进行任何转换。聚合运算符应用于返回标量的每个组时,该运算符可在关系数据库中转换为 SQL GROUP BY。 SQL GROUP BY 也会受到限制。 它要求只按标量值进行分组。 投影只能包含分组键列或对列应用的任何聚合。 EF Core 标识此模式并将其转换为服务器,如以下示例中所示:

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]

EF Core 还会转换符合以下条件的查询:分组的聚合运算符出现在 Where 或 OrderBy(或其他排序方式)LINQ 运算符中。 它在 SQL 中将 HAVING 子句用于 where 子句。 在应用 GroupBy 运算符之前的查询部分可以是任何复杂查询,只要它可转换为服务器即可。 此外,将聚合运算符应用于分组查询以从生成的源中移除分组后,可以像使用任何其他查询一样,在它的基础上进行组合。

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            where g.Count() > 0
            orderby g.Key
            select new { g.Key, Count = g.Count() };

生成SQL语句:

SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]
HAVING COUNT(*) > 0
ORDER BY [p].[AuthorId]

EF Core 支持的聚合运算符如下所示 图片.png

可能支持其他聚合运算符。 检查提供程序文档以获取更多函数映射。


尽管没有表示 IGrouping 的数据库结构,但在某些情况下,EF Core 7.0 和更新版本可以在从数据库返回结果后创建分组。 这类似于 Include运算符在包含相关集合时的工作方式。 以下 LINQ 查询使用 GroupBy 运算符按 Price 属性的值对结果进行分组。

var query = context.Books.GroupBy(s => s.Price);
• 1

SQL语句执行:

SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

在这种情况下,GroupBy 运算符不会直接转换为 SQL 中的 GROUP BY 子句,EF Core 会在从服务器返回结果后创建分组。


六、Left Join

虽然 Left Join 不是 LINQ 运算符,但关系数据库具有常用于查询的 Left Join 的概念。 LINQ 查询中的特定模式提供与服务器上的 LEFT JOIN 相同的结果。 EF Core 标识此类模式,并在服务器端生成等效的 LEFT JOIN。 该模式包括在两个数据源之间创建 GroupJoin,然后通过对分组源使用 SelectMany 运算符与 DefaultIfEmpty 来平展分组,从而在内部不具有相关元素时匹配 null。 下面的示例显示该模式的样式及其生成的内容。

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            from p in grouping.DefaultIfEmpty()
            select new { b, p };


七、总结

以上模式在表达式树中创建复杂的结构。因此,EF Core要求在紧随运算符的步骤中将GroupJoin运算符的分组结果平展。即使使用GroupJoin-DefaultIfEmpty-SelectMany,但采用其他的模式,也不能将其标识为Left Join

相关实践学习
体验RDS通用云盘核心能力
本次实验任务是创建一个云数据库RDS MySQL(通用云盘),并通过云服务器ECS对RDS MySQL实例进行压测,体验IO加速和IO突发带来的性能提升;并通过DMS执行DDL,将数据归档到OSS,再结合云盘缩容,体验数据归档带来的成本优势。
目录
相关文章
|
6月前
|
开发框架 缓存 .NET
【Entity Framework】EF中DbSet类详解
【Entity Framework】EF中DbSet类详解
113 1
【Entity Framework】EF中DbSet类详解
|
6月前
|
SQL 存储 开发框架
实体框架EF(Entity Framework)简介
实体框架EF(Entity Framework)简介
152 7
|
6月前
|
存储 SQL 开发框架
【Entity Framework】如何使用EF中的生成值
【Entity Framework】如何使用EF中的生成值
50 0
|
6月前
|
SQL 存储 开发框架
【Entity Framework】EF中的增删改查
【Entity Framework】EF中的增删改查
167 0
|
6月前
|
存储 SQL 开发框架
【Entity Framework】你要知道EF中功能序列与值转换
【Entity Framework】你要知道EF中功能序列与值转换
44 0
|
6月前
|
SQL 数据库
【Entity Framework】如何理解EF中的级联删除
【Entity Framework】如何理解EF中的级联删除
60 0
|
6月前
|
开发框架 .NET 数据库
【Entity Framework】EF中SaveChanges如何使用
【Entity Framework】EF中SaveChanges如何使用
57 0
|
6月前
|
SQL 数据库连接 数据库
【Entity Framework】EF连接字符串和模型
【Entity Framework】EF连接字符串和模型
44 0
|
6月前
|
API 数据库 索引
【Entity Framework】聊聊EF中键
【Entity Framework】聊聊EF中键
32 0
|
6月前
|
SQL 开发框架 .NET
【Entity Framework】聊一聊EF如何使用数据库函数
【Entity Framework】聊一聊EF如何使用数据库函数
95 0