Join Opérations dans LINQ

Une join 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.

La jointure est une opération importante dans les requêtes qui ciblent les sources de données dont les relations 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 souhaitez rechercher tous les étudiants de chaque département, vous pouvez recourir à une opération de join.

Les méthodes de join 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. (À titre de comparaison, Transact-SQL prend en charge des opérateurs join autres que equals, par exemple, l’opérateur less than.) En termes de base de données relationnelle, Join implémente un join interne, un type de join dans lequel seuls les objets qui ont une correspondance dans l’autre jeu de données sont renvoyés. 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 join externe gauche est une join 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 join interne ou une join externe gauche.

Deux cercles se chevauchant montrant l’interne/externe.

Méthodes

Nom de la méthode Description Syntaxe d'expression de requête C# Informations supplémentaires
Join Joint deux séquences selon les fonctions de sélection de clé et extrait des paires de valeurs. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Joint deux séquences selon les fonctions de sélection de clé et regroupe les résultats correspondants pour chaque élément. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Remarque

Les exemples suivants de cet article utilisent les sources de données courantes pour cette zone.
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.
Vous trouverez l’exemple de jeu de données dans le référentiel source.

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; }
}

L’exemple suivant utilise la clause join … in … on … equals … pour join 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 join 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 join 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 join interne. Les exemples suivants vous montrent comment effectuer quatre variations d’une join interne :

  • Une join interne simple qui met en corrélation des éléments de deux sources de données sur la base d’une clé simple.
  • Une join 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 join multiple dans laquelle les opérations de join consécutives sont ajoutées les unes aux autres.
  • Une jointure join qui est implémentée à l’aide d’une join groupée.

join à 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.

join à clé composite

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 join à partir des éléments de chaque liste retournent un type anonyme qui se compose des propriétés. L’opération de join 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.

// 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);
}

join multiple

Vous pouvez effectuer une join multiple en ajoutant n’importe quel nombre d’opérations de join 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 join 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 renvoyés par la première join avec des objets Teacher basés sur l’ID de cet enseignant correspondant à l’ID du chef de 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 il s’agit d’une join interne, seuls les objets de la première source de données qui ont une correspondance dans la deuxième 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}".""");
}

join interne à l’aide d’une join regroupée

L’exemple suivant montre comment implémenter une join interne en utilisant une join 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 join 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 qui aurait été obtenu en utilisant la clause join sans la clause into pour effectuer une join 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 join 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 join 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 join 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 join groupée. Pour plus d’informations, consultez Entity Framework Core, GroupJoin.

Le premier exemple de cet article montre comment effectuer une join groupée. Le deuxième exemple montre comment utiliser une join groupée pour créer des éléments XML.

join groupée

L’exemple suivant effectue une join groupée d’objets de types Department et StudentDepartment.ID correspond à la propriété Student.DepartmentID. Contrairement à une join non groupée qui produit une paire d’éléments pour chaque correspondance, la join groupée produit un seul objet résultant pour chaque élément de la première collection, qui est un objet Department dans cet exemple. 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}");
    }
}

join groupée pour créer des éléments 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 join externe gauche est une join 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 join externe gauche en appelant la méthode DefaultIfEmpty sur les résultats d’une join groupée.

L’exemple suivant montre comment utiliser la méthode DefaultIfEmpty sur les résultats d’une join groupée pour effectuer une join externe gauche.

Pour créer une join externe gauche entre deux collections, la première étape consiste à effectuer une join interne à l’aide d’une join 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 join 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}");
}

Voir aussi