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.

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.

Two overlapping circles showing inner/outer.

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 StudentDeoartment.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, department) => new { student, subgroup = department.DefaultIfEmpty() })
    .Select(gj => new
    {
        gj.student.FirstName,
        gj.student.LastName,
        Department = gj.subgroup?.FirstOrDefault()?.Name ?? string.Empty
    });

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

Voir aussi