语言集成查询(LINQ)包含许多复杂运算符,这些运算符结合了多个数据源或执行复杂的处理。 并非所有 LINQ 运算符在服务器端都有合适的翻译。 有时,一个查询以某种形式可以传送到服务器,但如果以其他形式写,即使结果相同,也无法传送。 本页介绍一些复杂运算符及其支持的变体。 在未来版本中,我们可以识别更多模式并添加相应的翻译。 请记住,翻译支持在提供商之间有所不同,这一点也很重要。 在 SqlServer 中转换的特定查询可能不适用于 SQLite 数据库。
小窍门
可以在 GitHub 上查看本文 的示例 。
联接
LINQ Join 运算符允许你根据每个数据源的键选择器链接两个数据源,当键匹配时生成对应值的元组。 在关系数据库中,它自然会转化为 INNER JOIN
。 虽然 LINQ 联接具有外部键和内部键选择器,但数据库需要单个联接条件。 因此,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 };
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 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 可以转换为服务器端的各种不同查询。
集合选择器不引用外部
当集合选择器未引用外部源中的任何内容时,结果是两个数据源的笛卡尔乘积。 在关系数据库中,它被翻译为 CROSS JOIN
。
var query = from b in context.Set<Blog>()
from p in context.Set<Post>()
select new { b, p };
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]
集合选择器在 where 子句中引用外部元素
当集合选择器具有引用外部元素的 where 子句时,EF Core 会将其转换为数据库联接,并使用谓词作为联接条件。 通常,在外部元素上使用集合导航作为集合选择器时,会出现这种情况。 如果外部元素的集合为空,则不会为该外部元素生成任何结果。 但是,如果 DefaultIfEmpty
对集合选择器应用,则外部元素将与内部元素的默认值连接。 由于这种区别,此类查询在缺少DefaultIfEmpty
和LEFT JOIN
时,在应用DefaultIfEmpty
时转换为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 };
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]
集合选择器在非"where"情形中引用了外部元素
当集合选择器引用不在 where 子句内的外部元素时(如上例所示),它不会转换为数据库连接。 因此,我们需要评估每个外部元素的集合选择器。 它在许多关系数据库中转换为 APPLY
操作。 如果外部元素的集合为空,则不会为该外部元素生成任何结果。 但是,如果 DefaultIfEmpty
对集合选择器应用,则外部元素将与内部元素的默认值连接。 由于这种区别,在应用DefaultIfEmpty
时,此类查询会在缺少DefaultIfEmpty
和OUTER APPLY
的情况下转换为CROSS APPLY
。 某些数据库(如 SQLite)不支持 APPLY
运算符,因此此类查询可能无法翻译。
var query = from b in context.Set<Blog>()
from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title)
select new { b, p };
var query2 = from b in context.Set<Blog>()
from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title).DefaultIfEmpty()
select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
CROSS APPLY [Posts] AS [p]
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
OUTER APPLY [Posts] AS [p]
按组分组
LINQ GroupBy 运算符创建一个类型 IGrouping<TKey, TElement>
的结果,其中 TKey
可以是任意类型,也可以 TElement
是任意类型。 此外, IGrouping
实现 IEnumerable<TElement>
,这意味着可以在分组后使用任何 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 的 where 子句使用了HAVING
子句。 在应用 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() };
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 |
---|---|
Average(x => x.Property) | AVG(属性) |
Count() | COUNT(*) |
LongCount() | COUNT(*) |
Max(x => x.Property) | MAX(属性) |
Min(x => x.Property) | MIN(属性) |
求和(x => x.Property) | SUM(属性) |
可能支持其他聚合运算符。 检查提供程序文档以获取更多函数映射信息。
尽管没有代表IGrouping
的数据库结构,但在某些情况下,EF Core 7.0 和更高版本可以在从数据库返回结果后进行分组。 这类似于 Include
运算符在包括相关集合时的工作方式。 以下 LINQ 查询使用 GroupBy 运算符按 Price 属性的值对结果进行分组。
var query = context.Books.GroupBy(s => s.Price);
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]
在这种情况下,GroupBy 运算符并不直接转换为 SQL 中的 GROUP BY
子句,而是结果从服务器返回后,分组会被创建。
左联接
虽然 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 };
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]
上述模式在表达式树中创建复杂结构。 因此,EF Core 要求你在紧跟该运算符的步骤中平展 GroupJoin 运算符的分组结果。 即使使用 GroupJoin-DefaultIfEmpty-SelectMany,但采用不同的模式,我们可能不将其标识为左联接。