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.
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.
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.
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 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.
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 DepartmentID
ID
. 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 Department.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, 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}");
}