Compartilhar 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 dados System.Collections.Generic.IEnumerable<T>. Fontes de dados baseadas em System.Linq.IQueryProvider usam as fontes de dados System.Linq.IQueryable<T> e as árvores de expressão. As árvores de expressão possuem limitações na sintaxe C# permitida. Além disso, todas as fontes de dados IQueryProvider, como EF Core, podem impor mais restrições. Verifique a documentação da fonte de dados.

A Junção é uma operação importante em consultas que têm como alvo fontes de dados cujas relações entre si não podem ser seguidas diretamente. Na programação orientada a objetos, a união pode significar uma correlação entre objetos que não é modelada, como a direção inversa de um relacionamento unidirecional. Um exemplo de relacionamento unilateral é uma classe Student que possui uma propriedade do tipo Department que representa o principal, mas a classe Department não possui uma propriedade que seja uma coleção de objetos Student. Se você tem uma lista de objetos Department e você quer encontrar todos os clientes em cada cidade, você pode usar uma operação join para encontrá-los.

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

Dois círculos sobrepostos mostrando interna e externa.

Métodos

Nome do método Descrição Sintaxe de expressão de consulta C# Mais informações
Join Une duas sequências com base nas funções de seletor de chave e extrai pares de valores. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Une duas sequências baseadas em funções de seletor de chave e agrupa as correspondências resultantes para cada elemento. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Os exemplos a seguir neste artigo usam fontes de dados comuns para esta área:

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

Cada Student tem um nível de escolaridade, um departamento primário e uma série de pontuações. Um Teacher também tem uma propriedade City que identifica o campus onde o docente ministra aulas. A Department tem um nome e uma referência a um Teacher que atua como chefe do departamento.

O exemplo a seguir usa a cláusula join … in … on … equals … para join duas sequências com base em um 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 cláusula join … in … on … equals … into … para join duas sequências com base em um 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, uma relacionais join interna produz um conjunto de resultados no qual cada elemento da primeira coleção aparece uma vez para todo elemento correspondente na segunda coleção. Se um elemento da primeira coleção não tiver elementos correspondentes, ele não aparecerá no conjunto de resultados. O método Join, que é chamado pela cláusula join no C#, implementa uma join interna. Os exemplos a seguir mostram como executar quatro variações de uma join interna:

  • Uma join interna simples que correlaciona os elementos de duas fontes de dados com base em uma chave simples.
  • Uma join interna simples que correlaciona os 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 que você correlacione os elementos com base em mais de uma propriedade.
  • Uma múltipla join na qual operações join sucessivas são acrescentadas umas às outras.
  • Uma join interna que é implementada por meio de uma junção de grupo join.

Chave única join

O exemplo a seguir combina Teacher objetos com Deparment objetos cujo TeacherId corresponde a esse Teacher. A cláusula select 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 método 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}");
}

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

Chave composta join

Em vez de correlacionar os 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 de seletor 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 devem ter o mesmo rótulo no tipo anônimo cada chave. As propriedades também devem aparecer na mesma ordem.

O exemplo a seguir usa uma lista de objetos Teacher e uma lista de objetos Student para determinar quais professores também são alunos. Ambos os tipos possuem propriedades que representam o nome e o sobrenome de cada pessoa. As funções que criam as chaves join dos elementos de cada lista retornam um tipo anônimo que consiste nas propriedades. A operação join compara essas chaves compostas quanto à igualdade e retorna pares de objetos de cada lista em que o nome e o sobrenome correspondem.

// 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 método Join, 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últiplo join

Qualquer quantidade de operações join pode ser acrescentado uma a outra para realizar uma join múltipla. Cada cláusula join em C# correlaciona uma fonte de dados especificada com os resultados da join anterior.

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

A segunda cláusula join correlaciona os tipos anônimos retornados pelo primeiro join com objetos Teacher baseados na ID desse professor que correspondem à ID do chefe do 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 esta é uma operação interna, join apenas 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 vários métodos 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}".""");
}

join interna usando join agrupada

O exemplo a seguir mostra como implementar uma join interna usando um grupo join. A lista de objetos Department é unida ao grupo à lista de objetos Student com base na Department.ID correspondência com a propriedade Student.DepartmentID. O grupo join cria uma coleção de grupos intermediários, em que cada grupo é composto por um objeto Department e uma sequência de objetos Student correspondentes. A segunda cláusula from combina (ou nivela) esta sequência de sequências em uma sequência mais longa. A cláusula select especifica o tipo de elementos na sequência final. Esse 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 obtidos usando o método GroupJoin da seguinte forma:

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 que seria obtido usando a cláusula join sem a cláusula into para realizar uma join interna. O código a seguir demonstra esta 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 Join único pode ser utilizado conforme 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 a produção de estruturas de dados hierárquicos. Ela combina cada elemento da primeira coleção com um conjunto de elementos correlacionados da segunda coleção.

Observação

Cada elemento da primeira coleção aparece no conjunto de resultados de uma junção de grupo, join independentemente de se os elementos correlacionados encontram-se na segunda coleção. Caso nenhum elemento correlacionado seja encontrado, a sequência de elementos correlacionados desse elemento ficará vazia. O seletor de resultado, portanto, tem acesso a todos os elementos da primeira coleção. Isso difere do seletor de resultado de um não é de grupo, join que não pode acessar os 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 relacionais tradicionais. No entanto, esse método implementa um superconjunto de junções internas e junções externas à esquerda. Ambas as operações podem ser gravadas em termos de uma junção agrupada join. Para obter mais informações, veja Entity Framework Core, GroupJoin.

O primeiro exemplo neste artigo mostra como executar uma junção de grupo join. O segundo exemplo mostra como usar uma junção de grupo join para criar elementos XML.

Agrupar join

O exemplo a seguir realiza uma junção de grupo join de objetos do tipo Department e Student com base em Department.ID correspondente à Student.DepartmentID propriedade. Ao contrário de uma junção que não é de grupo join, que produziria um par de elementos para cada correspondência, a junção de 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 objetos Student, são agrupados em uma coleção. Por fim, a função de seletor de resultado cria um tipo anônimo para cada correspondência que consiste em Department.Name e em uma coleção de objetos Student.

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, a variável query contém a consulta que cria uma lista onde cada elemento é do tipo anônimo que contém o nome do departamento e uma coleção de alunos que estudam naquele departamento.

A consulta equivalente usando a sintaxe do 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 um XML

As junções de grupo são ideais para a criação de XML usando o LINQ to XML. O exemplo a seguir é semelhante ao exemplo anterior, exceto que em vez de criar tipos anônimos, a função de seletor de resultado 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 a sintaxe do 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 esquerdas

Uma junção externa esquerda join é uma join junção em que cada elemento da primeira coleção é retornado, mesmo que ele tenha elementos correlacionados na segunda coleção. É possível usar o LINQ para executar uma junção join externa esquerda chamando o método DefaultIfEmpty nos resultados de uma junção de grupo join.

O exemplo a seguir demonstra como usar o método DefaultIfEmpty nos resultados de uma junção de grupo join para executar uma junção externa esquerda join.

A primeira etapa da produção de uma junção externa esquerda join de duas coleções é executar uma junção interna join usando uma junção de grupo join. (Consulte Executar junções internas para obter uma explicação desse processo.) Nesse exemplo, a lista de Department objetos é unida internamente à lista de Student objetos com base no ID de um Department objeto que corresponde ao ID 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 nenhuma correspondência na coleção direita. Isso é feito chamando DefaultIfEmpty em cada sequência de elementos correspondentes da junção de grupo join. Neste exemplo, DefaultIfEmpty é chamado em cada sequência de objetos Student correspondentes. O método retorna uma coleção que contém um único valor padrão se a sequência de objetos Student correspondentes estiver vazia para qualquer objeto Department, garantindo que cada objeto Department seja representado na coleção de resultados.

Observação

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

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

Confira também