共用方式為


Join LINQ 中的作業

兩個數據源的 聯結是將一個數據源中的物件與另一個數據源中共用通用屬性的物件進行關聯。

重要

這些範例會使用 System.Collections.Generic.IEnumerable<T> 資料來源。 根據 System.Linq.IQueryProvider 的資料來源會使用 System.Linq.IQueryable<T> 資料來源和運算式樹狀架構。 運算式樹狀架構在允許的 C# 語法方面有限制。 此外,每個 IQueryProvider 資料來源 (例如 EF Core) 可能會施加更多限制。 檢查資料來源的文件。

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

LINQ 架構中提供的聯結方法 JoinGroupJoin。 這些方法會執行等聯結,或是執行根據其索引鍵相等與否配對兩個資料來源的聯結。 (如需比較,Transact-SQL 支援 equals以外的聯結運算符,例如 less than 運算符。在關係資料庫詞彙中,Join 實作內部聯結,其中只會傳回在其他數據集中具有相符對象的聯結類型。 GroupJoin 方法從關聯式資料庫觀點來看沒有直接的對應項目,但它會實作內部聯結和左方外部聯結的超集。 左外部聯結是一個聯結,即使在另一個數據源中沒有對應元素,也會傳回第一個(左)數據源的每個元素。

下圖顯示兩組集合的概念檢視,以及包含在內部聯結或左外部聯結中之集合內的元素。

顯示內部/外部的兩個重疊圓形。

方法

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

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

Queryable.GroupJoin

注意

本文中的下列範例會使用此區域的通用數據源。
每個 Student 都有一個等級、一個主要部門和一系列分數。 Teacher 也有一個 City 屬性,可識別教師持有課程的校園。 Department 具有名稱,以及擔任部門負責人 Teacher 的參考。
您可以在來源存放庫中找到範例數據集

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; }
}

下列範例會使用 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}");
    }
}

執行內部聯結

在關係資料庫詞彙中,內部聯結 會產生結果集,其中第一個集合的每個元素都會針對第二個集合中的每個相符項目顯示一次。 如果第一個集合中的元素沒有相符的元素,就不會出現在結果集中。 Join 方法由 C# 中的 join 子句呼叫,實現了內部聯結。 下列範例示範如何執行內聯接的四種變化:

  • 根據簡單的鍵值,將來自兩個數據源的元素進行簡單的內部聯結,使其相互關聯。
  • 根據 複合 鍵,將來自兩個數據源的元素進行內部連接和關聯。 複合索引鍵,也就是由多個值組成的索引鍵,可讓您根據多個屬性將項目相互關聯。
  • 多個聯結,其中後續的聯結作業依序接續進行。
  • 使用群組聯結實作的內部聯結。

單一鍵連接

下列範例會比對 Teacher 物件和 Department 物件 (其 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 子句會根據和 Student 物件的 DepartmentID 相符的 Department 物件的 ID 比對學生和部門。 它會傳回一系列包含 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 屬性相符的 Department.ID 以群組方式聯結至 Student.DepartmentID 物件清單。 群組聯結會建立中繼群組的集合,其中每個群組都包含一個 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}");
}

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

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 元素。

群組聯結

下列範例會針對類型為 DepartmentStudent 的物件,依據其 Department.ID 屬性與 Student.DepartmentID 屬性的匹配,執行群組聯結。 群組聯結與非群組聯結不同,非群組聯結會為每個匹配項目生成多個元素對,而群組聯結僅針對第一個集合的每個項目產生一個結果物件,在此範例中為一個 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 物件的清單會根據和學生的 Student 相符的 Department 物件識別碼內部聯結到 DepartmentID 物件的清單。

第二個步驟是在結果集中包含第一個 (左) 集合中的每個項目,即使該元素在右集合中沒有相符的項目。 這可藉由在群組連接的每個相符元素序列上呼叫 DefaultIfEmpty 來完成。 在此範例中,會在每個相符 DefaultIfEmpty 物件的序列上呼叫 Student。 如果任何 Student 物件的相符 Department 物件序列是空的,方法會傳回包含單一預設值的集合,確保結果集合會顯示每個 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, departmentList) => new { student, subgroup = departmentList })
    .SelectMany(
        joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
        (student, department) => new
        {
            student.student.FirstName,
            student.student.LastName,
            Department = department.Name
        });

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

另請參閱