Join Operacje w LINQ
Jednym ze źródeł danych jest join skojarzenie obiektów w jednym źródle danych z obiektami, które współużytkuje wspólny atrybut w innym źródle danych.
Ważne
Te przykłady używają System.Collections.Generic.IEnumerable<T> źródła danych. Źródła danych oparte na System.Linq.IQueryProvider System.Linq.IQueryable<T> źródłach danych i drzewach wyrażeń. Drzewa wyrażeń mają ograniczenia dotyczące dozwolonej składni języka C#. Ponadto każde IQueryProvider
źródło danych, takie jak EF Core , może nakładać więcej ograniczeń. Zapoznaj się z dokumentacją źródła danych.
Łączenie jest ważną operacją 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ć join operacji , aby je znaleźć.
Metody join podane 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 join operatory inne niż equals
, na przykład less than
operator). W terminach relacyjnej bazy danych implementuje wewnętrzny jointypjoin, Join 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. Lewa zewnętrzna join jest elementem join , który zwraca każdy element pierwszego (po lewej) źródle danych, nawet jeśli nie ma skorelowanych elementów w innym źródle danych.
Na poniższej ilustracji przedstawiono koncepcyjny widok dwóch zestawów i elementy w tych zestawach, które znajdują się w wewnętrznym join lub lewym zewnętrznym .join
Metody
Nazwa metody | opis | Składnia wyrażeń zapytań języka C# | Więcej informacji |
---|---|---|---|
Join | Łączy dwie sekwencje na podstawie funkcji selektora kluczy i wyodrębnia pary wartości. | join … in … on … equals … |
Enumerable.Join Queryable.Join |
GroupJoin | Łączy dwie sekwencje na podstawie funkcji selektora kluczy i grupuje wynikowe dopasowania dla każdego elementu. | join … in … on … equals … into … |
Enumerable.GroupJoin Queryable.GroupJoin |
Uwaga
W poniższych przykładach w tym artykule użyto typowych źródeł danych dla tego obszaru.
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.
Przykładowy zestaw danych można znaleźć w repozytorium źródłowym.
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; }
}
W poniższym przykładzie użyto klauzuli join … in … on … equals …
do join dwóch sekwencji 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 …
do join dwóch sekwencji na podstawie określonej wartości i grupuje 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 wewnętrzny join 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 wewnętrzny joinelement . W poniższych przykładach pokazano, jak wykonać cztery odmiany wewnętrznego joinelementu :
- Prosty wewnętrzny join , który koreluje elementy z dwóch źródeł danych na podstawie prostego klucza.
- Wewnętrznyjoin, który 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 join operacji, w których kolejne join operacje są dołączane do siebie nawzajem.
- Wewnętrzny join , który jest implementowany przy użyciu grupy join.
Pojedynczy klucz join
Poniższy przykład pasuje do Teacher
obiektów z obiektami Department
, których TeacherId
pasuje do tego Teacher
obiektu . 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.
Klucz złożony join
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ą join klucze z elementów każdej listy, zwracają typ anonimowy, który składa się z właściwości. Operacja join 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);
}
Wielokrotny join
Dowolną liczbę join operacji można dołączać do siebie, aby wykonać wiele joinoperacji. Każda klauzula join
w języku C# koreluje określone źródło danych z wynikami poprzedniego joinelementu .
join
Pierwsza klauzula pasuje do uczniów i działów w oparciu o Student
obiekt pasujący Department
do obiektu DepartmentID
ID
. Zwraca sekwencję typów anonimowych, które zawierają Student
obiekt i Department
obiekt.
Druga join
klauzula koreluje typy anonimowe zwracane przez pierwszy join z Teacher
obiektami 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 wewnętrzną joinoperacją, 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}".""");
}
Wewnętrzny join przy użyciu grupowanych join
W poniższym przykładzie pokazano, jak zaimplementować wewnętrzny join element przy użyciu grupy join. Lista Department
obiektów jest przyłączona do listy Student
obiektów na Department.ID
podstawie pasującej Student.DepartmentID
właściwości. join Grupa 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 wewnętrznego join. 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
Grupa join jest przydatna 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 grupy join 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 grupie innej niż grupa join, która nie może uzyskać dostępu do elementów z pierwszej kolekcji, które nie mają dopasowania 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 zgrupowanego joinobiektu . Aby uzyskać więcej informacji, zobacz Entity Framework Core, GroupJoin.
W pierwszym przykładzie w tym artykule pokazano, jak wykonać grupę join. W drugim przykładzie pokazano, jak utworzyć elementy XML za pomocą grupy join .
Grupa join
Poniższy przykład wykonuje grupę join obiektów typu Department
i Student
na Department.ID
podstawie pasującej Student.DepartmentID
właściwości. W przeciwieństwie do grupy innej niż grupa join, która tworzy parę elementów dla każdego dopasowania, grupa join 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}");
}
}
Grupowanie join 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
Lewa zewnętrzna join jest elementem join , w którym zwracany jest każdy element pierwszej kolekcji, niezależnie od tego, czy ma jakiekolwiek skorelowane elementy w drugiej kolekcji. LinQ można użyć do wykonania lewej zewnętrznej, join wywołując DefaultIfEmpty metodę w wynikach grupy join.
W poniższym przykładzie pokazano, jak użyć DefaultIfEmpty metody w wynikach grupy join do wykonania lewej zewnętrznej join.
Pierwszym krokiem tworzenia lewej zewnętrznej join dwóch kolekcji jest wykonanie wewnętrznego join przy użyciu grupy join. (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 grupy join. 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, 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}");
}