使用 LINQ 高效地仅加载嵌套集合的某些元素

Load only some elements of a nested collection efficiently with LINQ

我有以下 LINQ 查询(使用 EF Core 6 和 MS SQL 服务器):

var resultSet = dbContext.Systems
            .Include(system => system.Project)
            .Include(system => system.Template.Type)
            .Select(system => new
            {
                System = system,
                TemplateText = system.Template.TemplateTexts.FirstOrDefault(templateText => templateText.Language == locale.LanguageIdentifier),
                TypeText = system.Template.Type.TypeTexts.FirstOrDefault(typeText => typeText.Language == locale.LanguageIdentifier)
            })
            .FirstOrDefault(x => x.System.Id == request.Id);

要求是检索与请求的 ID 匹配的系统并加载其项目、模板和模板的类型信息。该模板有多个模板文本(每种翻译语言一个),但我只想加载与请求的语言环境匹配的模板文本,同样处理模板类型的 TypeTexts 元素。

上面的 LINQ 查询在一个查询中执行此操作并将其转换为以下 SQL 查询(我编辑了 SELECT 语句以使用 * 而不是生成的一长串列):

SELECT [t1].*, [t2].*, [t5].*
FROM (
    SELECT TOP(1) [p].*, [t].*, [t0].*
    FROM [ParkerSystems] AS [p]
    LEFT JOIN [Templates] AS [t] ON [p].[TemplateId] = [t].[Id]
    LEFT JOIN [Types] AS [t0] ON [t].[TypeId] = [t0].[Id]
    LEFT JOIN [Projects] AS [p0] ON [p].[Project_ProjectId] = [p0].[ProjectId]
    WHERE [p].[SystemId] = @__request_Id_1
) AS [t1]
LEFT JOIN (
    SELECT [t3].*
    FROM (
        SELECT [t4].*, ROW_NUMBER() OVER(PARTITION BY [t4].[ReferenceId] ORDER BY [t4].[Id]) AS [row]
        FROM [TemplateTexts] AS [t4]
        WHERE [t4].[Language] = @__locale_LanguageIdentifier_0
    ) AS [t3]
    WHERE [t3].[row] <= 1
) AS [t2] ON [t1].[Id] = [t2].[ReferenceId]
LEFT JOIN (
    SELECT [t6].*
    FROM (
        SELECT [t7].*, ROW_NUMBER() OVER(PARTITION BY [t7].[ReferenceId] ORDER BY [t7].[Id]) AS [row]
        FROM [TypeTexts] AS [t7]
        WHERE [t7].[Language] = @__locale_LanguageIdentifier_0
    ) AS [t6]
    WHERE [t6].[row] <= 1
) AS [t5] ON [t1].[Id0] = [t5].[ReferenceId]

这还不错,它不是一个超级复杂的查询,但我觉得我的需求可以用更简单的 SQL 查询来解决:

SELECT * 
FROM [Systems] AS [p]
JOIN [Templates] AS [t] ON [p].[TemplateId] = [t].[Id]
JOIN [TemplateTexts] AS [tt] ON [p].[TemplateId] = [tt].[ReferenceId]
JOIN [Types] AS [ty] ON [t].[TypeId] = [ty].[Id]
JOIN [TemplateTexts] AS [tyt] ON [ty].[Id] = [tyt].[ReferenceId]
WHERE [p].[SystemId] = @systemId and tt.[Language] = 2 and tyt.[Language] = 2

我的问题是:是否有一个 different/simpler LINQ 表达式(在方法语法或查询语法中)产生相同的结果(一次性获取所有信息),因为理想情况下我不希望有拥有一个匿名对象,其中聚合了过滤后的子集合。对于更多的布朗尼积分,如果生成的 SQL 是 simpler/closer 我认为是一个简单的查询,那就太好了。

此 LINQ 查询与您的 SQL 接近,但我担心结果的正确性:

var resultSet = 
    (from system in dbContext.Systems
    from templateText in system.Template.TemplateTexts
    where templateText.Language == locale.LanguageIdentifier
    from typeText in system.Template.Type.TypeTexts
    where typeText.Language == locale.LanguageIdentifier
    select new
    {
        System = system,
        TemplateText = templateText
        TypeText = typeText
    })
    .FirstOrDefault(x => x.System.Id == request.Id);

Is there a different/simpler LINQ expression (...) that produces the same result

是(也许)但不是。

不,因为您正在查询 dbContext.Systems,因此 EF 将 return 所有与您的过滤器匹配的系统,即使它们没有 TemplateTexts 等。这就是为什么必须生成外连接。 EF 不知道您明显打算跳过没有这些嵌套数据的系统,也不知道这些系统不会出现在数据库中。 (您似乎假设,看到第二个查询)。

这说明了左连接到子查询。

这些子查询是因为FirstOrDefault而产生的。在 SQL 中,它总是需要某种子查询来获取一对多关系的“第一个”记录。这个 ROW_NUMBER() OVER 构造实际上是相当高效的。您的第二个查询没有任何“第一”记录的概念。它可能 return 不同的数据。

是的(也许),因为您还 Include 数据。我不确定为什么。有些人似乎认为 Include 是进行后续预测 (.Select) 所必需的,但事实并非如此。如果这是您使用 Includes 的原因,那么您可以删除它们,从而删除前几个连接。

OTOH 你也 Include system.Project 不在投影中,所以你似乎故意添加了 Includes。在这种情况下,它们会起作用,因为整个实体 system 都在投影中,否则 EF 会忽略它们。

如果您再次需要 Include,出于上述原因,EF 必须生成外部联接。

EF 决定单独处理 Includes 和预测,而手工制作 SQL,在数据先验知识的帮助下可以更有效地做到这一点。但是没有办法影响这种行为。