Join operace v LINQ

Spojení přidruží objekty v jednom zdroji dat k objektům, které sdílejí společný atribut v jiném zdroji dat.

Důležité

Tyto ukázky používají datový zdroj System.Collections.Generic.IEnumerable<T>. Zdroje dat založené na System.Linq.IQueryProvider používají System.Linq.IQueryable<T> zdroje dat a stromy výrazů. Stromy výrazů mají omezení na povolenou syntax jazyka C#. Každý zdroj dat, jako je například IQueryProviderEF Core, může uvalovat dodatečná omezení. Projděte si dokumentaci ke zdroji dat.

Spojení je důležitou operací v dotazech, které cílí na zdroje dat, jejichž vztahy mezi sebou nemůžete sledovat přímo. V objektově orientovaném programování může spojení znamenat korelaci mezi objekty, která není modelována, například zpětný směr jednosměrné relace. Příkladem jednosměrné relace je Student třída, která má vlastnost typu Department , která představuje hlavní objekt, ale Department třída nemá vlastnost, která je kolekcí Student objektů. Pokud máte seznam Department objektů a chcete najít všechny studenty v každém oddělení, můžete je najít pomocí operace spojení.

Rozhraní LINQ poskytuje metody spojení: Join a GroupJoin. Tyto metody provádějí equi-spojení nebo spojení, která spojují dva zdroje dat na základě shody jejich klíčů. Pro porovnání Transact-SQL podporuje operátory spojení jiné než equals, například operátor less than. V relačních databázových termínech Join implementuje vnitřní spojení, typ spojení, ve kterém se vrátí pouze ty objekty, které mají shodu v jiné sadě dat. Metoda GroupJoin nemá žádné přímé ekvivalenty v relačních databázových termínech, ale implementuje nadmnožinu vnitřních spojení a levých vnějších spojení. Levé vnější spojení je spojení, které vrací každý prvek prvního (levého) zdroje dat, i když neobsahuje žádné korelované prvky v jiném zdroji dat.

Následující obrázek znázorňuje koncepční zobrazení dvou sad a prvků v těchto sadách, které jsou součástí vnitřního spojení nebo levého vnějšího spojení.

Dva překrývající se kruhy zobrazující vnitřní/vnější.

Metody

Název metody Popis Syntaxe výrazu dotazu jazyka C# Další informace
Join Spojí dvě sekvence na základě funkcí selektoru klíčů a extrahuje dvojice hodnot. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Spojí dvě sekvence na základě funkcí selektoru klíčů a seskupí výsledné shody pro každý prvek. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin
LeftJoin Koreluje prvky dvou sekvencí na základě odpovídajících klíčů. N/A Enumerable.LeftJoin

Queryable.LeftJoin
RightJoin Koreluje prvky dvou sekvencí na základě odpovídajících klíčů. N/A Enumerable.RightJoin

Queryable.RightJoin

Poznámka:

Následující příklady v tomto článku používají společné zdroje dat pro tuto oblast.
Každý z nich Student má úroveň ročníku, hlavní oddělení a řadu skóre. A Teacher má také vlastnost City, která identifikuje areál, kde učitel vede hodiny. A Department má jméno a odkaz na Teacher, který slouží jako vedoucí oddělení.
Ukázkové datové sady najdete ve zdrojovém úložišti.

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

Poznámka:

Běžné zdroje dat pro tuto oblast najdete v článku Přehled standardních operátorů dotazů .

Následující příklad používá klauzuli join … in … on … equals … ke spojení dvou sekvencí na základě konkrétní hodnoty:

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

Předchozí dotaz můžete vyjádřit pomocí syntaxe metody, jak je znázorněno v následujícím kódu:

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

Následující příklad používá klauzuli join … in … on … equals … into … ke spojení dvou sekvencí na základě konkrétní hodnoty a seskupení výsledných shod pro každý prvek:

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

Předchozí dotaz můžete vyjádřit pomocí syntaxe metody, jak je znázorněno v následujícím příkladu:

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

Proveďte vnitřní spojení

Ve vztahu k relačním databázím vnitřní spojení produkuje sadu výsledků, ve které se každý prvek první kolekce objeví jednou pro každý odpovídající prvek ve druhé kolekci. Pokud prvek v první kolekci neobsahuje žádné odpovídající prvky, nezobrazí se v sadě výsledků. Metoda Join, kterou klauzule join v jazyce C# implementuje, provádí vnitřní spojení. Následující příklady ukazují, jak provádět čtyři varianty vnitřního spojení:

  • Jednoduché vnitřní spojení, které koreluje prvky ze dvou zdrojů dat na základě jednoduchého klíče.
  • Vnitřní spojení, které koreluje prvky ze dvou zdrojů dat na základě složeného klíče. Složený klíč, což je klíč, který se skládá z více než jedné hodnoty, umožňuje korelovat prvky na základě více než jedné vlastnosti.
  • Jedná se o vícenásobné spojení, při kterém postupně připojujete další operace spojení k sobě.
  • Vnitřní spojení, které používá spojení skupiny.

Spojení s jedním klíčem

Následující příklad shoduje Teacher objekty s Department objekty, jejichž TeacherId se shodují s tím Teacher. Klauzule select v jazyce C# definuje, jak výsledné objekty vypadají. V následujícím příkladu jsou výsledné objekty anonymní typy, které se skládají z názvu oddělení a jména učitele, který vede oddělení.

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

Stejné výsledky dosáhnete pomocí Join syntaxe metody:

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

Učitelé, kteří nejsou vedoucími oddělení, se v konečných výsledcích nezobrazují.

Spojování složeného klíče

Místo korelace prvků na základě pouze jedné vlastnosti použijte složený klíč k porovnání prvků na základě více vlastností. Zadejte funkci selektoru klíčů pro každou kolekci, která vrátí anonymní typ, který se skládá z vlastností, které chcete porovnat. Pokud vlastnosti pojmenujete, musí mít stejný štítek v anonymním typu každého klíče. Vlastnosti musí být také zobrazeny ve stejném pořadí.

Následující příklad používá seznam Teacher objektů a seznam Student objektů k určení, kteří učitelé jsou také studenti. Oba tyto typy mají vlastnosti, které představují jméno a jméno rodiny každé osoby. Funkce, které vytvoří spojovací klíče z prvků každého seznamu, vrátí anonymní typ, který se skládá z vlastností. Operace spojení porovnává tyto složené klíče pro rovnost a vrací dvojice objektů z každého seznamu, kde se jméno i jméno rodiny shodují.

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

Tuto metodu Join můžete použít, jak je znázorněno v následujícím příkladu:

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

Vícenásobné spojení

Můžete připojit libovolný počet operací spojení k provedení více spojení. Každá join klauzule v jazyce C# koreluje zadaný zdroj dat s výsledky předchozího spojení.

První join klauzule přiřazuje studenty a oddělení na základě toho, že objekt Student se shoduje s objektem DepartmentIDDepartment. Vrátí posloupnost anonymních typů, které obsahují Student objekt a Department objekt.

Druhá join klauzule koreluje anonymní typy vrácené prvním spojením s Teacher objekty na základě ID daného učitele, které odpovídá ID vedoucího oddělení. Vrátí posloupnost anonymních typů, které obsahují jméno studenta, název oddělení a název vedoucího oddělení. Vzhledem k tomu, že tato operace je vnitřní spojení, dotaz vrátí pouze ty objekty z prvního zdroje dat, které mají shodu ve druhém zdroji dat.

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

Ekvivalentní dotaz, který používá více Join metod, používá stejný přístup s anonymním typem:

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

Vnitřní spojení pomocí seskupených spojení

Následující příklad ukazuje, jak implementovat vnitřní spojení pomocí spojení skupiny. Seznam Department objektů je seskupen s seznamem Student objektů na základě vlastnosti Department.ID, která odpovídá Student.DepartmentID. Spojení skupiny vytvoří kolekci zprostředkujících skupin, kde každá skupina se skládá z objektu Department a posloupnosti odpovídajících Student objektů. Druhá from klauzule kombinuje (nebo sploští) tuto sekvenci sekvencí do jedné delší sekvence. Klauzule select určuje typ prvků v konečné sekvenci. Tento typ je anonymní typ, který se skládá z jména studenta a odpovídajícího názvu oddělení.

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

Stejné výsledky můžete dosáhnout pomocí GroupJoin metody, jak je znázorněno v následujícím příkladu:

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

Výsledek je ekvivalentem sady výsledků získané pomocí join klauzule bez into klauzule k provedení vnitřního spojení. Následující kód ukazuje tento ekvivalentní dotaz:

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

Pokud se chcete vyhnout řetězení, použijte jednu Join metodu, jak je znázorněno zde:

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

Provádění seskupených spojení

Spojení skupiny je užitečné pro vytváření hierarchických datových struktur. Každý prvek z první kolekce spáruje se sadou korelovaných prvků z druhé kolekce.

Poznámka:

Každý prvek první kolekce se zobrazí ve výsledné sadě spojení skupiny bez ohledu na to, zda jsou v druhé kolekci nalezeny korelované prvky. Pokud nejsou nalezeny žádné korelované prvky, sekvence korelovaných prvků pro daný prvek je prázdná. Selektor výsledku má proto přístup ke každému prvku první kolekce. Chování se liší od selektoru výsledků v neskupinovém spojení, který nemůže získat přístup k prvkům z první kolekce, jež nemají v druhé kolekci žádnou shodu.

Upozornění

Enumerable.GroupJoin nemá žádný přímý ekvivalent v tradičních termínech relační databáze. Tato metoda však implementuje nadmnožinu vnitřních a levých vnějších spojení. Obě tyto operace lze zapsat z hlediska seskupených spojení. Další informace najdete v tématu Entity Framework Core GroupJoin.

První příklad v tomto článku ukazuje, jak provést připojení ke skupině. Druhý příklad ukazuje, jak pomocí spojení skupiny vytvořit elementy XML.

Připojení ke skupině

Následující příklad provede spojení skupin objektů typu Department a Student na Department.ID základě odpovídající Student.DepartmentID vlastnosti. Na rozdíl od neskupinového spojení, které vytváří dvojici prvků pro každou shodu, skupinové spojení vytvoří pouze jeden výsledný objekt pro každý prvek první kolekce. V tomto příkladu je první kolekcí objekt Department. Odpovídající prvky z druhé kolekce, které v tomto příkladu jsou Student objekty, jsou seskupeny do kolekce. Nakonec funkce selektoru výsledků vytvoří anonymní typ pro každou shodu, která se skládá z Department.Name a kolekce Student objektů.

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

V předchozím příkladu obsahuje proměnná dotaz, který vytvoří seznam, query kde každý prvek je anonymní typ, který obsahuje název oddělení a kolekci studentů, kteří studují v daném oddělení.

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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

Připojení ke skupině pro vytvoření XML

Spojení skupin jsou ideální pro vytváření XML pomocí LINQ to XML. Následující příklad je podobný předchozímu příkladu s tím rozdílem, že místo vytváření anonymních typů vytvoří funkce selektoru výsledků elementy XML, které představují spojené objekty.

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

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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

Proveďte vnější spojení

.NET 10 zahrnuje metody LeftJoin a RightJoin ve třídách System.Linq.Enumerable a System.Linq.Queryable. Tyto metody provádějí vnější levý equijoin a vnější pravý equijoin. Vnější levé ekvivelentní spojení je spojení, kde každý člen první sekvence je zařazen ve výstupní řadě, i když druhá řada neobsahuje shodu. Vnější pravé equi-spojení je spojení, kde každý člen druhé sekvence je zahrnut ve výstupní sekvenci, i když první sekvence neobsahuje shodu.

Emulace levého vnějšího spojení

Před .NET 10 použijte LINQ k provedení levého vnějšího spojení voláním metody DefaultIfEmpty výsledků spojení skupiny.

Následující příklad ukazuje, jak použít metodu DefaultIfEmpty na výsledcích spojení skupin k provedení levého vnějšího spojení.

Prvním krokem při vytváření levého vnějšího spojení dvou kolekcí je provedení vnitřního spojení prostřednictvím skupinového spojení. (Viz Provedení vnitřních spojení pro vysvětlení tohoto procesu.) V tomto příkladu je seznam Department objektů spojován se seznamem Student objektů na základě Department ID objektu, které odpovídá ID studenta DepartmentID.

Druhým krokem je zahrnutí každého prvku první (levé) kolekce do sady výsledků, i když tento prvek nemá v pravé kolekci žádné shody. Tento krok provedete voláním DefaultIfEmpty na každé sekvenci odpovídajících prvků ze spojení grupy. V tomto příkladu voláte DefaultIfEmpty na každou sekvenci objektů, které odpovídají Student. Metoda vrátí kolekci, která obsahuje jednu výchozí hodnotu, pokud sekvence odpovídajících Student objektů je prázdná pro libovolný Department objekt, čímž zajistí, že každý Department objekt je reprezentován ve výsledné kolekci.

Poznámka:

Výchozí hodnota pro typ odkazu je null; proto příklad kontroluje nulový odkaz před přístupem ke každému prvku každé Student kolekce.

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

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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

Viz také