Бөлісу құралы:


Join операции в LINQ

Соединение связывает объекты в одном источнике данных с объектами, которые совместно используют общий атрибут в другом источнике данных.

Внимание

В этих примерах используется System.Collections.Generic.IEnumerable<T> источник данных. Источники данных, основанные на System.Linq.IQueryProvider использовании System.Linq.IQueryable<T> источников данных и деревьев выражений. Деревья выражений имеют ограничения на допустимый синтаксис C#. Кроме того, каждый IQueryProvider источник данных, например EF Core , может наложить больше ограничений. Ознакомьтесь с документацией по источнику данных.

Присоединение является важной операцией в запросах, направленных на источники данных, связи между которыми невозможно отследить напрямую. В объектно-ориентированном программировании присоединение может означать корреляцию между объектами, которые не моделировались, например обратное направление односторонняя связь. Пример односторонняя связь — Student это класс, имеющий свойство типа Department , представляющее основное, но Department класс не имеет свойства, являющегося коллекцией Student объектов. Если у вас есть список Department объектов, и вы хотите найти всех учащихся в каждом отделе, можно использовать операцию присоединения для их поиска.

Платформа LINQ предоставляет методы соединения: Join и GroupJoin. Они выполняют эквисоединения, или соединения, которые сопоставляют два источника данных на основе равенства их ключей. Для сравнения, 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
LeftJoin Сопоставляет элементы двух последовательностей на основе соответствующих ключей. N/A Enumerable.LeftJoin

Queryable.LeftJoin
RightJoin Сопоставляет элементы двух последовательностей на основе соответствующих ключей. N/A Enumerable.RightJoin

Queryable.RightJoin

Примечание.

В следующих примерах в этой статье используются общие источники данных для этой области.
Каждый из них 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, вызываемый условием join в языке C#, реализует внутреннее соединение. В следующих примерах показано, как выполнить четыре варианта внутреннего соединения:

  • Простое внутреннее соединение, которое сопоставляет элементы из двух источников данных на основе простого ключа.
  • Внутреннее соединение, которое сопоставляет элементы из двух источников данных на основе составного ключа. Составной ключ — это ключ, который состоит из нескольких значений, позволяющих сопоставлять элементы на основе сразу нескольких свойств.
  • Множественное объединение, в котором вы последовательно добавляете операции объединения друг к другу.
  • Внутреннее соединение, использующее групповой джойн.

Соединение с одним ключом

В следующем примере объекты совпадают Teacher с Department объектами, которые TeacherId совпадают с этим Teacher. Предложение select в C# определяет, как выглядят полученные объекты. В следующем примере результирующий объект — это анонимные типы, состоящие из имени отдела и имени преподавателя, который ведет отдел.

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

Несколько объединений

Можно добавить любое количество операций соединения для выполнения нескольких соединений. Каждое join предложение в C# сопоставляет указанный источник данных с результатами предыдущего соединения.

Первое join предложение соответствует учащимся и отделам на Student основе сопоставления DepartmentID объекта 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 объектов на 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-элементов.

Присоединение к группе

В следующем примере выполняется групповое объединение объектов типа Department и Student на основе сопоставления свойства Department.ID с Student.DepartmentID. В отличие от негруппового соединения, которое создает пару элементов для каждого совпадения, групповое соединение создает только один результирующий объект для каждого элемента первой коллекции. В этом примере первая коллекция является Department объектом. Соответствующие элементы из второй коллекции (в данном примере — объекты Student) группируются в коллекцию. Наконец, результирующая функция селектора создает для каждого совпадения анонимный тип, состоящий из Department.Name и коллекции объектов Student.

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

Групповые соединения идеально подходят для создания XML с помощью LINQ to 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);

Выполнение внешних соединений

.NET 10 включает LeftJoin и RightJoin методы в System.Linq.Enumerable и System.Linq.Queryable классы. Эти методы выполняют внешнее левое равное соединение и внешнее правое равное соединение соответственно. Внешнее левостороннее равноправное соединение — это соединение, в котором каждый член первой последовательности включён в результирующую последовательность, даже если во второй последовательности отсутствует совпадение. Внешнее правое эквисоединение — это соединение, в котором каждый член второй последовательности включается в выходную последовательность, даже если первая последовательность не включает совпадение.

Эмуляция левого внешнего соединения

Перед выпуском .NET 10, используйте LINQ для выполнения левостороннего внешнего соединения, вызывая метод DefaultIfEmpty для результатов группового соединения.

В следующем примере показано, как использовать DefaultIfEmpty метод в результатах соединения группы для выполнения левого внешнего соединения.

Первым шагом в создании левого внешнего соединения двух коллекций является выполнение внутреннего соединения, используя групповое объединение. (См. раздел Выполните внутренние соединения для объяснения этого процесса.) В этом примере список Department объектов присоединяется к списку Student объектов на Department основе идентификатора объекта, соответствующего учащемуся DepartmentID.

Вторым шагом является включение каждого элемента первой (левой) коллекции в набор результатов, даже если элемент не имеет совпадений в правой коллекции. Для этого необходимо вызвать DefaultIfEmpty для каждой из последовательностей элементов, соответствующих группового соединения. В этом примере вы вызываете DefaultIfEmpty на каждой последовательности соответствующих Student объектов. Метод возвращает коллекцию, содержащую одно значение по умолчанию, если последовательность соответствующих Student объектов пуста для любого Department объекта, гарантируя, что каждый Department объект представлен в коллекции результатов.

Примечание.

Значение по умолчанию для ссылочного типа — 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 ?? string.Empty
        });

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

См. также