Partilhar via


Join Operações em LINQ

Uma join de duas fontes de dados é a associação de objetos em uma fonte de dados com objetos que compartilham um atributo comum em outra fonte de dados.

Importante

Esses exemplos usam uma fonte de System.Collections.Generic.IEnumerable<T> dados. Fontes de dados baseadas em System.Linq.IQueryProvider fontes de dados de uso System.Linq.IQueryable<T> e árvores de expressão. As árvores de expressão têm limitações na sintaxe C# permitida. Além disso, cada IQueryProvider fonte de dados, como o EF Core , pode impor mais restrições. Verifique a documentação da sua fonte de dados.

A junção é uma operação importante em consultas que visam fontes de dados cujas relações entre si não podem ser seguidas diretamente. Na programação orientada a objetos, a junção pode significar uma correlação entre objetos que não é modelada, como a direção para trás de uma relação unidirecional. Um exemplo de uma relação unidirecional é uma Student classe que tem uma propriedade do tipo Department que representa a principal, mas a Department classe não tem uma propriedade que é uma coleção de Student objetos. Se você tiver uma lista de Department objetos e quiser encontrar todos os alunos em cada departamento, você pode usar uma join operação para encontrá-los.

Os join métodos fornecidos na estrutura LINQ são Join e GroupJoin. Esses métodos executam equijunções ou junções que correspondem a duas fontes de dados com base na igualdade de suas chaves. (Para comparação, o Transact-SQL oferece suporte a join operadores diferentes de equals, por exemplo, o less than operador.) Em termos de banco de dados relacional, Join implementa um , um jointipo interno no qual apenas os objetos que têm uma correspondência no outro conjunto de join dados são retornados. O GroupJoin método não tem equivalente direto em termos de banco de dados relacional, mas implementa um superconjunto de junções internas e externas esquerdas. Um externo join esquerdo é um join que retorna cada elemento da primeira fonte de dados (esquerda), mesmo que não tenha elementos correlacionados na outra fonte de dados.

A ilustração a seguir mostra uma visão conceitual de dois conjuntos e os elementos dentro desses conjuntos que estão incluídos em um externo joininterno join ou esquerdo.

Dois círculos sobrepostos mostrando interior/ exterior.

Métodos

Nome do método Description Sintaxe da expressão de consulta C# Mais Informações
Join Junta duas sequências baseadas em funções de seletor de teclas e extrai pares de valores. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Junta duas sequências com base nas funções do seletor de teclas e agrupa as correspondências resultantes para cada elemento. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Nota

Os exemplos a seguir neste artigo usam as fontes de dados comuns para essa área.
Cada Student um tem um nível de nota, um departamento primário e uma série de pontuações. A Teacher também tem uma City propriedade que identifica o campus onde o professor tem aulas. A Department tem um nome, e uma referência a um Teacher que serve como chefe de departamento.
Você pode encontrar o conjunto de dados de exemplo no repositório de origem.

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

O exemplo a seguir usa a join … in … on … equals … cláusula para join duas sequências com base no valor específico:

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

A consulta anterior pode ser expressa usando a sintaxe do método, conforme mostrado no código a seguir:

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

O exemplo a seguir usa a join … in … on … equals … into … cláusula para join duas sequências com base em valor específico e agrupa as correspondências resultantes para cada elemento:

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

A consulta anterior pode ser expressa usando a sintaxe do método, conforme mostrado no exemplo a seguir:

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

Executar junções internas

Em termos de banco de dados relacional, um interno join produz um conjunto de resultados no qual cada elemento da primeira coleção aparece uma vez para cada elemento correspondente na segunda coleção. Se um elemento na primeira coleção não tiver elementos correspondentes, ele não aparecerá no conjunto de resultados. O Join método, que é chamado pela join cláusula em C#, implementa um .join Os exemplos a seguir mostram como executar quatro variações de um interior join:

  • Um interno join simples que correlaciona elementos de duas fontes de dados com base em uma chave simples.
  • Um interno join que correlaciona elementos de duas fontes de dados com base em uma chave composta . Uma chave composta, que é uma chave que consiste em mais de um valor, permite correlacionar elementos com base em mais de uma propriedade.
  • Um múltiplo join no qual operações sucessivas join são anexadas umas às outras.
  • Um interno join que é implementado usando um grupo join.

Chave única join

O exemplo a seguir corresponde a Teacher objetos com Deparment objetos cujos TeacherId correspondem a .Teacher A select cláusula em C# define a aparência dos objetos resultantes. No exemplo a seguir, os objetos resultantes são tipos anônimos que consistem no nome do departamento e no nome do professor que lidera o departamento.

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

Você obtém os mesmos resultados usando a sintaxe do Join método:

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

Os professores que não são chefes de departamento não aparecem nos resultados finais.

Chave composta join

Em vez de correlacionar elementos com base em apenas uma propriedade, você pode usar uma chave composta para comparar elementos com base em várias propriedades. Especifique a função seletora de chave para cada coleção para retornar um tipo anônimo que consiste nas propriedades que você deseja comparar. Se você rotular as propriedades, elas deverão ter o mesmo rótulo no tipo anônimo de cada chave. As propriedades também devem aparecer na mesma ordem.

O exemplo a seguir usa uma lista de Teacher objetos e uma lista de Student objetos para determinar quais professores também são alunos. Ambos os tipos têm propriedades que representam o primeiro nome e o nome de família de cada pessoa. As funções que criam as chaves a join partir dos elementos de cada lista retornam um tipo anônimo que consiste nas propriedades. A join operação compara essas chaves compostas para igualdade e retorna pares de objetos de cada lista onde o primeiro nome e o nome da família coincidem.

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

Você pode usar o Join método, conforme mostrado no exemplo a seguir:

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

Múltiplos join

Qualquer número de operações pode ser anexado join entre si para executar um múltiplo join. Cada join cláusula em C# correlaciona uma fonte de dados especificada com os resultados do arquivo join.

A primeira join cláusula corresponde a alunos e departamentos com base na correspondência Department de DepartmentID um Student objeto com o ID. Ele retorna uma sequência de tipos anônimos que contêm o objeto e Department o Student objeto.

A segunda join cláusula correlaciona os tipos anônimos retornados pelo primeiro join com Teacher objetos baseados no ID do professor correspondente ao ID do chefe de departamento. Ele retorna uma sequência de tipos anônimos que contêm o nome do aluno, o nome do departamento e o nome do líder do departamento. Como essa operação é interna join, somente os objetos da primeira fonte de dados que têm uma correspondência na segunda fonte de dados são retornados.

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

O equivalente usando o método múltiplo Join usa a mesma abordagem com o tipo anônimo:

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

Interior join usando agrupado join

O exemplo a seguir mostra como implementar um interno join usando um grupo join. A lista de objetos é unida Department ao grupo à lista de Student objetos com base na Department.ID correspondência da Student.DepartmentID propriedade. O grupo join cria uma coleção de grupos intermediários, onde cada grupo consiste em um Department objeto e uma sequência de objetos correspondentes Student . A segunda from cláusula combina (ou nivela) esta sequência de sequências numa sequência mais longa. A select cláusula especifica o tipo de elementos na sequência final. Esse tipo é um tipo anônimo que consiste no nome do aluno e no nome do departamento correspondente.

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

Os mesmos resultados podem ser alcançados usando GroupJoin o método, como se segue:

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

O resultado é equivalente ao conjunto de resultados obtido usando a join cláusula sem a into cláusula para executar um .join O código a seguir demonstra essa consulta equivalente:

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

Para evitar o encadeamento, o método único Join pode ser usado como apresentado aqui:

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

Executar junções agrupadas

O grupo join é útil para produzir estruturas de dados hierárquicas. Emparelha cada elemento da primeira coleção com um conjunto de elementos correlacionados da segunda coleção.

Nota

Cada elemento da primeira coleção aparece no conjunto de resultados de um grupo join , independentemente de os elementos correlacionados serem encontrados na segunda coleção. No caso em que nenhum elemento correlacionado é encontrado, a sequência de elementos correlacionados para esse elemento é vazia. O seletor de resultados tem, portanto, acesso a todos os elementos da primeira coleção. Isso difere do seletor de resultados em um não-grupo join, que não pode acessar elementos da primeira coleção que não têm correspondência na segunda coleção.

Aviso

Enumerable.GroupJoin não tem equivalente direto em termos de banco de dados relacional tradicional. No entanto, esse método implementa um superconjunto de junções internas e externas esquerdas. Ambas as operações podem ser escritas em termos de um agrupamento join. Para obter mais informações, consulte Entity Framework Core, GroupJoin.

O primeiro exemplo neste artigo mostra como executar um grupo join. O segundo exemplo mostra como usar um grupo join para criar elementos XML.

Grupo join

O exemplo a seguir executa um grupo join de objetos do tipo Department e Student com base na Department.ID correspondência da Student.DepartmentID propriedade. Ao contrário de um não-grupo join, que produz um par de elementos para cada correspondência, o grupo join produz apenas um objeto resultante para cada elemento da primeira coleção, que neste exemplo é um Department objeto. Os elementos correspondentes da segunda coleção, que neste exemplo são Student objetos, são agrupados em uma coleção. Finalmente, a função seletor de resultados cria um tipo anônimo para cada correspondência que consiste em Department.Name e uma coleção de Student objetos.

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

No exemplo acima, query a variável contém a consulta que cria uma lista onde cada elemento é um tipo anônimo que contém o nome do departamento e uma coleção de alunos que estudam nesse departamento.

A consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Grupo join para criar XML

As junções de grupo são ideais para criar XML usando LINQ to XML. O exemplo a seguir é semelhante ao exemplo anterior, exceto que, em vez de criar tipos anônimos, a função seletor de resultados cria elementos XML que representam os objetos associados.

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 consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Executar junções externas à esquerda

Um exterior join esquerdo é um join elemento no qual cada elemento da primeira coleção é retornado, independentemente de ter ou não elementos correlacionados na segunda coleção. Você pode usar o LINQ para executar um externo join esquerdo chamando o DefaultIfEmpty método nos resultados de um grupo join.

O exemplo a seguir demonstra como usar o DefaultIfEmpty método nos resultados de um grupo join para executar um externo joinesquerdo .

O primeiro passo para produzir um exterior join esquerdo de duas coleções é executar um interior join usando um grupo join. (Ver Execute junções internas para uma explicação desse processo.) Neste exemplo, a lista de Department objetos é associada à lista de Student objetos com base na ID de um Department objeto que corresponde à do aluno DepartmentID.

A segunda etapa é incluir cada elemento da primeira coleção (esquerda) no conjunto de resultados, mesmo que esse elemento não tenha correspondências na coleção correta. Isso é feito chamando DefaultIfEmpty cada sequência de elementos correspondentes do grupo join. Neste exemplo, DefaultIfEmpty é chamado em cada sequência de objetos correspondentes Student . O método retorna uma coleção que contém um único valor padrão se a sequência de objetos correspondentes Student estiver vazia para qualquer Department objeto, garantindo que cada Department objeto seja representado na coleção de resultados.

Nota

O valor padrão para um tipo de referência é null; portanto, o exemplo verifica se há uma referência nula antes de acessar cada elemento de cada Student coleção.

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 consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Consulte também