Share via


Join Åtgärder i LINQ

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

Joining är en viktig åtgärd i frågor som riktar sig mot 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 elever på varje avdelning kan du använda en kopplingsåtgärd för att hitta dem.

Kopplingsmetoderna 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 Transact-SQL andra kopplingsoperatorer än equals, till exempel operatorn less than .) I relationsdatabastermer Join implementerar en inre koppling, en typ av koppling 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 koppling är en koppling 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 koppling eller en vänster yttre koppling.

Två överlappande cirklar som visar inre/ Yttre.

Metoder

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

Queryable.Join
GroupJoin Joins 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

I följande exempel i den här artikeln används vanliga datakällor för det här området:

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

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.

I följande exempel används join … in … on … equals … -satsen för att koppla 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 för att koppla två sekvenser baserat på ett specifikt värde och gruppera 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 en inre koppling 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 koppling. I följande exempel visas hur du utför fyra varianter av en inre koppling:

  • En enkel inre koppling som korrelerar element från två datakällor baserat på en enkel nyckel.
  • En inre koppling 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 flerkoppling där efterföljande kopplingsåtgärder läggs till varandra.
  • En inre koppling som implementeras med hjälp av en gruppkoppling.

Enkel nyckelkoppling

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 nyckelkoppling

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. Funktionerna som skapar kopplingsnycklarna från varje listas element returnerar en anonym typ som består av egenskaperna. Kopplingsåtgärden 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);
}

Flera kopplingar

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

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 kopplingen 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 koppling returneras 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 koppling med grupperad koppling

I följande exempel visas hur du implementerar en inre koppling med hjälp av en gruppkoppling. Listan över Department objekt är gruppansluten till listan över Student objekt baserat på den Department.ID matchande Student.DepartmentID egenskapen. Gruppkopplingen 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 koppling. 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

Gruppkopplingen ä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 gruppkoppling 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-gruppkoppling, 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 koppling. Mer information finns i Entity Framework Core, GroupJoin.

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

Gruppkoppling

I följande exempel utförs en gruppkoppling av objekt av typen Department och Student baserat på den Department.ID matchande Student.DepartmentID egenskapen. Till skillnad från en icke-gruppkoppling, som producerar ett par element för varje matchning, genererar gruppkopplingen 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}");
    }
}

Gruppkoppling 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 koppling är en koppling där varje element i den första samlingen returneras, oavsett om den har några korrelerade element i den andra samlingen. Du kan använda LINQ för att utföra en vänster yttre koppling genom att anropa DefaultIfEmpty metoden på resultatet av en gruppkoppling.

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

Det första steget i att skapa en vänster yttre koppling av två samlingar är att utföra en inre koppling med hjälp av en gruppkoppling. (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 gruppkopplingen. 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, 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}");
}

Se även