【Entity Framework】聊聊EF中复杂查询运算符
文章目录
- 【Entity Framework】聊聊EF中复杂查询运算符
- 一、概述
- 二、联接
- 三、GroupJoin
- 四、SelectMany
- 4.1/集合选择器不引用外部
- 4.2/集合选择器引用 where 子句中的外部
- 五、GroupBy
- 六、Left Join
- 七、总结
一、概述
语言集成查询 (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.PhotoIdselect new { person, photo };
下面语句生成SQL
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]
此外,如果键选择器是匿名类型,则 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 groupingselect new { b, grouping };
var query = from b in context.Set<Blog>()join p in context.Set<Post>()on b.BlogId equals p.BlogId into groupingselect 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语句如下:
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]
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.AuthorIdinto gselect 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.AuthorIdinto gwhere g.Count() > 0orderby g.Keyselect 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 支持的聚合运算符如下所示
序号 | .NET | SQL |
---|---|---|
1 | Average(x => x.Property) | AVG(Property) |
2 | Count() | Count(*) |
3 | LongCount() | Count(*) |
4 | Max(x => x.Property) | MAX(Property) |
5 | Min(x => x.Property) | MIN(Property) |
6 | Sum(x => x.Property) | SUM(Property) |
可能支持其他聚合运算符。 检查提供程序文档以获取更多函数映射。
尽管没有表示 IGrouping
的数据库结构,但在某些情况下,EF Core 7.0 和更新版本可以在从数据库返回结果后创建分组。 这类似于 Include
运算符在包含相关集合时的工作方式。 以下 LINQ 查询使用 GroupBy 运算符按 Price 属性的值对结果进行分组。
var query = context.Books.GroupBy(s => s.Price);
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 groupingfrom p in grouping.DefaultIfEmpty()select new { b, p };
七、总结
以上模式在表达式树中创建复杂的结构。因此,EF Core
要求在紧随运算符的步骤中将GroupJoin运算符的分组结果平展。即使使用GroupJoin-DefaultIfEmpty-SelectMany
,但采用其他的模式,也不能将其标识为Left Join
。