Join-Vorgänge in LINQ

Eine Verknüpfung zweier Datenquellen entspricht der Zuordnung von Objekten einer Datenquelle zu den Objekten einer anderen Datenquelle, die ein Attribut gemeinsam haben.

Der Join-Vorgang ist für Abfragen sehr wichtig. Er ist auf Datenquellen ausgerichtet, deren Beziehungen zueinander nicht direkt verfolgt werden können. Bei der objektorientierten Programmierung könnte ein Join eine nicht modellierte Korrelation zwischen Objekten bedeuten, z. B. die entgegengesetzte Richtung einer unidirektionalen Beziehung. Ein Beispiel einer unidirektionalen Beziehung ist die Student-Klasse mit einer Eigenschaft vom Typ Department, der die leitende Lehrkraft bezeichnet, während die Department-Klasse keine Eigenschaft besitzt, die einer Auflistung von Student-Objekten entspricht. Wenn Sie eine Liste von Department-Objekten besitzen und alle Lernenden in den einzelnen Fachbereichen ermitteln möchten, könnten Sie dafür einen Join-Vorgang verwenden.

Die im LINQ-Framework bereitgestellten Join-Methoden sind Join und GroupJoin. Diese Methoden führen Gleichheitsverknüpfungen oder Verknüpfungen durch, bei denen zwei Datenquellen auf Basis der Gleichheit ihrer Schlüssel verglichen werden. (Zum Vergleich: Transact-SQL unterstützt auch andere Join-Operatoren als equals, z. B. den less than-Operator.) Für relationale Datenbanken bedeutet dies, dass Join eine innere Verknüpfung implementiert, d.h. eine Art von Verknüpfung, bei der nur die Objekte zurückgegeben werden, die über eine Entsprechung im anderen Datensatz verfügen. Für die GroupJoin-Methode hat bei relationalen Datenbanken kein direktes Äquivalent. Sie implementieren eine übergeordnete Menge innerer und linker äußerer Joins. Ein Left Outer Join ist eine Verknüpfung, die jedes Element der ersten (linken) Datenquelle zurückgibt, selbst wenn die andere Datenquelle keine zugehörigen Elemente enthält.

Die folgende Abbildung zeigt eine Konzeptansicht zweier Sätze sowie der Elemente innerhalb dieser Sätze, die entweder in eine innere oder linke äußere Verknüpfung einbezogen sind.

Zwei überlappende Kreise innen/außen.

Methoden

Methodenname Beschreibung C#-Abfrageausdruckssyntax Weitere Informationen
Join Die Join-Methode verknüpft zwei Sequenzen auf Basis von Schlüsselauswahlfunktionen und extrahiert Wertepaare. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Die Methode verknüpft (Join) zwei Sequenzen auf Basis von Schlüsselauswahlfunktionen und gruppiert die daraus resultierenden Übereinstimmungen für die einzelnen Elemente. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

In den folgenden Beispielen in diesem Artikel werden die allgemeinen Datenquellen für diesen Bereich verwendet:

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

Allen Student sind eine Klassenstufe, ein primärer Fachbereich und mehrere Bewertungen zugeordnet. Teacher verfügen auch über eine City-Eigenschaft, die den Campus identifiziert, auf dem die Lehrkraft unterrichtet. Eine Department hat einen Namen und einen Verweis auf eine Teacher, die den Fachbereich leitet.

Im folgenden Beispiel wird die join … in … on … equals …-Klausel verwendet, um zwei Sequenzen auf der Grundlage eines bestimmten Werts zu verknüpfen:

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

Obige Abfrage kann mit Methodensyntax ausgedrückt werden, wie im folgenden Code dargestellt:

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

Im folgenden Beispiel wird die join … in … on … equals … into …-Klausel verwendet, um zwei Sequenzen auf der Grundlage eines bestimmten Werts zu verknüpfen und die resultierenden Übereinstimmungen für jedes Element zu gruppieren:

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

Obige Abfrage kann mit Methodensyntax ausgedrückt werden, wie im folgenden Beispiel dargestellt:

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

Ausführen von inneren Verknüpfungen

Bei relationalen Datenbanken erzeugt eine innere Verknüpfung einen Ergebnissatz, in dem jedes Element der ersten Aufzählung einmal für jedes übereinstimmende Element in der zweiten Auflistung erscheint. Wenn ein Element in der ersten Auflistung keine übereinstimmenden Elemente besitzt, ist es nicht im Resultset enthalten. Die Methode Join, die durch die join-Klausel in C# aufgerufen wird, implementiert eine innere Verknüpfung. Die folgenden Beispiele zeigen, wie Sie vier Variationen eines inneren Joins ausführen:

  • Eine einfache innere Verknüpfung, die Elemente aus zwei Datenquellen anhand eines einfachen Schlüssels verknüpft.
  • Eine innere Verknüpfung, die Elemente aus zwei Datenquellen anhand eines zusammengesetzten Schlüssels verknüpft. Mit einem zusammengesetzten Schlüssel, der aus mehr als einem Wert besteht, können Sie Elemente anhand mehr als einer Eigenschaft verknüpfen.
  • Eine Mehrfachverknüpfung, in der aufeinanderfolgende Verknüpfungsvorgänge aneinander gehängt werden.
  • Eine innere Verknüpfung, die mithilfe einer Gruppenverknüpfung implementiert wird.

Join mit einem Schlüssel

Das folgende Beispiel gleicht Teacher-Objekte mit Deparment-Objekten ab, deren TeacherId mit Teacher übereinstimmt. Die select-Klausel in C# definiert, wie die resultierenden Objekte aussehen. Im folgenden Beispiel sind die resultierenden Objekte anonyme Typen, die aus dem Fachbereichsnamen und dem Namen der Lehrkraft bestehen, die den Fachbereich leitet.

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

Sie erzielen die gleichen Ergebnisse mit der Join Methodensyntax:

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

Die Lehrkräfte, die keinen Fachbereich leiten, sind nicht in den Endergebnissen enthalten.

Join mit zusammengesetztem Schlüssel

Anstatt Elemente anhand nur einer Eigenschaft zu verknüpfen, können Sie einen zusammengesetzten Schlüssel verwenden, um Elemente anhand mehreren Eigenschaften zu vergleichen. Geben Sie die Schlüsselauswahlfunktion für jede Auflistung an, um einen anonymen Typ zurückgegeben, der aus den zu vergleichenden Eigenschaften besteht. Wenn Sie die Eigenschaften beschriften, müssen sie über die gleiche Bezeichnung in jedem anonymen Typ des Schlüssels verfügen. Die Eigenschaften müssen auch in der gleichen Reihenfolge angezeigt werden.

Im folgenden Beispiel wird eine Liste von Teacher-Objekten und eine Liste von Student-Objekten verwendet, um zu bestimmen, welche Lehrkräfte auch Lernende sind. Beide Typen weisen Eigenschaften auf, die Vor- und Nachname jeder Person darstellen. Die Funktion, die die Join-Schlüssel aus den Elementen jeder Liste erstellt, gibt einen anonymen Typ zurück, der aus den Eigenschaften besteht. Der Join-Vorgang vergleicht diese zusammengesetzten Schlüssel auf Gleichheit und gibt Objektpaare aus jeder Liste zurück, bei denen Vor- und Nachname übereinstimmen.

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

Verwenden Sie die Join Methode wie im folgenden Beispiel gezeigt:

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

Mehrere Join-Vorgänge

Eine beliebige Anzahl von Verknüpfungsvorgängen kann aneinander gehängt werden, um eine Mehrfachverknüpfung auszuführen. Jede join-Klausel in C# verknüpft eine angegebene Datenquelle mit den Ergebnissen der vorherigen Verknüpfung.

Die erste join-Klausel gleicht Lernende und Fachbereiche ab, bei denen die DepartmentID des Student-Objekt mit der ID eines Department-Objekts übereinstimmt. Sie gibt eine Sequenz von anonymen Typen zurück, die das Student-Objekt und das Department-Objekt enthält.

Die zweite join-Klausel korreliert die anonymen Typen, die vom ersten Join mit Teacher-Objekten zurückgegeben werden, basierend auf der ID von Lehrkräften ab, die der ID der Fachbereichsleitung entsprechen. Sie gibt eine Sequenz anonymer Typen zurück, die die Namen der Lernenden, des Fachbereichs und der Fachbereichsleitung enthalten. Da es sich um einen inneren Join-Vorgang handelt, werden nur die Elemente aus der ersten Datenquelle zurückgegeben, die eine Übereinstimmung in der zweiten Datenquelle haben.

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

Das Äquivalent mit mehreren Join-Methoden verwendet den gleichen Ansatz mit dem anonymen Typ:

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}".""");
}

Innerer Join mithilfe eines gruppierten Joins

In den folgenden Beispielen wird Ihnen gezeigt, wie eine innere Verknüpfung mithilfe einer Gruppenverknüpfung implementiert wird. Die Liste von Department-Objekten wird über einen Gruppen-Join mit der Liste von Student-Objekten verknüpft. Die Grundlage bildet ein Abgleich der Department.ID mit der Student.DepartmentID-Eigenschaft. Die Gruppeverknüpfung erstellt eine Auflistung von Zwischengruppen, bei der jede Gruppe aus einem Department-Objekt und einer Sequenz von übereinstimmenden Student-Objekten besteht. Die zweite from-Klausel kombiniert (oder vereinfacht) diese Sequenz von Sequenzen in einer längeren Sequenz. Die select-Klausel gibt den Typ der Elemente in der endgültigen Sequenz an. Dieser Typ ist ein anonymer Typ, der aus dem Namen der Lernenden und dem zugehörigen Fachbereichsnamen besteht.

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

Die gleichen Ergebnisse können mit der GroupJoin Methode wie folgt erzielt werden:

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

Das Ergebnis entspricht dem Resultset, das mithilfe der join-Klausel abgerufen werden würde, ohne dass die into-Klausel einen inneren Join ausführt. Der folgende Code veranschaulicht diese Abfrage:

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

Um Verkettungen zu vermeiden, kann die einzelne Join-Methode wie dargestellt verwendet werden:

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

Ausführen von Gruppenverknüpfungen

Die Gruppenverknüpfung ist nützlich für das Erstellen hierarchischer Datenstrukturen. Sie verbindet jedes Element aus der ersten Auflistung mit einem Satz von entsprechenden Elementen aus der zweiten Auflistung.

Hinweis

Jedes Element der ersten Auflistung erscheint im Ergebnissatz einer Gruppenverknüpfung, unabhängig davon, ob entsprechende Elemente in der zweiten Auflistung gefunden werden. Sollten keine entsprechenden Elemente gefunden werden, ist die Sequenz der entsprechenden Elemente für das Element leer. Die Ergebnisauswahl hat daher Zugriff auf jedes Element der ersten Auflistung. Dies unterscheidet sich von der Ergebnisauswahl in einer Verknüpfung, bei der keine Gruppen verknüpft werden. Diese kann nicht auf Elemente aus der ersten Auflistung zugreifen, die keine Übereinstimmung in der zweiten Auflistung haben.

Warnung

Enumerable.GroupJoin hat keine direkte Entsprechung unter den Begriffen herkömmlicher relationaler Datenbanken. Diese Methode implementiert jedoch eine Obermenge innerer Joins und linker äußerer Joins. Beide dieser Vorgänge können im Hinblick auf einen gruppierten Join geschrieben werden. Weitere Informationen finden Sie unter Entity Framework Core: GroupJoin.

Im ersten Beispiel in diesem Artikel wird das Ausführen eines gruppierten Joins gezeigt. Im zweiten Beispiel wird gezeigt, wie eine Gruppenverknüpfung zum Erstellen von XML-Elementen verwendet wird.

Gruppenverknüpfung

Das folgende Beispiel führt eine Gruppenverknüpfung von Objekten des Typs Department und Student aus, die auf der Deoartment.ID basiert und mit der Student.DepartmentID-Eigenschaft übereinstimmt. Bei einem Join, bei dem keine Gruppen verknüpft werden, wird für jede Übereinstimmung ein Elementpaar erzeugt. Im Gegensatz dazu erzeugt ein Gruppen-Join nur ein resultierendes Objekt für jedes Element der ersten Auflistung – in diesem Beispiel ein Department-Objekt. Die entsprechenden Elemente aus der zweiten Auflistung, die in diesem Beispiel Student-Objekte sind, werden in einer Auflistung gruppiert. Die Ergebnisauswahlfunktion erstellt schließlich einen anonymen Typ für jede Übereinstimmung, die aus Department.Name und einer Auflistung von Student-Objekten besteht.

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

Im obigen Beispiel enthält die Variable query die Abfrage, mit der eine Liste erstellt wird, in der jedes Element ein anonymer Typ ist, der den Namen des Fachbereichs und eine Auflistung der Lernenden in diesem Fachbereich enthält.

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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

Gruppen-Join zum Erstellen von XML-Code

Gruppenverknüpfungen lassen sich ideal für das Erstellen von XML mithilfe von LINQ to XML nutzen. Das folgende Beispiel ähnelt dem vorherigen, nur dass die Ergebnisauswahlfunktion anstatt eines anonymen Typs XML-Elemente erstellt, die die verknüpften Objekte darstellen.

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

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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

Ausführen von Left Outer Joins

Ein Left Outer Join ist eine Verknüpfung, die jedes Element der ersten Auflistung zurückgibt, unabhängig davon, ob es entsprechende Elemente in der zweiten Auflistung gibt. Sie können LINQ verwenden, um eine linke äußere Verknüpfung auszuführen, indem die Methode DefaultIfEmpty bei den Ergebnissen einer Gruppenverknüpfung aufgerufen wird.

Im folgenden Beispiel wird veranschaulicht, wie die Methode DefaultIfEmpty bei den Ergebnissen einer Gruppenverknüpfung verwendet wird, um eine linke äußere Verknüpfung auszuführen.

Der erste Schritt zum Erstellen eines Left Outer Join von zwei Auflistungen besteht darin, eine innere Verknüpfung durch Gruppenverknüpfung auszuführen. (Siehe Ausführen innerer Verknüpfungen für eine Erläuterung dieses Vorgangs.) In diesem Beispiel wird ein innerer Join der Liste der Department-Objekte mit der Liste der Student-Objekte basierend auf der ID eines Department-Objekt durchgeführt, das mit der DepartmentID der Lernenden übereinstimmt.

Der zweite Schritt besteht darin, jedes Element der ersten (linken) Liste in den Ergebnissatz einzuschließen, selbst wenn das Element in der rechten Auflistung keine Übereinstimmungen hat. Dies wird durch den Aufruf von DefaultIfEmpty für jede Sequenz von übereinstimmenden Elementen aus der Gruppenverknüpfung erreicht. In diesem Beispiel wird DefaultIfEmpty für jede Sequenz von übereinstimmenden Student-Objekten aufgerufen. Diese Methode gibt eine Auflistung zurück, die einen einzelnen Standardwert enthält, wenn die Sequenz von übereinstimmenden Student-Objekten für jedes Department-Objekt leer ist, womit die Methode sicherstellt, dass jedes Department-Objekt in der Ergebnisauflistung dargestellt wird.

Hinweis

Der Standardwert für einen Verweistyp ist null; deshalb wird im Beispiel nach einem NULL-Verweis gesucht, bevor auf jedes Element jeder Student-Auflistung zugegriffen wird.

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

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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

Siehe auch