Dela via


Join Åtgärder i LINQ

En join av två datakällor är associationen mellan objekt i en datakälla och objekt som delar ett gemensamt attribut i en annan datakälla.

Viktigt!

Dessa exempel använder en System.Collections.Generic.IEnumerable<T> datakälla. Datakällor baserade på System.Linq.IQueryProvider användning av System.Linq.IQueryable<T> datakällor och uttrycksträd. Uttrycksträd har begränsningar för den tillåtna C#-syntaxen. Dessutom kan varje IQueryProvider datakälla, till exempel EF Core , införa fler begränsningar. Kontrollera dokumentationen för din datakälla.

Anslutning är en viktig åtgärd i frågor som riktar sig till datakällor vars relationer till varandra inte kan följas direkt. I objektorienterad programmering kan koppling innebära en korrelation mellan objekt som inte är modellerade, till exempel bakåtriktningen för en enkelriktad relation. Ett exempel på en enkelriktad relation är en Student klass som har en egenskap av typen Department som representerar huvudnamnet, men Department klassen har ingen egenskap som är en samling Student objekt. Om du har en lista över Department objekt och vill hitta alla studenter på varje avdelning kan du använda en join åtgärd för att hitta dem.

Metoderna join som tillhandahålls i LINQ-ramverket är Join och GroupJoin. Dessa metoder utför likvärdighet eller kopplingar som matchar två datakällor baserat på likheten mellan deras nycklar. (Som jämförelse stöder join Transact-SQL andra operatorer än equals, till exempel operatorn less than .) I relationsdatabastermer Join implementerar en inre join, en typ av join där endast de objekt som har en matchning i den andra datauppsättningen returneras. Metoden GroupJoin har ingen direkt motsvarighet i relationsdatabastermer, men den implementerar en supermängd av inre kopplingar och vänster yttre kopplingar. En vänster yttre join är en join som returnerar varje element i den första (vänstra) datakällan, även om den inte har några korrelerade element i den andra datakällan.

Följande bild visar en konceptuell vy över två uppsättningar och elementen i de uppsättningar som ingår i antingen en inre join eller en vänster yttre join.

Två överlappande cirklar som visar inre/ yttre.

Metoder

Metodnamn beskrivning Syntax för C#-frågeuttryck Mer information
Join Kopplar ihop två sekvenser baserat på nyckelväljarens funktioner och extraherar par med värden. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Kopplar två sekvenser baserat på nyckelväljarens funktioner och grupperar de resulterande matchningarna för varje element. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Kommentar

I följande exempel i den här artikeln används vanliga datakällor för det här området.
Var Student och en har en betygsnivå, en primär avdelning och en serie poäng. En Teacher har också en City egenskap som identifierar det campus där läraren har klasser. A Department har ett namn och en referens till en Teacher som fungerar som avdelningschef.
Du hittar exempeldatauppsättningen i källdatabasen.

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

I följande exempel används join … in … on … equals … -satsen till join två sekvenser baserat på ett specifikt värde:

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

Föregående fråga kan uttryckas med hjälp av metodsyntax enligt följande kod:

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

I följande exempel används join … in … on … equals … into … -satsen till join två sekvenser baserat på ett specifikt värde och grupperar de resulterande matchningarna för varje element:

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

Föregående fråga kan uttryckas med hjälp av metodsyntax enligt följande exempel:

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

Utföra inre kopplingar

I relationsdatabastermer skapar ett inre join en resultatuppsättning där varje element i den första samlingen visas en gång för varje matchande element i den andra samlingen. Om ett element i den första samlingen inte har några matchande element visas det inte i resultatuppsättningen. Metoden Join , som anropas av join -satsen i C#, implementerar en inre join. I följande exempel visas hur du utför fyra varianter av en inre join:

  • En enkel inre join som korrelerar element från två datakällor baserat på en enkel nyckel.
  • Ett inre join som korrelerar element från två datakällor baserat på en sammansatt nyckel. Med en sammansatt nyckel, som är en nyckel som består av mer än ett värde, kan du korrelera element baserat på mer än en egenskap.
  • En multipel join där efterföljande join åtgärder läggs till varandra.
  • Ett inre join som implementeras med hjälp av en grupp join.

Enkel nyckel join

Följande exempel matchar Teacher objekt med Deparment objekt vars TeacherId matchar .Teacher Satsen select i C# definierar hur de resulterande objekten ser ut. I följande exempel är de resulterande objekten anonyma typer som består av avdelningsnamnet och namnet på den lärare som leder avdelningen.

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

Du får samma resultat med hjälp av metodsyntaxen Join :

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 lärare som inte är avdelningschefer visas inte i slutresultatet.

Sammansatt nyckel join

I stället för att korrelera element baserat på bara en egenskap kan du använda en sammansatt nyckel för att jämföra element baserat på flera egenskaper. Ange nyckelväljarens funktion för varje samling för att returnera en anonym typ som består av de egenskaper som du vill jämföra. Om du märker egenskaperna måste de ha samma etikett i varje nyckels anonyma typ. Egenskaperna måste också visas i samma ordning.

I följande exempel används en lista med Teacher objekt och en lista över Student objekt för att avgöra vilka lärare som också är elever. Båda dessa typer har egenskaper som representerar för- och efternamn för varje person. De funktioner som skapar join nycklarna från varje listas element returnerar en anonym typ som består av egenskaperna. Åtgärden join jämför dessa sammansatta nycklar för likhet och returnerar par med objekt från varje lista där både förnamnet och familjenamnet matchar.

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

Du kan använda Join metoden, som du ser i följande exempel:

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

Multipel join

Valfritt antal join åtgärder kan läggas till varandra för att utföra flera join. Varje join sats i C# korrelerar en angiven datakälla med resultatet från föregående join.

Den första join satsen matchar studenter och avdelningar baserat på ett Student objekts DepartmentID matchning av ett Department objekts ID. Den returnerar en sekvens med anonyma typer som innehåller Student objektet och Department objektet.

Den andra join satsen korrelerar de anonyma typer som returneras av den första join med Teacher objekt baserat på lärarens ID som matchar avdelningschefens ID. Den returnerar en sekvens av anonyma typer som innehåller elevens namn, avdelningsnamnet och avdelningsledarens namn. Eftersom den här åtgärden är en inre joinreturneras endast de objekt från den första datakällan som har en matchning i den andra datakällan.

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

Motsvarigheten med flera Join metoder använder samma metod med den anonyma typen:

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

Inre join genom att använda grupperade join

I följande exempel visas hur du implementerar ett inre join med hjälp av en grupp join. Listan över Department objekt är gruppansluten till listan över Student objekt baserat på den Department.ID matchande Student.DepartmentID egenskapen. Gruppen join skapar en samling mellanliggande grupper, där varje grupp består av ett Department objekt och en sekvens med matchande Student objekt. Den andra from satsen kombinerar (eller jämnar ut) sekvensen i en längre sekvens. Satsen select anger typ av element i den sista sekvensen. Den typen är en anonym typ som består av elevens namn och det matchande avdelningsnamnet.

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

Samma resultat kan uppnås med hjälp av GroupJoin metoden enligt följande:

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

Resultatet motsvarar den resultatuppsättning som erhålls med hjälp join av -satsen utan into -satsen för att utföra en inre join. Följande kod visar den här motsvarande frågan:

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

För att undvika sammanlänkning kan den enda Join metoden användas enligt beskrivningen här:

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

Utföra grupperade kopplingar

Gruppen join är användbar för att skapa hierarkiska datastrukturer. Den parkopplar varje element från den första samlingen med en uppsättning korrelerade element från den andra samlingen.

Kommentar

Varje element i den första samlingen visas i resultatuppsättningen för en grupp join oavsett om korrelerade element finns i den andra samlingen. Om inga korrelerade element hittas är sekvensen med korrelerade element för det elementet tom. Resultatväljaren har därför åtkomst till varje element i den första samlingen. Detta skiljer sig från resultatväljaren i en icke-grupp join, som inte kan komma åt element från den första samlingen som inte har någon matchning i den andra samlingen.

Varning

Enumerable.GroupJoin har ingen direkt motsvarighet i traditionella relationsdatabastermer. Den här metoden implementerar dock en superuppsättning inre kopplingar och vänster yttre kopplingar. Båda dessa åtgärder kan skrivas i termer av en grupperad join. Mer information finns i Entity Framework Core, GroupJoin.

Det första exemplet i den här artikeln visar hur du utför en grupp join. Det andra exemplet visar hur du använder en grupp join för att skapa XML-element.

Grupp join

I följande exempel utförs en grupp join med objekt av typen Department och Student baseras på den Department.ID matchande Student.DepartmentID egenskapen. Till skillnad från en icke-grupp join, som producerar ett par element för varje matchning, genererar gruppen join bara ett resulterande objekt för varje element i den första samlingen, vilket i det här exemplet är ett Department objekt. Motsvarande element från den andra samlingen, som i det här exemplet är Student objekt, grupperas i en samling. Slutligen skapar resultatväljaren en anonym typ för varje matchning som består av Department.Name och en samling 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}");
    }
}

I exemplet ovan query innehåller variabeln frågan som skapar en lista där varje element är en anonym typ som innehåller institutionens namn och en samling studenter som studerar på den avdelningen.

Motsvarande fråga med metodsyntax visas i följande kod:

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

Gruppera join för att skapa XML

Gruppkopplingar är idealiska för att skapa XML med hjälp av LINQ till XML. Följande exempel liknar föregående exempel, förutom att i stället för att skapa anonyma typer skapar resultatväljarens funktion XML-element som representerar de kopplade objekten.

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

Motsvarande fråga med metodsyntax visas i följande kod:

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

Utföra vänster yttre kopplingar

En vänster yttre join är en join där varje element i den första samlingen returneras, oavsett om det har några korrelerade element i den andra samlingen. Du kan använda LINQ för att utföra en vänster yttre join genom att anropa DefaultIfEmpty metoden på resultatet av en grupp join.

I följande exempel visas hur du använder DefaultIfEmpty metoden på resultatet av en grupp join för att utföra en vänster yttre join.

Det första steget i att skapa en vänster yttre join av två samlingar är att utföra ett inre join med hjälp av en grupp join. (Se Utför inre kopplingar för en förklaring av den här processen.) I det här exemplet är listan med Department objekt inre kopplad till listan över Student objekt baserat på ett Department objekts ID som matchar elevens DepartmentID.

Det andra steget är att inkludera varje element i den första (vänstra) samlingen i resultatuppsättningen även om elementet inte har några matchningar i den högra samlingen. Detta uppnås genom att anropa DefaultIfEmpty varje sekvens med matchande element från gruppen join. I det här exemplet DefaultIfEmpty anropas på varje sekvens med matchande Student objekt. Metoden returnerar en samling som innehåller ett enda standardvärde om sekvensen med matchande Student objekt är tom för alla Department objekt, vilket säkerställer att varje Department objekt representeras i resultatsamlingen.

Kommentar

Standardvärdet för en referenstyp är null. Därför söker exemplet efter en null-referens innan varje element i varje Student samling används.

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

Motsvarande fråga med metodsyntax visas i följande kod:

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

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

Se även