Join műveletek a LINQ-ban

Az illesztések az egyik adatforrásban lévő objektumokat olyan objektumokkal társítják, amelyek közös attribútumot használnak egy másik adatforrásban.

Fontos

Ezek a minták adatforrást System.Collections.Generic.IEnumerable<T> használnak. Az System.Linq.IQueryProvider-alapú adatforrások System.Linq.IQueryable<T> adatokforrásokat és kifejezésfákat használnak. A kifejezésfák korlátozottan használhatják az engedélyezett C# szintaxist. Emellett minden adatforrás IQueryProvider , például az EF Core további korlátozásokat is alkalmazhat. Ellenőrizze az adatforrás dokumentációját.

A csatlakozás olyan lekérdezések fontos művelete, amelyek olyan adatforrásokat céloznak meg, amelyek kapcsolatát nem lehet közvetlenül követni. Az objektumorientált programozásban az összekapcsolás olyan korrelációt jelenthet a nem modellezett objektumok között, mint például egy egyirányú kapcsolat visszafelé irányuló iránya. Az egyirányú kapcsolatokra példa egy Student osztály, amelynek típusa Department a főt jelöli, de az Department osztálynak nincs objektumgyűjteményt tartalmazó tulajdonsága Student . Ha rendelkezik Department objektumok listájával, és az egyes részlegek összes tanulóját meg szeretné találni, egy illesztési művelettel megkeresheti őket.

A LINQ-keretrendszer illesztési módszereket biztosít: Join és GroupJoin. Ezek a metódusok ekvijoinokat, vagyis a kulcsok egyenlősége alapján történő illesztéseket hajtanak végre két adatforráson. Összehasonlításképpen a Transact-SQL támogatja a equals kívüli illesztési operátorokat, például a less than operátort. A relációs adatbázis szempontjából Join egy belső illesztés implementálása, egy olyan illesztéstípus, amelyben csak azokat az objektumokat adja vissza a rendszer, amelyek megegyeznek a többi adatkészletben. A GroupJoin metódusnak nincs közvetlen megfelelője a relációs adatbázis szempontjából, de belső illesztések és bal oldali külső illesztések szuperhalmazát valósítja meg. A bal oldali külső illesztés olyan illesztés, amely az első (bal oldali) adatforrás minden elemét visszaadja, még akkor is, ha nincsenek korrelált elemei a másik adatforrásban.

Az alábbi ábrán két halmaz és a belső illesztésben vagy bal oldali külső illesztésben szereplő elemek fogalmi nézete látható.

Két egymást átfedő kör, amelyek belső/külső köröket mutatnak.

Metódusok

Eljárás neve Leírás C# lekérdezési kifejezés szintaxisa További információ
Join Két sorozatot illeszt össze a kulcsválasztó függvények alapján, és kinyeri az értékpárokat. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Két sorozatot illeszt össze a kulcsválasztó függvények alapján, és csoportosítja az eredményként kapott egyezéseket az egyes elemekhez. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin
LeftJoin Két sorozat elemeit korrelálja egyező kulcsok alapján. N/A Enumerable.LeftJoin

Queryable.LeftJoin
RightJoin Két sorozat elemeit korrelálja egyező kulcsok alapján. N/A Enumerable.RightJoin

Queryable.RightJoin

Megjegyzés

A cikkben szereplő alábbi példák a terület közös adatforrásait használják.
Mindegyiknek Student van egy osztályszintje, egy elsődleges tanszéke és egy sor pontszáma. A Teacher elemnek is van egy City tulajdonsága, amely azonosítja azt a campust, ahol a tanár órákat tart. A Department nevet visel, és egy Teacher-ra van hivatkozással, aki az osztályvezető.
A mintaadatkészlet a forrásadattárban található.

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

Megjegyzés

A terület gyakori adatforrásai a Standard lekérdezési operátorok áttekintése című cikkben találhatóak.

Az alábbi példa a join … in … on … equals … feltételt használja két szekvencia összekapcsolására egy adott érték alapján.

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

Az előző lekérdezést metódusszintaxissal fejezheti ki, ahogyan az az alábbi kódban is látható:

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

Az alábbi példa a join … in … on … equals … into … záradékot használja két szekvenciához egy adott érték alapján, és csoportosítja az eredményként kapott egyezéseket az egyes elemekhez:

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

Az előző lekérdezést metódusszintaxissal fejezheti ki, ahogyan az a következő példában is látható:

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

Belső illesztések végrehajtása

A relációs adatbázis szempontjából egy belső illesztés olyan eredményhalmazt hoz létre, amelyben az első gyűjtemény minden eleme egyszer megjelenik a második gyűjtemény minden egyező eleméhez. Ha az első gyűjtemény egyik eleme nem rendelkezik egyező elemekkel, az nem jelenik meg az eredményhalmazban. A Join metódus, amelyet a join C# záradéka hív meg, implementál egy belső illesztést. Az alábbi példák bemutatják, hogyan lehet véghez vinni négyféle belső illesztést.

  • Egyszerű belső illesztés, amely két adatforrás elemeit egy egyszerű kulcs alapján korrelálja.
  • Belső illesztés, amely két adatforrás elemeit korrelálja egy összetett kulcs alapján. Az összetett kulcs, amely egynél több értékből álló kulcs, lehetővé teszi az elemek egynél több tulajdonságon alapuló korrelációját.
  • Több csatlakozás, amelyekben egymást követő csatlakozási műveleteket illesztünk egymáshoz.
  • Egy belső illesztés, amely csoportillesztést használ.

Egykulcsos összekapcsolás

Az alábbi példa olyan Teacher objektumokat párosít Department objektumokkal, amelyek TeacherId tulajdonsága megegyezik azzal Teacher. A select C# záradéka határozza meg az eredményként kapott objektumok megjelenését. Az alábbi példában az eredményül kapott objektumok névtelen típusok, amelyek a részleg nevét és a részleget vezető tanár nevét tartalmazzák.

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

Ugyanezeket az eredményeket a Join metódus szintaxisával érheti el.

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

Azok a tanárok, akik nem osztályvezetők, nem jelennek meg a végleges eredményekben.

Összetett kulcs illesztése

Az elemek egyetlen tulajdonságon alapuló korrelációja helyett összetett kulccsal hasonlítsa össze a több tulajdonságon alapuló elemeket. Adja meg az egyes gyűjtemények kulcsválasztó függvényét egy névtelen típus visszaadásához, amely az összehasonlítandó tulajdonságokból áll. Ha megjelöli a tulajdonságokat, akkor minden névtelen típusú kulcsnak ugyanazzal a címkével kell rendelkeznie. A tulajdonságoknak ugyanabban a sorrendben is meg kell jelenniük.

Az alábbi példa egy objektumlistát Teacher és egy objektumlistát Student használ annak meghatározására, hogy mely tanárok is diákok. Mindkét típus rendelkezik olyan tulajdonságokkal, amelyek az egyes személyek vezeték- és családnevét jelölik. Az illesztőkulcsokat az egyes listaelemekből létrehozó függvények névtelen típust adnak vissza, amely a tulajdonságokból áll. Az illesztési művelet összehasonlítja ezeket az összetett kulcsokat az egyenlőség szempontjából, és minden listából visszaadja az objektumpárokat, ahol az utónév és a családnév is egyezik.

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

A metódust az Join alábbi példában látható módon használhatja:

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

Több illesztés

Több illesztés végrehajtásához tetszőleges számú illesztési műveletet hozzáfűzhet. A C# minden join záradéka egy adott adatforrást korrelál az előző illesztés eredményeivel.

Az első join záradék a tanulókat és a részlegeket párosítja egy Student objektum DepartmentID és egy Department objektum ID alapján. A Student és Department objektumot tartalmazó névtelen típusok sorozatát adja vissza.

A második join záradék korrelálja az első illesztés által visszaadott névtelen típusokat Teacher objektumokkal az osztályvezető azonosítójával egyező tanári azonosító alapján. Névtelen típusokat ad vissza, amelyek tartalmazzák a tanuló nevét, a részleg nevét és a részlegvezető nevét. Mivel ez a művelet egy belső illesztés, a lekérdezés csak azokat az objektumokat adja vissza az első adatforrásból, amelyek megegyeznek a második adatforrásban.

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

A több Join metódust használó egyenértékű lekérdezés ugyanazt a megközelítést használja a névtelen típussal:

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

Belső illesztés csoportosított csatlakozással

Az alábbi példa bemutatja, hogyan valósíthat meg belső illesztéseket csoportillesztés használatával. Az Department objektumok listája a Student objektumok listájával van csoportosan összekapcsolva, a Department.ID és a Student.DepartmentID tulajdonságok egyezése alapján. A csoportillesztés köztes csoportok gyűjteményét hozza létre, ahol minden csoport egy Department objektumból és egy egyező Student objektumok sorozatából áll. A második from záradék ezt a sorozatot egy hosszabb sorozatba egyesíti (vagy simítja). A select záradék a végső sorrend elemeinek típusát határozza meg. Ez a típus egy névtelen típus, amely a tanuló nevéből és az egyező részleg nevéből áll.

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

Ugyanezeket az eredményeket a metódussal GroupJoin érheti el, ahogyan az alábbi példában is látható:

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

Az eredmény megegyezik azzal az eredményhalmazsal, amelyet az join záradék használatával kapott a belső illesztés végrehajtásához szükséges into záradék nélkül. Az alábbi kód ezt az egyenértékű lekérdezést mutatja be:

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

A láncolás elkerülése érdekében használja az itt bemutatott egyetlen Join módszert:

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

Csoportosított illesztések végrehajtása

A csoportillesztés hierarchikus adatstruktúrák létrehozásához hasznos. Párosítja az első gyűjtemény egyes elemeit a második gyűjtemény korrelált elemeivel.

Megjegyzés

Az első gyűjtemény minden eleme megjelenik egy csoportillesztés eredményhalmazában, függetlenül attól, hogy a korrelált elemek megtalálhatók-e a második gyűjteményben. Ha nem találhatók korrelált elemek, az adott elemhez tartozó korrelált elemek sorrendje üres. Az eredményválasztó ezért hozzáfér az első gyűjtemény minden eleméhez. Ez a viselkedés eltér a nem csoporthoz tartozó illesztés eredményválasztójától, amely nem fér hozzá az első gyűjtemény azon elemeihez, amelyek nem egyeznek a második gyűjteményben.

Figyelmeztetés

Enumerable.GroupJoin nincs közvetlen egyenértékű a hagyományos relációs adatbázis szempontjából. Ez a módszer azonban a belső illesztések és a bal oldali külső illesztések szuperhalmazát implementálja. Mindkét művelet csoportosított illesztésként írható. További információ: Entity Framework Core, GroupJoin.

A cikk első példája bemutatja, hogyan lehet csoporthoz csatlakozni. A második példa bemutatja, hogyan használható csoportillesztés XML-elemek létrehozására.

Csoporthoz való csatlakozás

Az alábbi példa a Department-nak a Student tulajdonsággal való egyezése alapján végzi el a Department.ID és Student.DepartmentID típusú objektumok csoportos összekapcsolását. A nem csoportos illesztéssel ellentétben, amely minden egyes egyezéshez létrehoz egy-egy elemet, a csoportillesztés csak egy eredményt adó objektumot hoz létre az első gyűjtemény minden eleméhez. Ebben a példában az első gyűjtemény egy Department objektum. A második gyűjtemény megfelelő elemei, amelyek ebben a példában Student objektumok, gyűjteménybe vannak csoportosítva. Végül az eredményválasztó függvény egy névtelen típust hoz létre minden egyezés számára, amely a Department.Name elemből és a Student objektumok gyűjteményéből áll.

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

Az előző példában a változó tartalmazza azt a query lekérdezést, amely létrehoz egy listát, amelyben minden elem egy névtelen típus, amely tartalmazza a részleg nevét és a részlegen tanuló diákok gyűjteményét.

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

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

Csoportillesztés XML létrehozásához

A csoportillesztések ideálisak az XML létrehozásához a LINQ és az XML használatával. Az alábbi példa az előző példához hasonló, azzal a kivételrel, hogy névtelen típusok létrehozása helyett az eredményválasztó függvény xml-elemeket hoz létre, amelyek az összekapcsolt objektumokat jelölik.

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

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

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

Külső illesztések végrehajtása

.NET 10 LeftJoin és RightJoin metódusokat tartalmaz a System.Linq.Enumerable és System.Linq.Queryable osztályokban. Ezek a módszerek egy külső bal oldali equijoint, illetve egy külső jobb oldali equijoint hajtanak végre. A külső bal oldali egyezés olyan illesztés, amelyben az első sorozat minden tagja szerepel a kimeneti sorozatban, még akkor is, ha a második sorozat nem tartalmaz egyezést. A külső jobb oldali egyenlőség-alapú illesztés egy olyan illesztés, amelyben a második sorozat minden tagja szerepel az eredmény sorozatban, még akkor is, ha az első sorozat nem tartalmaz egyezést.

Bal oldali külső illesztés emulálása

A .NET 10 előtti időszakban a LINQ használatával végezzen bal külső illesztést a csoportillesztés eredményein a DefaultIfEmpty metódus meghívásával.

Az alábbi példa bemutatja, hogyan használhatja a DefaultIfEmpty metódust egy csoportillesztés eredményein a bal oldali külső illesztés végrehajtásához.

Két gyűjtemény bal oldali külső illesztésének első lépése egy belső illesztés csoportillesztés használatával történő végrehajtása. (Lásd:Belső illesztések végrehajtása a folyamat magyarázatához.) Ebben a példában az objektumok listája Department egy olyan objektumazonosító alapján Student csatlakozik az objektumok listájáhozDepartment, amely megfelel a tanulóénakDepartmentID.

A második lépés az, hogy az első (bal oldali) gyűjtemény minden elemét belefoglalja az eredményhalmazba, még akkor is, ha az elemnek nincs egyezése a megfelelő gyűjteményben. Ezt a lépést úgy hajthatja végre, hogy meghívja a DefaultIfEmpty függvényt minden egyes, a csoportillesztésből származó egyező elem sorozatára. Ebben a példában az egymással egyező DefaultIfEmpty objektumok mindegyik sorozatára meghívja a Student függvényt. A metódus egy olyan gyűjteményt ad vissza, amely egyetlen alapértelmezett értéket tartalmaz, ha az egyező Student objektumok sorozata üres bármely Department objektum esetében, biztosítva, hogy minden Department objektum szerepeljen az eredménygyűjteményben.

Megjegyzés

A referenciatípus alapértelmezett értéke null, ezért az példában ellenőrizzük a null értékű hivatkozást, mielőtt hozzáférnénk az egyes Student gyűjtemények elemeihez.

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

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

var query = students
    .GroupJoin(
        departments,
        student => student.DepartmentID,
        department => department.ID,
        (student, departmentList) => new { student, subgroup = departmentList })
    .SelectMany(
        joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
        (student, department) => new
        {
            student.student.FirstName,
            student.student.LastName,
            Department = department?.Name ?? string.Empty
        });

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

Lásd még