Join Műveletek a LINQ-ban

Két adatforrás összekapcsolása az egyik adatforrás objektumainak társítása olyan objektumokkal, amelyek közös attribútumot használnak egy másik adatforrásban.

JoinAz ing egy fontos művelet olyan lekérdezésekben, amelyek olyan adatforrásokat céloznak meg, amelyek egymáshoz való viszonya nem követhető közvetlenül. 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 az objektumok listájával Department , és meg szeretné keresni az egyes részlegek összes tanulóját, egy illesztési művelettel megkeresheti őket.

A LINQ-keretrendszerben megadott illesztési módszerek a következők Join : és GroupJoin. Ezek a metódusok a kulcsok egyenlősége alapján két adatforrásnak megfelelő egyenlő illesztéseket vagy illesztéseket hajtanak végre. (Összehasonlításképpen a Transact-SQL támogatja a nem az operátortól eltérő equalsillesztő operátorokat, például az operátort less than .) 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 belső/ Külső.

Metódusok

Metódus neve Leírás C# lekérdezési kifejezés szintaxisa További információ
Join Joins két szekvenciát a kulcsválasztó függvények alapján, és kinyeri az értékpárokat. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Joins két szekvenciát 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

A cikkben szereplő alábbi példák a terület közös adatforrásait használják:

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

Mindegyiknek Student van egy osztályszintje, egy elsődleges osztálya és egy sor pontszáma. Az A-nek Teacher is van egy City tulajdonsága, amely azonosítja azt a campust, ahol a tanár órákat tart. Az A-nek Department van egy neve, és egy olyan személyre Teacher való hivatkozás, aki az osztályvezető.

Az alábbi példa a join … in … on … equals … záradékot használja két sorozat adott értéken alapuló összekapcsolásához:

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és metódusszintaxissal fejezhető ki az alábbi kódban látható módon:

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ék használatával illeszt két sorozatot 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és metódusszintaxissal fejezhető ki az alábbi példában látható módon:

// 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 a 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 C#-ban a join záradék által meghívott metódus belső illesztés implementál. Az alábbi példák bemutatják, hogyan hajthat végre négy változatot egy belső illesztésben:

  • Egyszerű belső illesztés, amely két adatforrás elemeit egy egyszerű kulcs alapján korrelálja.
  • Egy belső illesztés, amely két adatforrás elemeit egy összetett kulcs alapján korrelálja. 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 illesztés , amelyben az egymást követő illesztési műveletek egymáshoz vannak fűzve.
  • Egy belső illesztés, amelyet csoportillesztés használatával valósítunk meg.

Egykulcsos illesztés

Az alábbi példa olyan objektumokkal egyezik Teacher meg, amelyek TeacherId megegyeznek a TeacherkövetkezővelDeparment. 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 metódus szintaxisával Join é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égeredményben.

Összetett kulcs illesztése

Az elemek egyetlen tulajdonságon alapuló korrelációja helyett összetett kulccsal összehasonlíthatja az elemeket több tulajdonság alapján. 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, mindegyik kulcs névtelen típusában 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

Tetszőleges számú illesztési művelet hozzáfűzhető egymáshoz több illesztés végrehajtásához. 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 egy objektum objektumának megfelelő Department objektum DepartmentID alapján Student egyezik a tanulókkal és a részlegekévalID. Az objektumot és Department objektumot Student 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 az adott tanár azonosítójának megfelelő objektumokkal Teacher . 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 rendszer csak azokat az objektumokat adja vissza, amelyek az első adatforrásból származnak, és 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ű módszer 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 illesztés használatával

Az alábbi példa bemutatja, hogyan valósíthat meg belső illesztéseket csoportillesztés használatával. Az objektumok listája Department a tulajdonságnak megfelelő Student.DepartmentID objektumok listájához StudentDepartment.ID van csoportosítva. A csoportillesztés köztes csoportok gyűjteményét hozza létre, ahol minden csoport egy objektumból és egy Department 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}");
}

Ugyanezek az eredmények a következő módszerrel érhetők el GroupJoin :

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 a join záradék nélküli into záradék használatával kapott egy belső illesztés végrehajtásához. 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 az itt bemutatott egyetlen Join módszer használható:

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.

Feljegyzé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. Abban az esetben, 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 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 végezhet csoportillesztéseket. A második példa bemutatja, hogyan hozhat létre XML-elemeket csoportillesztés használatával.

Csoporthoz való csatlakozás

Az alábbi példa a tulajdonságnak DepartmentStudentDepartment.ID megfelelő Student.DepartmentID típusú objektumok csoportillesztését hajtja végre. 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, amely ebben a példában 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 létrehoz egy névtelen típust az egyes egyezésekhez, amelyekből és objektumok gyűjteményéből Department.NameStudent á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}");
    }
}

A fenti példában a változó azt a lekérdezést tartalmazza, amely létrehoz egy listát, query 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);

Bal oldali külső illesztések végrehajtása

A bal oldali külső illesztés olyan illesztés, amelyben az első gyűjtemény minden eleme vissza lesz adva, függetlenül attól, hogy a második gyűjteményben vannak-e korrelált elemei. A LINQ használatával bal oldali külső illesztést hajthat végre, ha meghívja a DefaultIfEmpty metódust a csoportillesztés eredményein.

Az alábbi példa bemutatja, hogyan használható a módszer a DefaultIfEmpty 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 Department csatlakozik az objektumok listájáhozStudent, 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. Ez úgy érhető el, hogy meghívja DefaultIfEmpty a csoportillesztés egyező elemeinek mindegyik sorozatát. Ebben a példában DefaultIfEmpty a program meghívja az egyező Student objektumok mindegyik sorozatát. 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.

Feljegyzés

A referenciatípus nullalapértelmezett értéke tehát a null értékű hivatkozás, mielőtt hozzáfér az egyes Student gyűjtemények egyes 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, 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}");
}

Lásd még