使用LINQ进行左外连接——理解代码

7
我希望有人能解释一下在使用LINQ时,术语"into"的含义。通常情况下,我试图理解如何在C#中进行INNER JOIN、LEFT OUTER JOIN等操作。
我有一个名为"学生"的主表,其中存储了一些外键ID,当运行查询时,这些ID将被替换为它们的名称。这些名称是从查找表(如Marks、SoftwareVersions、Departments等)中读取的。所有字段都是必需的,但MarkID除外。我尝试在LINQ中构建的查询如下:
SELECT * FROM dbo.Students
INNER JOIN dbo.Departments ON dbo.Students.DepartmentID=dbo.Departments.DepartmentID
INNER JOIN dbo.SoftwareVersions ON dbo.Students.SoftwareVersionID=dbo.SoftwareVersions.SoftwareVersionID
INNER JOIN dbo.Statuses ON dbo.Students.StatusID=dbo.Statuses.StatusID
LEFT JOIN dbo.Marks ON dbo.Students.MarkID=dbo.Marks.MarkID
WHERE dbo.Students.DepartmentID=17;

我通过阅读大量文章和观看一些视频,终于成功运行了下面的代码,但我感觉自己对代码还没有完全理解。让我困惑的部分在第5行以into结尾,然后在接下来的一行以from m ...开头。我不知道into的作用是什么,也不知道from m ...到底发生了什么。这是使用LINQ的代码:

var result = from st in dbContext.Students where st.DepartmentID == 17
             join d in dbContext.Departments on st.DepartmentID equals d.DepartmentID
             join sv in dbContext.SoftwareVersions on st.SoftwareVersionID equals sv.SoftwareVersionID
             join stat in dbContext.Statuses on st.StatusID equals stat.StatusID
             join m in dbContext.Marks on st.MarkID equals m.MarkID into marksGroup
             from m in marksGroup.DefaultIfEmpty()
             select new
             {
                 student = st.StudentName,
                 department = p.DepartmentName,
                 software = sv.SoftwareVersionName,
                 status = st.StatusName,
                 marked = m != null ? m.MarkName : "-- Not marked --"
             };

请参见:https://dev59.com/U2865IYBdhLWcg3wW9RE#3855926 - Magnus
请每个帖子只提出一个问题。这两个问题涉及与EF的完全不同的方面。有关第一个问题,请参见此处:https://dev59.com/fWUp5IYBdhLWcg3wAjxF#15599143 - Gert Arnold
关于内连接和左外连接的一些信息,我建议参考 https://www.youtube.com/watch?v=Te2o5qakvZk 和 https://www.youtube.com/watch?v=5K8jwrlKV8E。 - Sebastian 506563
谢谢您的评论。我将编辑此问题并在另一个问题中询问异常情况。 - Celdor
2个回答

4
我认为如何执行左外连接MSDN页面中的示例部分解释得非常清楚。让我们将其应用到您的示例中。引用该页面的第一段话:

生成两个集合的左外连接的第一步是使用组联接执行内部联接。(有关此过程的说明,请参见C#编程指南中的{{link2:如何执行内部联接}}。)在此示例中,基于与Pet.Owner匹配的Person对象,将Person对象列表与Pet对象列表进行内部联接。

因此,在您的情况下,第一步是根据Students对象中的MarkIDMarks对象中的MarkID匹配,执行Students对象列表与Marks对象列表的内部连接。如引用所示,使用group join执行内部连接。如果您在MSDN页面上检查如何执行组连接的Note部分,您会发现:

每个第一个集合的元素都出现在组连接的结果集中,无论是否在第二个集合中找到相关元素。 如果没有找到相关元素,则该元素的相关元素序列为空。 因此,结果选择器可以访问第一个集合的每个元素。

在您的示例中,使用 into 可以使结果进行分组连接,其中包含所有 Students 对象和 Marks 对象的相关元素序列(如果没有匹配的 Marks 对象,则序列将为空)。
现在让我们回到 MSDN 页面上的“如何执行左外部连接”第二段。
第二步是即使第一个(左侧)集合中的元素在右侧集合中没有匹配项,也要将其包括在结果集中。这可以通过在每个匹配元素序列上调用 DefaultIfEmpty 来实现。在本示例中,对每个匹配 Pet 对象的序列调用 DefaultIfEmpty。该方法返回一个集合,如果任何 Person 对象的匹配 Pet 对象序列为空,则包含单个默认值,从而确保每个 Person 对象都在结果集合中表示。

再次以您的示例为例,DefaultIsEmpty()在每个匹配的Marks对象序列上被调用。如上所述,该方法返回一个集合,如果任何Student对象的匹配Marks对象序列为空,则该集合包含一个单一的默认值,这确保了每个Student对象都将在结果集合中表示。因此,您拥有的是一组元素,其中包含所有Student对象和匹配的Marks对象,或者如果没有匹配的Marks对象,则为Marks的默认值,在这种情况下为null


好的。我感觉我已经接近答案了。我理解了直到(包括)“into”的所有内容。它按Student分组结果。但是,假设每个学生可以有多个成绩。我相信我有一个结构化的分组连接结果,如下所示:Student1和来自连接的其他字段:Student1的5个标记列表Student2 ...:nullStudent3 ...:2个标记列表等。如何将其投影到一个简单的SQL数据库表中,其中每个StudentX都有可能重复记录每个StudentX的所有可能Marks - Celdor
很抱歉,我不确定我理解您的问题。您是否想知道此 EF 查询将生成什么 SQL 查询?此外,您是否检查了分组连接的示例?该示例包含一种情况,即左侧集合中的1个元素具有来自右侧集合的多个关联元素,这可能与您的问题相关。 - Michael
抱歉,我的英语不是母语! - Celdor
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Celdor
是的,它会将每一行可能的结果投射到新的匿名类型实例中。如果你想要像group join示例中那样有结构,你需要摆脱from m in marksGroup.DefaultIfEmpty(),并将你的选择重写为select new {..., marks = marksGroup};,这将给你一个学生和该学生每行可用的所有marks。如果没有成绩存在,marks将为空。 - Michael
谢谢你的帮助。花了一些时间,但我终于明白了 :) - Celdor

1
我能说的是,“into MarksGroup”将您加入的表格数据存储在一个临时结果集中(应用程序为基础,不是数据库为基础)(在SQL术语中:一个表格,因此它是一个SELECT INTO)。
接下来一行代码从Marksgroup中选择包含您数据的列(在SQL术语中:SELECT student, department, software, status, marked FROM Marksgroup)。
所以基本上,它从数据库中获取您的数据,然后将其放在“Marksgroup”中,然后在下一步中重新获取Marksgroup以取出您想在C#代码中使用的数据。
尝试摆脱Marksgroup,这应该是可能的(没有使用您的代码进行测试)。它应该是这样的:
from st in dbContext.Students where st.DepartmentID == 17
             join d in dbContext.Departments on st.DepartmentID equals d.DepartmentID
             join sv in dbContext.SoftwareVersions on st.SoftwareVersionID equals sv.SoftwareVersionID
             join stat in dbContext.Statuses on st.StatusID equals stat.StatusID
             join m in dbContext.Marks on st.MarkID equals m.MarkID

             select new
             {
                 student = st.StudentName,
                 department = p.DepartmentName,
                 software = sv.SoftwareVersionName,
                 status = st.StatusName,
                 marked = m != null ? m.MarkName : "-- Not marked --"
             };

您的第二个问题涉及到 'm':如果没有临时结果集“Marksgroup”,这也应该显示不同的行为。


错误。你刚刚把一个外连接变成了内连接。 - Gert Arnold
@Ziko,虽然您已经接受了这个答案,请注意此查询可能会产生与您的不同结果。 - Gert Arnold
抱歉,你是对的,我没有足够注意纯SQL代码。 - Markus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接