Join LINQ 中的作業

兩個資料來源的「聯結」,就是某個資料來源中的物件,和另一個資料來源中共用通用屬性的物件的關聯。

查詢目標資料來源時,如果無法直接追蹤這些來源彼此的關係,Join就是很重要的作業方式。 在物件導向的程式設計中,聯結可能表示物件之間的相互關聯沒有模組化,例如單向關聯性的返回方向。 一個單面關聯性的範例是 Student 類別,其具有代表主要型別 Department 的屬性,但 Department 類別沒有 Student 物件集合的屬性。 若您有 Department 物件清單,且您想要尋找每個部門的所有學生,您就可以使用聯結作業來進行尋找。

LINQ 架構中所提供的 join 方法是 JoinGroupJoin。 這些方法會執行等聯結,或是執行根據其索引鍵相等與否配對兩個資料來源的聯結。 (相較下,Transact-SQL 支援 equals 以外的聯結運算子,例如 less than 運算字)。在關聯式資料庫規定中,Join 會實作內部聯結,在這種聯結中,只會傳回在其他資料集中有相符項目的物件。 GroupJoin 方法從關聯式資料庫觀點來看沒有直接的對應項目,但它會實作內部聯結和左方外部聯結的超集。 左方外部聯結是傳回第一個 (左) 資料來源中每個項目的聯結,即使它在其他資料來源中沒有相互關聯的項目也一樣。

以下概念圖示範兩個集合,以及兩個集合中包含在內部聯結或左外部聯結中的項目。

Two overlapping circles showing inner/outer.

方法

方法名稱 描述 C# 查詢運算式語法 相關資訊
Join 根據索引鍵選取器函式Join兩個序列並擷取值組。 join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin 根據索引鍵選取器函式Join兩個序列,並為每個項目的相符結果進行分組。 join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

本文中的下列範例會使用適用於此區域的通用資料來源:

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}
public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

每個 Student 都有一個等級、一個主要部門和一系列分數。 Teacher 也有一個 City 屬性,可識別教師持有課程的校園。 Department 具有名稱,以及擔任部門負責人 Teacher 的參考。

下列範例使用 join … in … on … equals … 子句,根據特定值聯結兩個序列:

var query = from student in students
            join department in departments on student.DepartmentID equals department.ID
            select new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name };

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

上述查詢可以使用方法語法來表示,如下列程式碼所示:

var query = students.Join(departments,
    student => student.DepartmentID, department => department.ID,
    (student, department) => new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name });

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

下列範例使用 join … in … on … equals … into … 子句,根據特定值聯結兩個序列,並將每個元素產生的相符結果分組:

IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
                    join student in students on department.ID equals student.DepartmentID into studentGroup
                    select studentGroup;

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

上述查詢可以使用方法語法來表示,如下列範例所示:

// Join department and student based on DepartmentId and grouping result
IEnumerable<IEnumerable<Student>> studentGroups = departments.GroupJoin(students,
    department => department.ID, student => student.DepartmentID,
    (department, studentGroup) => studentGroup);

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

執行內部聯結

在關聯式資料庫術語中,內部聯結產生的結果集中,第一個集合的每個項目都會為第二個集合中的每個相符項目出現一次。 如果第一個集合中的元素沒有相符的元素,就不會出現在結果集中。 C# 中 join 子句呼叫的 Join 方法會實作內部聯結。 下列範例會說明如何執行內部聯結的四種變化:

  • 簡單內部聯結,根據簡單的索引鍵將兩個資料來源的項目相互關聯。
  • 內部聯結,根據「複合」的索引鍵將兩個資料來源的項目相互關聯。 複合索引鍵,也就是由多個值組成的索引鍵,可讓您根據多個屬性將項目相互關聯。
  • 「多個聯結」,彼此附加的連續聯結作業。
  • 使用群組聯結實作的內部聯結。

單一索引鍵聯結

下列範例會比對 Teacher 物件和 Deparment 物件 (其 TeacherIdTeacher 相符)。 C# 的 select 子句會定義產生物件的外觀。 在下列範例中,產生的物件為匿名型別,其中包含部門名稱,以及領導該部門的教師名稱。

var query = from department in departments
            join teacher in teachers on department.TeacherID equals teacher.ID
            select new
            {
                DepartmentName = department.Name,
                TeacherName = $"{teacher.First} {teacher.Last}"
            };

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

您可以使用 Join 方法語法實現相同的結果:

var query = teachers
    .Join(departments, teacher => teacher.ID, department => department.TeacherID,
        (teacher, department) =>
        new { DepartmentName = department.Name, TeacherName = $"{teacher.First} {teacher.Last}" });

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

不是部門負責人的教師不會出現在最終結果中。

複合索引鍵聯結

非僅根據一個屬性相互關聯的項目,您可以使用複合索引鍵根據多個屬性來比較項目。 請為每個集合指定索引鍵選取器函式,以傳回包含要比較屬性的匿名型別。 如果您標示屬性,它們在每個索引鍵的匿名型別中必須有相同的標籤。 屬性也必須以相同的順序出現。

下列範例會使用 Teacher 物件清單和 Student 物件清單來判斷哪些教師也是學生。 這兩種型別都具有代表每個人的名字和姓氏的屬性。 從每個清單的項目建立聯結索引鍵的函式會傳回包含屬性的匿名型別。 聯結作業會比較這些複合索引鍵是否相等,並傳回每份清單中名字和姓氏相符的物件配對。

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query =
    from teacher in teachers
    join student in students on new
    {
        FirstName = teacher.First,
        LastName = teacher.Last
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select teacher.First + " " + teacher.Last;

string result = "The following people are both teachers and students:\r\n";
foreach (string name in query)
{
    result += $"{name}\r\n";
}
Console.Write(result);

您可以使用 Join 方法,如下列範例所示:

IEnumerable<string> query = teachers
    .Join(students,
        teacher => new { FirstName = teacher.First, LastName = teacher.Last },
        student => new { student.FirstName, student.LastName },
        (teacher, student) => $"{teacher.First} {teacher.Last}"
 );

Console.WriteLine("The following people are both teachers and students:");
foreach (string name in query)
{
    Console.WriteLine(name);
}

多重聯結

彼此可以附加任何數目的聯結作業,以執行多個聯結。 C# 中每個 join 子句都會相互關聯指定的資料來源與上一個聯結的結果。

第一個 join 子句會根據和 Department 物件的 ID 相符的 Student 物件的 DepartmentID 比對學生和部門。 它會傳回一系列包含 Student 物件和 Department 物件的匿名型別。

第二個 join 子句會根據和部門負責人識別碼相符的教師識別碼,將第一個聯結所傳回的匿名型別與 Teacher 物件相互關聯。 它會傳回一系列的匿名型別,其中包含學生的姓名、部門名稱和部門領導者的姓名。 因為此作業是內部聯結,所以只會傳回第一個資料來源中在第二個資料來源中有相符項目的這些物件。

// The first join matches Department.ID and Student.DepartmentID from the list of students and
// departments, based on a common ID. The second join matches teachers who lead departments
// with the students studying in that department.
var query = from student in students
    join department in departments on student.DepartmentID equals department.ID
    join teacher in teachers on department.TeacherID equals teacher.ID
    select new {
        StudentName = $"{student.FirstName} {student.LastName}",
        DepartmentName = department.Name,
        TeacherName = $"{teacher.First} {teacher.Last}"
    };

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

使用多個 Join 方法的對等方法會使用與匿名型別相同的方法:

var query = students
    .Join(departments, student => student.DepartmentID, department => department.ID,
        (student, department) => new { student, department })
    .Join(teachers, commonDepartment => commonDepartment.department.TeacherID, teacher => teacher.ID,
        (commonDepartment, teacher) => new
        {
            StudentName = $"{commonDepartment.student.FirstName} {commonDepartment.student.LastName}",
            DepartmentName = commonDepartment.department.Name,
            TeacherName = $"{teacher.First} {teacher.Last}"
        });

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

使用群組聯結的內部聯結

下例會示範如何使用群組聯結實作內部聯結。 Department 物件清單是根據和 Student.DepartmentID 屬性相符的 Department.ID 以群組方式聯結至 Student 物件清單。 群組聯結會建立中繼群組的集合,其中每個群組都包含一個 Department 物件和一系列的相符 Student 物件。 第二個 from 子句會將此系列的序列合併 (或壓平合併) 成一個較長的序列。 select 子句會指定最終序列中的元素型別。 該型別是匿名型別,其中包含學生的姓名和相符的部門名稱。

var query1 =
    from department in departments
    join student in students on department.ID equals student.DepartmentID into gj
    from subStudent in gj
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
    };
Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in query1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

您可以使用 GroupJoin 方法達成相同的結果,如下所示:

var queryMethod1 = departments
    .GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, gj) => new { department, gj })
    .SelectMany(departmentAndStudent => departmentAndStudent.gj,
        (departmentAndStudent, subStudent) => new
        {
            DepartmentName = departmentAndStudent.department.Name,
            StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
        });

Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in queryMethod1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

此結果相當於使用不含 into 子句的 join 子句執行內部聯結所取得的結果集。 下列程式碼可示範此對等查詢:

var query2 = from department in departments
    join student in students on department.ID equals student.DepartmentID
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    };

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in query2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

若要避免鏈結,可以使用單一 Join 方法,如這裡所示:

var queryMethod2 = departments.Join(students, departments => departments.ID, student => student.DepartmentID,
    (department, student) => new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    });

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in queryMethod2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

執行群組聯結

群組聯結適合用來產生階層式資料結構。 它會配對第一個集合中的項目和第二個集合中一組相互關聯的項目。

注意

無論第二個集合中是否找到相互關聯的項目,第一個集合的每個項目都會出現在群組聯結的結果集中。 如果找不到任何相互關聯的項目,該項目之相互關聯項目的序列是空的。 因此,結果選取器可以存取第一個集合的每個項目。 和非群組聯結中的結果選取器不同,它無法存取和第二個集合沒有相符項目的第一個集合項目。

警告

Enumerable.GroupJoin 在傳統關聯式資料庫詞彙中沒有直接對等項目。 不過,此方法確實會實作內部聯結和左方外部聯結的超集。 這兩項作業都會以分組聯結的方式撰寫。 如需詳細資訊,請參閱 Entity Framework Core,GroupJoin

本文中的第一個範例示範如何執行群組聯結。 第二個範例會示範如何使用群組聯結建立 XML 元素。

群組聯結

下例會根據符合 Student.DepartmentID 屬性的 Deoartment.ID,執行型別 DepartmentStudent 的物件群組聯結。 和非群組聯結不同,它會針對每個相符產生元素配對,群組聯結只會針對第一個集合的每個元素產生結果物件,在本範例中為 Department 物件。 第二個集合的對應項目,在本例中為 Student 物件,會群組成集合。 最後,結果選取器函式會為每個符合項目建立匿名型別,包含 Department.NameStudent 物件集合。

var query = from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new
    {
        DepartmentName = department.Name,
        Students = studentGroup
    };

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

在上述範例中, query 變數包含建立清單的查詢,其中每個元素都是匿名型別,其中包含該部門的名稱和在該部門學習的學生集合。

使用方法語法的對等查詢會顯示在下列程式碼中:

var query = departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
    (department, Students) => new { DepartmentName = department.Name, Students });

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

用於建立 XML 的群組聯結

群組聯結最適合使用 LINQ to XML 建立 XML。 下例類似上例,只不過它不是建立匿名型別,而是結果選取器函式會建立表示已加入物件的 XML 元素。

XElement departmentsAndStudents = new("DepartmentEnrollment",
    from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new XElement("Department",
        new XAttribute("Name", department.Name),
        from student in studentGroup
        select new XElement("Student",
            new XAttribute("FirstName", student.FirstName),
            new XAttribute("LastName", student.LastName)
        )
    )
);

Console.WriteLine(departmentsAndStudents);

使用方法語法的對等查詢會顯示在下列程式碼中:

XElement departmentsAndStudents = new("DepartmentEnrollment",
    departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, Students) => new XElement("Department",
            new XAttribute("Name", department.Name),
            from student in Students
            select new XElement("Student",
                new XAttribute("FirstName", student.FirstName),
                new XAttribute("LastName", student.LastName)
            )
        )
    )
);

Console.WriteLine(departmentsAndStudents);

執行左方外部聯結

左方外部聯結是第一個集合中的每個項目都會傳回的聯結,不論它在第二個集合中是否有任何相互關聯的項目。 您可以對群組聯結的結果呼叫 DefaultIfEmpty 方法,使用 LINQ 執行左方外部聯結。

下列範例示範如何對群組聯結的結果使用 DefaultIfEmpty 方法,來執行左方外部聯結。

產生兩個集合的左方外部聯結的第一個步驟,是使用群組聯結執行內部聯結。 (如需此程序的說明,請參閱執行內部聯結。)在本範例中,Department 物件的清單會根據和學生的 DepartmentID 相符的 Department 物件識別碼內部聯結到 Student 物件的清單。

第二個步驟是在結果集中包含第一個 (左) 集合中的每個項目,即使該元素在右集合中沒有相符的項目。 對來自群組聯結的每個相符項目序列呼叫 DefaultIfEmpty 即可完成此作業。 在此範例中,會在每個相符 Student 物件的序列上呼叫 DefaultIfEmpty。 如果任何 Department 物件的相符 Student 物件序列是空的,方法會傳回包含單一預設值的集合,確保結果集合會顯示每個 Department 物件。

注意

參考型別的預設值是 null,因此範例會先檢查 Null 參考,再存取每個 Student 集合的每個項目。

var query =
    from student in students
    join department in departments on student.DepartmentID equals department.ID into gj
    from subgroup in gj.DefaultIfEmpty()
    select new
    {
        student.FirstName,
        student.LastName,
        Department = subgroup?.Name ?? string.Empty
    };

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

使用方法語法的對等查詢會顯示在下列程式碼中:

var query = students.GroupJoin(departments, student => student.DepartmentID, department => department.ID,
    (student, department) => new { student, subgroup = department.DefaultIfEmpty() })
    .Select(gj => new
    {
        gj.student.FirstName,
        gj.student.LastName,
        Department = gj.subgroup?.FirstOrDefault()?.Name ?? string.Empty
    });

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

另請參閱