语言集成查询(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 对集合选择器应用,则外部元素将与内部元素的默认值连接。 由于这种区别,此类查询在缺少INNER JOIN和DefaultIfEmpty时,在应用LEFT JOIN时转换为DefaultIfEmpty。
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 对集合选择器应用,则外部元素将与内部元素的默认值连接。 由于这种区别,此类查询在缺少CROSS APPLY和DefaultIfEmpty时,在应用OUTER APPLY时转换为DefaultIfEmpty。 某些数据库(如 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]
GroupBy
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 中将 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() };
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(Property) |
| Count() | COUNT(*) |
| LongCount() | COUNT(*) |
| Max(x => x.Property) | MAX(Property) |
| Min(x => x.Property) | MIN(Property) |
| 求和(x => x.Property) | SUM(Property) |
可能支持其他聚合运算符。 检查提供程序文档以获取更多函数映射。
尽管没有代表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 子句,EF Core 会在从服务器返回结果后创建分组。
左连接
虽然 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,但采用其他的模式,也不能将其标识为 Left Join。