Join Операции в LINQ
Соединение двух источников данных — это связь объектов в одном источнике данных с объектами, которые имеют общий атрибут в другом источнике данных.
Внимание
В этих примерах используется System.Collections.Generic.IEnumerable<T> источник данных. Источники данных, основанные на System.Linq.IQueryProvider использовании System.Linq.IQueryable<T> источников данных и деревьев выражений. Деревья выражений имеют ограничения на допустимый синтаксис C#. Кроме того, каждый IQueryProvider
источник данных, например EF Core , может наложить больше ограничений. Ознакомьтесь с документацией по источнику данных.
Joining — это важная операция в запросах, предназначенных для источников данных, связи которых друг с другом не могут выполняться напрямую. В объектно-ориентированном программировании присоединение может означать корреляцию между объектами, которые не моделировались, например обратное направление односторонняя связь. Пример односторонняя связь — Student
это класс, имеющий свойство типа Department
, представляющее основное, но Department
класс не имеет свойства, являющегося коллекцией Student
объектов. Если у вас есть список Department
объектов, и вы хотите найти всех учащихся в каждом отделе, можно использовать операцию присоединения для их поиска.
На платформе LINQ представлены методы объединения Join и GroupJoin. Они выполняют эквисоединения, или соединения, которые сопоставляют два источника данных на основе равенства их ключей. (Для сравнения Transact-SQL поддерживает операторы соединения, отличные equals
less than
от оператора. В терминах Join реляционной базы данных реализует внутреннее соединение, тип соединения, в котором возвращаются только те объекты, которые имеют совпадение в другом наборе данных. Метод GroupJoin не имеет прямого эквивалента в терминах реляционных баз данных, но реализует надмножество внутренних соединений и левых внешних соединений. Левое внешнее соединение — это соединение, которое возвращает каждый элемент первого (левого) источника данных, даже если в другом источнике данных не имеется соответствующих элементов.
На следующем рисунке показано концептуальное представление из двух наборов и элементов, входящих в эти наборы, которые включены либо во внутреннее соединение, либо в левое внешнее соединение.
Методы
Имя метода | Description | Синтаксис выражения запроса 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}");
}
}
Выполнение внутренних соединений
В терминах реляционных баз данных внутреннее соединение формирует результирующий набор, в котором каждый элемент первой коллекции отображается по одному разу для каждого соответствующего элемента во второй коллекции. Если элемент в первой коллекции не имеет соответствующих элементов, он не отображается в результирующем наборе. Метод Join, который вызывается предложением join
в C#, реализует внутреннее соединение. В следующих примерах показано, как выполнить четыре варианта внутреннего соединения:
- Простое внутреннее соединение, сопоставляющее элементы из двух источников данных на основе простого ключа.
- Внутреннее соединение, сопоставляющее элементы из двух источников данных на основе составного ключа. Составной ключ — это ключ, который состоит из нескольких значений, позволяющих сопоставлять элементы на основе сразу нескольких свойств.
- Множественное соединение, при котором несколько последовательных операций соединения добавляются друг к другу.
- Внутреннее соединение, реализуемое с помощью группового соединения.
Соединение с одним ключом
В следующем примере объекты совпадают Teacher
с Deparment
объектами, которые 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
основе сопоставления Department
объекта DepartmentID
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-элементов.
групповое соединение,
В приведенном ниже примере выполняется групповое соединение объектов типа 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);
Выполнение левых внешних соединений
Левое внешнее соединение — это соединение, при котором каждый элемент первой коллекции возвращается независимо от наличия взаимосвязанных элементов во второй коллекции. 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.AsQueryable() })
.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}");
}