Join Opérations dans LINQ
Une jointure de deux sources de données est l’association des objets d’une source de données aux objets qui partagent un attribut commun dans une autre source de données.
Important
Ces exemples utilisent une source de données System.Collections.Generic.IEnumerable<T>. Les sources de données basées sur System.Linq.IQueryProvider utilisent des sources de données System.Linq.IQueryable<T> et des arborescences d’expressions. Les arborescences d’expressions présentent des limitations sur la syntaxe C# autorisée. De plus, chaque source de données IQueryProvider
, telle que EF Core peut imposer des restrictions supplémentaires. Consultez la documentation de votre source de données.
Les opérations Join sont des opérations importantes dans les requêtes qui ciblent des sources de données dont les relations entre elles ne peuvent pas être suivies directement. En programmation orientée objet, une jointure peut signifier une corrélation entre objets qui n’est pas modélisée, par exemple le sens inverse d’une relation unidirectionnelle. Voici un exemple de relation unidirectionnelle : une classe Student
a une propriété de type Department
qui représente l’élément majeur, mais la classe Department
n’a pas de propriété correspondant à une collection d’objets Student
. Si vous avez une liste d’objets Department
et si vous voulez rechercher tous les étudiants de chaque département, vous pouvez utiliser une opération de jointure pour les trouver.
Les méthodes de jointure fournies dans le framework LINQ sont Join et GroupJoin. Ces méthodes effectuent des équijointures, qui sont des jointures associant deux sources de données en fonction de l’égalité de leurs clés. (Par comparaison, Transact-SQL prend en charge des opérateurs de jointure autres que equals
, par exemple l’opérateur less than
.) Dans le contexte des bases de données relationnelles, Join implémente une jointure interne, qui est un type de jointure dans lequel seuls sont retournés les objets qui ont une correspondance dans l’autre jeu de données. La méthode GroupJoin n’a aucun équivalent direct dans le contexte des bases de données relationnelles, mais elle implémente un sur-ensemble de jointures internes et de jointures externes gauches. Une jointure externe gauche est une jointure qui retourne chaque élément de la source de données (gauche), même si elle n’a pas d’éléments corrélés dans l’autre source de données.
L'illustration suivante présente une vue conceptuelle de deux ensembles, ainsi que leurs éléments inclus dans une jointure interne ou une jointure externe gauche.
Méthodes
Nom de la méthode | Description | Syntaxe d'expression de requête C# | Informations complémentaires |
---|---|---|---|
Join | Joins deux séquences selon les fonctions de sélecteur de clés et extrait des paires de valeurs. | join … in … on … equals … |
Enumerable.Join Queryable.Join |
GroupJoin | Joins deux séquences selon les fonctions de sélecteur de clés et regroupe les correspondances obtenues pour chaque élément. | join … in … on … equals … into … |
Enumerable.GroupJoin Queryable.GroupJoin |
Les exemples suivants de cet article utilisent les sources de données courantes pour ce domaine :
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; }
}
Chaque Student
a un niveau scolaire, un département principal et une série de notes. Un Teacher
a également une propriété City
qui identifie le campus où l’enseignant donne des cours. Un Department
a un nom, et une référence à un Teacher
qui est responsable du département.
L’exemple suivant utilise la clause join … in … on … equals …
pour joindre deux séquences basées sur une valeur spécifique :
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}");
}
La requête précédente peut être exprimée en utilisant la syntaxe de méthode, comme illustré dans le code suivant :
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}");
}
L’exemple suivant utilise la clause join … in … on … equals … into …
pour joindre deux séquences basées sur une valeur spécifique et regroupe les correspondances obtenues pour chaque élément :
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}");
}
}
La requête précédente peut être exprimée en utilisant la syntaxe de méthode, comme illustré dans le code suivant :
// 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}");
}
}
Effectuer des jointures internes
Dans le domaine des bases de données relationnelles, une jointure interne produit un jeu de résultats dans lequel chaque élément de la première collection apparaît une fois pour chaque élément correspondant dans la deuxième collection. Si un élément de la première collection n’a pas d’éléments correspondants, il n’apparaît pas dans le jeu de résultats. La méthode Join, qui est appelée par la clause join
en C#, implémente une jointure interne. Les exemples suivants vous montrent comment effectuer quatre variantes d’une jointure interne :
- Une jointure interne simple qui met en corrélation des éléments de deux sources de données sur la base d’une clé simple.
- Une jointure interne qui met en corrélation des éléments de deux sources de données sur la base d’une clé composite. Une clé composite, qui est une clé composée de plusieurs valeurs, permet de mettre en corrélation des éléments sur la base de plusieurs propriétés.
- Une jointure multiple dans laquelle les opérations de jointure consécutives sont ajoutées les unes aux autres.
- Une jointure interne qui est implémentée à l’aide d’une jointure groupée.
Jointure de clé unique
L’exemple suivant trouve les objets Teacher
avec des objets Deparment
dont TeacherId
correspond à Teacher
. La clause select
dans C# définit l’apparence des objets résultants. Dans l’exemple suivant, les objets résultants sont des types anonymes qui se composent du nom du département et du nom de l’enseignant qui dirige le département.
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}");
}
Vous obtenez les mêmes résultats à l’aide de la syntaxe de méthode 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}");
}
Les enseignants qui ne sont pas chefs de département n’apparaissent pas dans les résultats finaux.
Jointure de clés composites
Au lieu de mettre en corrélation des éléments sur la base d’une seule propriété, vous pouvez utiliser une clé composite pour comparer des éléments en fonction de plusieurs propriétés. Spécifiez la fonction de sélecteur de clé pour chaque collection pour retourner un type anonyme qui se compose des propriétés que vous voulez comparer. Si vous étiquetez les propriétés, elles doivent avoir la même étiquette dans le type anonyme de chaque clé. Les propriétés doivent également apparaître dans le même ordre.
L’exemple suivant utilise une liste d’objets Teacher
et une liste d’objets Student
pour déterminer quels enseignants sont également étudiants. Ces deux types ont des propriétés qui représentent le prénom et le nom de famille de chaque personne. Les fonctions qui créent les clés de jointure à partir des éléments de chaque liste retournent un type anonyme qui se compose des propriétés. L’opération de jointure effectue une comparaison d’égalité de ces clés composites et retourne les paires d’objets de chaque liste où il y a correspondance entre le prénom et le nom de famille.
// 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);
Vous pouvez utiliser la méthode Join, comme illustré dans l’exemple suivant :
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);
}
Jointure multiple
Vous pouvez effectuer une jointure multiple en ajoutant n’importe quel nombre d’opérations de jointure les unes aux autres. Chaque clause join
dans C# met en corrélation une source de données spécifiée avec les résultats de la jointure précédente.
La première clause join
trouve les étudiants et les départements en fonction de la correspondance du DepartmentID
d’un objet Student
avec l’ID
d’un objet Department
. Elle retourne une séquence de types anonymes qui contiennent l’objet Student
et l’objet Department
.
La deuxième clause join
met en corrélation les types anonymes retournés par la première jointure avec des objets Teacher
où l’ID de l’enseignant correspond à l’ID du chef du département. Elle retourne une séquence de types anonymes qui contiennent le nom de l’étudiant, le nom du département et le nom du chef du département. Comme cette opération est une jointure interne, seuls les objets de la première source de données qui ont une correspondance dans la seconde source de données sont retournés.
// 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}".""");
}
L’équivalent utilisant plusieurs méthodes Join utilise la même approche avec le type anonyme :
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}".""");
}
Jointure interne en utilisant une jointure groupée
L’exemple suivant montre comment implémenter une jointure interne en utilisant une jointure groupée. La liste d’objets Department
est jointe par groupe à la liste d’objets Student
sur la base du Department.ID
correspondant à la propriété Student.DepartmentID
. La jointure groupée crée une collection de groupes intermédiaires où chaque groupe se compose d’un objet Department
et d’une séquence d’objets Student
correspondants. La deuxième clause from
combine (ou aplatit) cette séquence de séquences en une séquence plus longue. La clause select
spécifie le type d’éléments dans la séquence finale. Ce type est un type anonyme qui se compose du nom de l’étudiant et du nom du département correspondant.
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}");
}
Les mêmes résultats peuvent être obtenus à l’aide de GroupJoin la méthode suivante :
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}");
}
Le résultat est équivalent au jeu de résultats obtenu en utilisant la clause join
sans la clause into
pour effectuer une jointure interne. Le code suivant montre cette requête équivalente :
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}");
}
Pour éviter le chaînage, la méthode unique Join peut être utilisée comme indiqué ici :
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}");
}
Effectuer des jointures groupées
La jointure groupée est utile pour produire des structures de données hiérarchiques. Elle associe chaque élément de la première collection à un jeu d’éléments corrélés de la deuxième collection.
Remarque
Chaque élément de la première collection apparaît dans le jeu de résultats d’une jointure groupée, même si des éléments corrélés sont trouvés dans la deuxième collection. Si aucun élément corrélé n’est trouvé, la séquence d’éléments corrélés pour cet élément est vide. Le sélecteur de résultats a donc accès à chaque élément de la première collection. Cela n’est pas le cas du sélecteur de résultats dans une jointure non groupée, qui ne peut pas accéder à des éléments de la première collection qui n’ont aucune correspondance dans la deuxième collection.
Avertissement
Enumerable.GroupJoin n’a pas d’équivalent direct dans les termes de base de données relationnelle traditionnels. Toutefois, cette méthode implémente un sur-ensemble de jointures internes et de jointures externes gauches. Ces deux opérations peuvent être écrites en termes de jointure groupée. Pour plus d’informations, consultez Entity Framework Core, GroupJoin.
Le premier exemple de cet article montre comment effectuer une jointure groupée. Le deuxième exemple montre comment utiliser une jointure groupée pour créer des éléments XML.
Jointure groupée
L’exemple suivant effectue une jointure groupée d’objets de types Department
et Student
où Department.ID
correspond à la propriété Student.DepartmentID
. Contrairement à une jointure non groupée, qui produit une paire d’éléments pour chaque correspondance, la jointure groupée produit un seul objet résultant pour chaque élément de la première collection, qui dans cet exemple est un objet Department
. Les éléments correspondants de la deuxième collection, qui sont des objets Student
dans cet exemple, sont regroupés dans une collection. Enfin, le sélecteur de résultats crée un type anonyme pour chaque correspondance qui se compose de Department.Name
et d’une collection d’objets 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}");
}
}
Dans l’exemple ci-dessus, la variable query
contient la requête qui crée une liste où chaque élément est un type anonyme qui contient le nom du département et une collection d’étudiants qui y étudient.
La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :
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}");
}
}
Jointure groupée pour créer du XML
Les jointures groupées sont appropriées pour créer des éléments XML à l’aide de LINQ to XML. L’exemple suivant est similaire à l’exemple précédent, sauf qu’au lieu de créer des types anonymes, le sélecteur de résultats crée des éléments XML qui représentent les objets joints.
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);
La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :
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);
Effectuer des jointures externes gauches
Une jointure externe gauche est une jointure dans laquelle chaque élément de la première collection est retourné, qu’elle ait ou non des éléments corrélés dans la deuxième collection. Vous pouvez utiliser LINQ pour effectuer une jointure externe gauche en appelant la méthode DefaultIfEmpty sur les résultats d’une jointure groupée.
L’exemple suivant montre comment utiliser la méthode DefaultIfEmpty sur les résultats d’une jointure groupée pour effectuer une jointure externe gauche.
Pour créer une jointure externe gauche entre deux collections, la première étape consiste à effectuer une jointure interne à l’aide d’une jointure groupée. (Pour savoir comment faire, consultez Effectuer des jointures internes.) Dans cet exemple, la liste d’objets Department
se voit appliquer une jointure interne avec la liste d’objets Student
sur la base de la correspondance entre l’ID d’un objet Department
et le DepartmentID
d’un étudiant.
La deuxième étape consiste à inclure tous les éléments de la première collection (celle de gauche) dans le jeu de résultats, y compris les éléments sans correspondance dans la collection de droite. Pour cela, appelez DefaultIfEmpty sur chaque séquence d’éléments correspondants de la jointure groupée. Dans cet exemple, la méthode DefaultIfEmpty est appelée sur chaque séquence d’objets Student
correspondants. La méthode retourne une collection qui contient une seule valeur par défaut si la séquence des objets Student
correspondant à un objet Department
est vide, ce qui garantit que chaque objet Department
est représenté dans la collection de résultats.
Remarque
La valeur par défaut pour un type référence est null
. L’exemple recherche donc une référence null avant d’accéder à chaque élément de chaque collection 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}");
}
La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :
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}");
}