Join Operacje w LINQ

Sprzężenie dwóch źródeł danych to skojarzenie obiektów w jednym źródle danych z obiektami, które współdzielą wspólny atrybut w innym źródle danych.

Joining to ważna operacja w zapytaniach przeznaczonych dla źródeł danych, których relacje ze sobą nie mogą być wykonywane bezpośrednio. W programowaniu obiektowym łączenie może oznaczać korelację między obiektami, które nie są modelowane, takie jak kierunek wsteczny relacji jednokierunkowej. Przykładem relacji jednokierunkowej jest Student klasa, która ma właściwość typu Department reprezentującą główną, ale Department klasa nie ma właściwości, która jest kolekcją Student obiektów. Jeśli masz listę Department obiektów i chcesz znaleźć wszystkich uczniów w każdym dziale, możesz użyć operacji sprzężenia, aby je znaleźć.

Metody sprzężenia dostępne w strukturze LINQ to Join i GroupJoin. Te metody wykonują równoczesne lub sprzężenia pasujące do dwóch źródeł danych na podstawie równości kluczy. (Dla porównania język Transact-SQL obsługuje operatory sprzężenia inne niż equals, na przykład less than operator). W warunkach Join relacyjnej bazy danych implementuje sprzężenie wewnętrzne, typ sprzężenia, w którym zwracane są tylko te obiekty, które mają dopasowanie w innym zestawie danych. Metoda GroupJoin nie ma bezpośredniego odpowiednika w terminach relacyjnej bazy danych, ale implementuje nadzbiór sprzężeń wewnętrznych i lewe sprzężenia zewnętrzne. Lewe sprzężenie zewnętrzne to sprzężenie zwracające każdy element pierwszego (po lewej) źródle danych, nawet jeśli nie ma skorelowanych elementów w innym źródle danych.

Poniższa ilustracja przedstawia koncepcyjny widok dwóch zestawów oraz elementy w tych zestawach, które znajdują się w sprzężeniu wewnętrznym lub lewym sprzężeniu zewnętrznym.

Dwa nakładające się okręgi pokazujące wewnętrzne / Zewnętrzne.

Metody

Nazwa metody opis Składnia wyrażeń zapytań języka C# Więcej informacji
Join Joins dwie sekwencje oparte na funkcjach selektora kluczy i wyodrębnia pary wartości. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Joins dwie sekwencje oparte na funkcjach selektora kluczy i grupuje wynikowe dopasowania dla każdego elementu. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

W poniższych przykładach w tym artykule użyto typowych źródeł danych dla tego obszaru:

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

Każdy z nich Student ma poziom klasy, dział podstawowy i serię wyników. Obiekt Teacher ma również właściwość identyfikującą City kampus, w którym nauczyciel posiada zajęcia. Element Department ma nazwę i odwołanie do osoby Teacher , która służy jako szef działu.

W poniższym przykładzie użyto klauzuli join … in … on … equals … , aby połączyć dwie sekwencje na podstawie określonej wartości:

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

Powyższe zapytanie można wyrazić przy użyciu składni metody, jak pokazano w poniższym kodzie:

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

W poniższym przykładzie użyto klauzuli join … in … on … equals … into … , aby połączyć dwie sekwencje na podstawie określonej wartości i grupować wynikowe dopasowania dla każdego elementu:

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

Powyższe zapytanie można wyrazić przy użyciu składni metody, jak pokazano w poniższym przykładzie:

// 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}");
    }
}

Wykonywanie sprzężeń wewnętrznych

W kategoriach relacyjnej bazy danych sprzężenie wewnętrzne generuje zestaw wyników, w którym każdy element pierwszej kolekcji pojawia się jeden raz dla każdego pasującego elementu w drugiej kolekcji. Jeśli element w pierwszej kolekcji nie zawiera pasujących elementów, nie jest wyświetlany w zestawie wyników. Metoda Join , która jest wywoływana przez klauzulę join w języku C#, implementuje sprzężenie wewnętrzne. W poniższych przykładach pokazano, jak wykonać cztery odmiany sprzężenia wewnętrznego:

  • Proste sprzężenie wewnętrzne, które koreluje elementy z dwóch źródeł danych na podstawie prostego klucza.
  • Sprzężenie wewnętrzne, które koreluje elementy z dwóch źródeł danych na podstawie klucza złożonego. Klucz złożony, który jest kluczem składającym się z więcej niż jednej wartości, umożliwia korelowanie elementów na podstawie więcej niż jednej właściwości.
  • Wiele sprzężeń , w których kolejne operacje sprzężenia są dołączane do siebie nawzajem.
  • Sprzężenie wewnętrzne implementowane przy użyciu sprzężenia grupowego.

Sprzężenia pojedynczego klucza

Poniższy przykład pasuje do Teacher obiektów z obiektami Deparment , których TeacherId pasuje do tego Teacherobiektu . Klauzula select w języku C# definiuje wygląd wynikowych obiektów. W poniższym przykładzie wynikowe obiekty są typami anonimowymi, które składają się z nazwy działu i nazwy nauczyciela prowadzącego dział.

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

Te same wyniki są osiągane przy użyciu Join składni metody:

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

Nauczyciele, którzy nie są szefami działów, nie pojawiają się w końcowych wynikach.

Sprzężenia klucza złożonego

Zamiast korelować elementy na podstawie tylko jednej właściwości, można użyć klucza złożonego do porównywania elementów na podstawie wielu właściwości. Określ funkcję selektora kluczy dla każdej kolekcji, aby zwrócić typ anonimowy składający się z właściwości, które chcesz porównać. Jeśli etykietujesz właściwości, muszą mieć tę samą etykietę w typie anonimowym każdego klucza. Właściwości muszą być również wyświetlane w tej samej kolejności.

W poniższym przykładzie użyto listy Teacher obiektów i listy Student obiektów, aby określić, którzy nauczyciele są również uczniami. Oba te typy mają właściwości reprezentujące imię i nazwisko każdej osoby. Funkcje, które tworzą klucze sprzężenia z elementów każdej listy, zwracają typ anonimowy, który składa się z właściwości. Operacja sprzężenia porównuje te klucze złożone pod kątem równości i zwraca pary obiektów z każdej listy, gdzie zarówno imię, jak i nazwa rodziny są zgodne.

// 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);

Możesz użyć Join metody , jak pokazano w poniższym przykładzie:

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

Wiele sprzężeń

Aby wykonać wiele sprzężeń, można dołączyć do siebie dowolną liczbę operacji sprzężenia. Każda klauzula join w języku C# koreluje określone źródło danych z wynikami poprzedniego sprzężenia.

join Pierwsza klauzula pasuje do uczniów i działów w oparciu o Student obiekt pasujący Department do obiektu DepartmentIDID. Zwraca sekwencję typów anonimowych, które zawierają Student obiekt i Department obiekt.

Druga join klauzula koreluje typy anonimowe zwracane przez pierwsze sprzężenia z obiektami Teacher na podstawie identyfikatora tego nauczyciela pasującego do identyfikatora głównego działu. Zwraca sekwencję typów anonimowych, które zawierają nazwę ucznia, nazwę działu i nazwę kierownika działu. Ponieważ ta operacja jest sprzężenia wewnętrznego, zwracane są tylko te obiekty z pierwszego źródła danych, które mają dopasowanie w drugim źródle danych.

// 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}".""");
}

Odpowiednik przy użyciu wielu Join metod używa tego samego podejścia z typem anonimowym:

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}".""");
}

Sprzężenie wewnętrzne przy użyciu sprzężenia grupowanego

W poniższym przykładzie pokazano, jak zaimplementować sprzężenie wewnętrzne przy użyciu sprzężenia grupy. Lista Department obiektów jest przyłączona do listy Student obiektów na Department.ID podstawie pasującej Student.DepartmentID właściwości. Sprzężenia grupy tworzy kolekcję grup pośrednich, gdzie każda grupa składa się z Department obiektu i sekwencji pasujących Student obiektów. Druga from klauzula łączy (lub spłaszcza) tę sekwencję sekwencji w jedną dłuższą sekwencję. Klauzula select określa typ elementów w sekwencji końcowej. Ten typ jest typem anonimowym, który składa się z nazwy ucznia i pasującej nazwy działu.

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

Te same wyniki można osiągnąć przy użyciu GroupJoin metody w następujący sposób:

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

Wynik jest odpowiednikiem zestawu wyników uzyskanego przy użyciu join klauzuli bez into klauzuli w celu wykonania sprzężenia wewnętrznego. Poniższy kod demonstruje to równoważne zapytanie:

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

Aby uniknąć tworzenia łańcuchów, można użyć pojedynczej Join metody, jak pokazano poniżej:

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

Wykonywanie sprzężeń grupowanych

Sprzężenie grupy jest przydatne do tworzenia hierarchicznych struktur danych. Łączy każdy element z pierwszej kolekcji z zestawem skorelowanych elementów z drugiej kolekcji.

Uwaga

Każdy element pierwszej kolekcji jest wyświetlany w zestawie wyników sprzężenia grupy niezależnie od tego, czy skorelowane elementy znajdują się w drugiej kolekcji. W przypadku, gdy nie znaleziono skorelowanych elementów, sekwencja skorelowanych elementów dla tego elementu jest pusta. Selektor wyników ma zatem dostęp do każdego elementu pierwszej kolekcji. Różni się to od selektora wyników w sprzężeniu niegrupowym, który nie może uzyskać dostępu do elementów z pierwszej kolekcji, która nie jest zgodna w drugiej kolekcji.

Ostrzeżenie

Enumerable.GroupJoin nie ma bezpośredniego odpowiednika w tradycyjnych terminach relacyjnej bazy danych. Jednak ta metoda implementuje nadzbiór sprzężeń wewnętrznych i lewe sprzężenia zewnętrzne. Oba te operacje można napisać pod względem sprzężenia zgrupowanego. Aby uzyskać więcej informacji, zobacz Entity Framework Core, GroupJoin.

W pierwszym przykładzie w tym artykule pokazano, jak wykonać sprzężenie grupy. W drugim przykładzie pokazano, jak utworzyć elementy XML za pomocą sprzężenia grupowego.

Dołącz do grupy

Poniższy przykład wykonuje sprzężenie grupy obiektów typu Department i Student na Deoartment.ID podstawie dopasowania Student.DepartmentID właściwości. W przeciwieństwie do sprzężenia niegrupowego, który tworzy parę elementów dla każdego dopasowania, sprzężenia grupy tworzy tylko jeden wynikowy obiekt dla każdego elementu pierwszej kolekcji, który w tym przykładzie jest obiektem Department . Odpowiednie elementy z drugiej kolekcji, które w tym przykładzie są Student obiektami, są pogrupowane w kolekcję. Na koniec funkcja selektora wyników tworzy typ anonimowy dla każdego dopasowania składającego Department.Name się z i kolekcji Student obiektów.

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

W powyższym przykładzie zmienna zawiera zapytanie, które tworzy listę, query gdzie każdy element jest typem anonimowym zawierającym nazwę działu i kolekcję studentów, którzy badają się w tym dziale.

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

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

Dołączanie do grupy w celu utworzenia kodu XML

Sprzężenia grup są idealne do tworzenia kodu XML przy użyciu linQ to XML. Poniższy przykład jest podobny do poprzedniego przykładu, z tą różnicą, że zamiast tworzenia typów anonimowych funkcja selektora wyników tworzy elementy XML reprezentujące sprzężone obiekty.

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

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

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

Wykonywanie lewych sprzężeń zewnętrznych

Lewe sprzężenia zewnętrzne to sprzężenia, w którym zwracany jest każdy element pierwszej kolekcji, niezależnie od tego, czy ma jakiekolwiek skorelowane elementy w drugiej kolekcji. Za pomocą LINQ można wykonać lewe sprzężenie zewnętrzne, wywołując DefaultIfEmpty metodę w wynikach sprzężenia grupy.

W poniższym przykładzie pokazano, jak użyć DefaultIfEmpty metody w wynikach sprzężenia grupy w celu wykonania lewego sprzężenia zewnętrznego.

Pierwszym krokiem tworzenia lewego sprzężenia zewnętrznego dwóch kolekcji jest wykonanie sprzężenia wewnętrznego przy użyciu sprzężenia grupy. (Zobacz Wykonaj sprzężenia wewnętrzne, aby uzyskać wyjaśnienie tego procesu). W tym przykładzie lista Department obiektów jest przyłączona do listy Student obiektów na Department podstawie identyfikatora obiektu zgodnego z identyfikatorem ucznia DepartmentID.

Drugim krokiem jest dołączenie każdego elementu pierwszej kolekcji (po lewej) do zestawu wyników, nawet jeśli ten element nie ma dopasowań w prawej kolekcji. Jest to realizowane przez wywołanie DefaultIfEmpty każdej sekwencji pasujących elementów z sprzężenia grupy. W tym przykładzie DefaultIfEmpty wywoływana jest każda sekwencja pasujących Student obiektów. Metoda zwraca kolekcję zawierającą pojedynczą, domyślną wartość, jeśli sekwencja pasujących Student obiektów jest pusta dla dowolnego Department obiektu, zapewniając, że każdy Department obiekt jest reprezentowany w kolekcji wyników.

Uwaga

Wartość domyślna typu odwołania to null; w związku z tym przykład sprawdza odwołanie o wartości null przed uzyskaniem dostępu do każdego elementu każdej Student kolekcji.

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

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

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

Zobacz też