Join Bewerkingen in LINQ

Een join van twee gegevensbronnen is de koppeling van objecten in de ene gegevensbron met objecten die een gemeenschappelijk kenmerk in een andere gegevensbron delen.

Joining is een belangrijke bewerking in query's die zijn gericht op gegevensbronnen waarvan de relaties met elkaar niet rechtstreeks kunnen worden gevolgd. Bij objectgeoriënteerde programmering kan samenvoegen een correlatie betekenen tussen objecten die niet zijn gemodelleerd, zoals de achteruitrichting van een eenrichtingsrelatie. Een voorbeeld van een eenrichtingsrelatie is een Student klasse met een eigenschap van het type Department dat de primaire waarde vertegenwoordigt, maar de Department klasse heeft geen eigenschap die een verzameling Student objecten is. Als u een lijst Department met objecten hebt en u alle studenten in elke afdeling wilt vinden, kunt u een join-bewerking gebruiken om ze te vinden.

De joinmethoden die in het LINQ-framework worden geboden, zijn Join en GroupJoin. Met deze methoden worden equijoins uitgevoerd of samengevoegd die overeenkomen met twee gegevensbronnen op basis van gelijkheid van hun sleutels. (Ter vergelijking ondersteunt Transact-SQL joinoperators anders dan equalsbijvoorbeeld de less than operator.) Implementeert in relationele databasetermen Join een inner join, een type join waarin alleen de objecten met een overeenkomst in de andere gegevensset worden geretourneerd. De GroupJoin methode heeft geen direct equivalent in relationele databasetermen, maar implementeert een superset van inner joins en left outer joins. Een left outer join is een join die elk element van de eerste (linker) gegevensbron retourneert, zelfs als er geen gecorreleerde elementen in de andere gegevensbron zijn.

In de volgende afbeelding ziet u een conceptuele weergave van twee sets en de elementen in die sets die zijn opgenomen in een inner join of een left outer join.

Twee overlappende cirkels met binnenste/ Buitenste.

Methoden

Methodenaam Beschrijving Syntaxis van C#-queryexpressie Meer informatie
Join Joins twee reeksen op basis van sleutelkiezerfuncties en extraheert paren waarden. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Joins twee reeksen op basis van sleutelkiezerfuncties en groepeert de resulterende overeenkomsten voor elk element. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

In de volgende voorbeelden in dit artikel worden de algemene gegevensbronnen voor dit gebied gebruikt:

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

Elk Student heeft een cijferniveau, een primaire afdeling en een reeks scores. Een Teacher heeft ook een City eigenschap die de campus identificeert waar de docent klassen heeft. A Department heeft een naam en een verwijzing naar een Teacher persoon die als afdelingshoofd fungeert.

In het volgende voorbeeld wordt de join … in … on … equals … component gebruikt om twee reeksen samen te voegen op basis van een specifieke waarde:

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

De voorgaande query kan worden uitgedrukt met behulp van methodesyntaxis, zoals wordt weergegeven in de volgende code:

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

In het volgende voorbeeld wordt de join … in … on … equals … into … component gebruikt om twee reeksen samen te voegen op basis van specifieke waarde en de resulterende overeenkomsten voor elk element te groeperen:

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

De voorgaande query kan worden uitgedrukt met behulp van methodesyntaxis, zoals wordt weergegeven in het volgende voorbeeld:

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

Inner joins uitvoeren

In relationele databasetermen produceert een inner join een resultatenset waarin elk element van de eerste verzameling één keer wordt weergegeven voor elk overeenkomend element in de tweede verzameling. Als een element in de eerste verzameling geen overeenkomende elementen bevat, wordt dit niet weergegeven in de resultatenset. Met Join de methode, die wordt aangeroepen door de join component in C#, wordt een inner join geïmplementeerd. In de volgende voorbeelden ziet u hoe u vier variaties van een inner join uitvoert:

  • Een eenvoudige inner join die elementen van twee gegevensbronnen correleert op basis van een eenvoudige sleutel.
  • Een inner join die elementen uit twee gegevensbronnen correleert op basis van een samengestelde sleutel. Met een samengestelde sleutel, een sleutel die uit meer dan één waarde bestaat, kunt u elementen correleren op basis van meer dan één eigenschap.
  • Een meervoudige join waarin opeenvolgende joinbewerkingen aan elkaar worden toegevoegd.
  • Een inner join die wordt geïmplementeerd met behulp van een groepsdeelname.

Join met één sleutel

Het volgende voorbeeld komt overeen Teacher met objecten met Deparment objecten waarvan TeacherId deze Teacherovereenkomt. De select component in C# definieert hoe de resulterende objecten eruitzien. In het volgende voorbeeld zijn de resulterende objecten anonieme typen die bestaan uit de afdelingsnaam en de naam van de docent die de afdeling leidt.

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

U bereikt dezelfde resultaten met behulp van de syntaxis van de Join methode:

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

De docenten die geen afdelingshoofden zijn, worden niet weergegeven in de eindresultaten.

Samengestelde sleuteldeelname

In plaats van elementen te correleren op basis van slechts één eigenschap, kunt u een samengestelde sleutel gebruiken om elementen te vergelijken op basis van meerdere eigenschappen. Geef de sleutelkiezerfunctie op voor elke verzameling om een anoniem type te retourneren dat bestaat uit de eigenschappen die u wilt vergelijken. Als u de eigenschappen labelt, moeten ze hetzelfde label hebben in het anonieme type van elke sleutel. De eigenschappen moeten ook in dezelfde volgorde worden weergegeven.

In het volgende voorbeeld wordt een lijst Teacher met objecten en een lijst Student met objecten gebruikt om te bepalen welke docenten ook leerlingen/studenten zijn. Beide typen hebben eigenschappen die de voor- en familienaam van elke persoon vertegenwoordigen. De functies waarmee de joinsleutels van de elementen van elke lijst worden gemaakt, retourneren een anoniem type dat uit de eigenschappen bestaat. De join-bewerking vergelijkt deze samengestelde sleutels voor gelijkheid en retourneert paren van objecten uit elke lijst waarin zowel de voornaam als de familienaam overeenkomen.

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

U kunt de Join methode gebruiken, zoals wordt weergegeven in het volgende voorbeeld:

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

Meerdere joins

Elk aantal joinbewerkingen kan aan elkaar worden toegevoegd om een meerdere joins uit te voeren. Elke join component in C# correleert een opgegeven gegevensbron met de resultaten van de vorige join.

De eerste join component komt overeen met studenten en afdelingen op basis van de overeenkomsten Department van DepartmentIDIDeen Student object. Het retourneert een reeks anonieme typen die het Student object en Department object bevatten.

De tweede join component correleert de anonieme typen die worden geretourneerd door de eerste join met Teacher objecten op basis van de id van die docent die overeenkomen met de afdelingshoofd-id. Het retourneert een reeks anonieme typen die de naam van de student, de naam van de afdeling en de naam van de afdelingsleider bevatten. Omdat deze bewerking een inner join is, worden alleen die objecten uit de eerste gegevensbron geretourneerd die een overeenkomst hebben in de tweede gegevensbron.

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

Het equivalent met meerdere Join methoden gebruikt dezelfde benadering met het anonieme type:

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

Inner join met behulp van gegroepeerde join

In het volgende voorbeeld ziet u hoe u een inner join implementeert met behulp van een groepsdeelname. De lijst Department met objecten wordt gegroepeerd aan de lijst met objecten op basis van Student de Department.ID overeenkomende Student.DepartmentID eigenschap. De groepsdeelname maakt een verzameling tussenliggende groepen, waarbij elke groep bestaat uit een Department object en een reeks overeenkomende Student objecten. De tweede from component combineert (of vlakt) deze reeks reeksen in één langere reeks. De select component specificeert het type elementen in de uiteindelijke volgorde. Dit type is een anoniem type dat bestaat uit de naam van de student en de overeenkomende afdelingsnaam.

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

Dezelfde resultaten kunnen als volgt worden bereikt met behulp van GroupJoin de methode:

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

Het resultaat is gelijk aan de resultatenset die is verkregen met behulp van de join component zonder de into component om een inner join uit te voeren. De volgende code demonstreert deze equivalente query:

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

Om ketens te voorkomen, kan de ene Join methode worden gebruikt zoals hier wordt weergegeven:

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

Gegroepeerde joins uitvoeren

De groepsdeelname is handig voor het produceren van hiërarchische gegevensstructuren. Het paren van elk element uit de eerste verzameling met een set gecorreleerde elementen uit de tweede verzameling.

Notitie

Elk element van de eerste verzameling wordt weergegeven in de resultatenset van een groepsdeelname, ongeacht of gecorreleerde elementen worden gevonden in de tweede verzameling. In het geval dat er geen gecorreleerde elementen worden gevonden, is de volgorde van gecorreleerde elementen voor dat element leeg. De resultaatkiezer heeft daarom toegang tot elk element van de eerste verzameling. Dit verschilt van de resultaatkiezer in een niet-groepsdeelname, die geen toegang heeft tot elementen uit de eerste verzameling die niet overeenkomen in de tweede verzameling.

Waarschuwing

Enumerable.GroupJoin heeft geen direct equivalent in traditionele relationele databasetermen. Deze methode implementeert echter wel een superset van inner joins en left outer joins. Beide bewerkingen kunnen worden geschreven in termen van een gegroepeerde join. Zie Entity Framework Core, GroupJoinvoor meer informatie.

In het eerste voorbeeld in dit artikel ziet u hoe u een groepsdeelname uitvoert. In het tweede voorbeeld ziet u hoe u een groepsdeelname gebruikt om XML-elementen te maken.

Groepsdeelname

In het volgende voorbeeld wordt een groepsdeelname van objecten van het type Department uitgevoerd en Student op basis van de Deoartment.ID overeenkomende Student.DepartmentID eigenschap. In tegenstelling tot een niet-groepsdeelname, die voor elke overeenkomst een paar elementen produceert, produceert de groepsdeelname slechts één resulterend object voor elk element van de eerste verzameling, dat in dit voorbeeld een Department object is. De bijbehorende elementen uit de tweede verzameling, die in dit voorbeeld objecten zijn Student , worden gegroepeerd in een verzameling. Ten slotte maakt de functie resultaatkiezer een anoniem type voor elke overeenkomst die bestaat uit Department.Name en een verzameling Student objecten.

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

In het bovenstaande voorbeeld query bevat de variabele de query waarmee een lijst wordt gemaakt waarin elk element een anoniem type is dat de naam van de afdeling bevat en een verzameling studenten die op die afdeling studeren.

De equivalente query met behulp van de methodesyntaxis wordt weergegeven in de volgende code:

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

Groep toevoegen om XML te maken

Groepsdeelnames zijn ideaal voor het maken van XML met behulp van LINQ naar XML. Het volgende voorbeeld is vergelijkbaar met het vorige voorbeeld, behalve dat in plaats van anonieme typen te maken, de functie resultaatkiezer XML-elementen maakt die de gekoppelde objecten vertegenwoordigen.

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

De equivalente query met behulp van de methodesyntaxis wordt weergegeven in de volgende code:

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

Left outer joins uitvoeren

Een left outer join is een join waarin elk element van de eerste verzameling wordt geretourneerd, ongeacht of deze gecorreleerde elementen in de tweede verzameling bevat. U kunt LINQ gebruiken om een left outer join uit te voeren door de DefaultIfEmpty methode aan te roepen voor de resultaten van een groepsdeelname.

In het volgende voorbeeld ziet u hoe u de DefaultIfEmpty methode gebruikt voor de resultaten van een groepsdeelname om een left outer join uit te voeren.

De eerste stap bij het produceren van een left outer join van twee verzamelingen is het uitvoeren van een inner join met behulp van een groepsdeelname. (Zie Voer inner joins uit voor een uitleg van dit proces.) In dit voorbeeld wordt de lijst Department met objecten binnenste gekoppeld aan de lijst met objecten op basis van de id van Student een Department object die overeenkomt met de student DepartmentID.

De tweede stap bestaat uit het opnemen van elk element van de eerste (linker) verzameling in de resultatenset, zelfs als dat element geen overeenkomsten bevat in de juiste verzameling. Dit wordt bereikt door elke reeks overeenkomende elementen van de groepsdeelname aan te roepen DefaultIfEmpty . In dit voorbeeld DefaultIfEmpty wordt elke reeks overeenkomende Student objecten aangeroepen. De methode retourneert een verzameling die één standaardwaarde bevat als de reeks overeenkomende Student objecten leeg is voor een Department object, zodat elk Department object wordt weergegeven in de resultatenverzameling.

Notitie

De standaardwaarde voor een verwijzingstype is null; daarom controleert het voorbeeld op een null-verwijzing voordat elk element van elke Student verzameling wordt geopend.

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

De equivalente query met behulp van de methodesyntaxis wordt weergegeven in de volgende code:

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

Zie ook