Operaciones Join en LINQ

Una join de dos orígenes de datos es la asociación de objetos de un origen de datos con los objetos que comparten un atributo común en otro origen de datos.

Importante

Estos ejemplos usan un origen de datos System.Collections.Generic.IEnumerable<T>. Los orígenes de datos basados en System.Linq.IQueryProvider usanSystem.Linq.IQueryable<T> orígenes de datos y árboles de expresión . Los árboles de expresión tienen limitaciones en la sintaxis de C# permitida. Además, cada origen de datos IQueryProvider, como EF Core puede imponer más restricciones. Compruebe la documentación del origen de datos.

La combinación es una operación importante en las consultas destinadas a orígenes de datos cuyas relaciones entre sí no se puede seguir directamente. En la programación orientada a objetos, una combinación podría significar una correlación entre objetos que no está modelada, como el sentido contrario de una relación unidireccional. Un ejemplo de una relación unidireccional es una clase Student que tiene una propiedad de tipo Department que representa su área de especialización, pero la clase Department no tiene una propiedad que sea una colección de objetos Student. Si tiene una lista de objetos Department y quiere encontrar todos los alumnos de cada departamento, podría usar una operación de join para encontrarlos.

Los métodos de join que se han proporcionado en el marco de LINQ son Join y GroupJoin. Estos métodos efectúan combinaciones de igualdad, o combinaciones que hacen corresponder dos orígenes de datos en función de la igualdad de sus claves. (A modo de comparación, Transact-SQL admite operadores de join distintos de equals, por ejemplo, el operador less than). En términos de bases de datos relacionales, Join implementa una join interna, un tipo de join en la que solo se devuelven aquellos objetos que tienen una coincidencia en el otro conjunto de datos. El método GroupJoin no tiene equivalente directo en términos de bases de datos relacionales; pero implementa un superconjunto de combinaciones internas y combinaciones externas izquierdas. Una join externa izquierda es una join que devuelve cada elemento del primer origen de datos (izquierda), aunque no tenga elementos correlacionados en el otro origen de datos.

En la ilustración siguiente se muestra una vista conceptual de dos conjuntos y los elementos de esos conjuntos que se incluyen en una join interna o en una join externa izquierda.

Dos círculos superpuestos en los que se muestra el interior y el exterior.

Métodos

Nombre del método Descripción Sintaxis de la expresión de consulta de C# Más información
Join Combina dos secuencias según las funciones de selector de claves y extrae pares de valores. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Combina dos secuencias según las funciones de selector de claves y agrupa los resultados coincidentes para cada elemento. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Nota:

En los ejemplos siguientes de este artículo se usan los orígenes de datos comunes para esta área.
Cada Student tiene un nivel académico, un departamento principal y una serie de puntuaciones. Un Teacher también tiene una propiedad City que identifica el campus donde el profesor imparte clases. Un Department tiene un nombre y una referencia a un Teacher que actúa como jefe del departamento.
Puede encontrar el conjunto de datos de ejemplo en el repositorio de origen.

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

En el ejemplo siguiente se usa la cláusula join … in … on … equals … para join dos secuencias en función de un 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}");
}

La consulta anterior se puede expresar mediante la sintaxis del método, como se muestra en el código siguiente:

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

En el ejemplo siguiente se usa la cláusula join … in … on … equals … into … para join dos secuencias en función de un valor específico y se agrupan las coincidencias resultantes de 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}");
    }
}

La consulta anterior se puede expresar mediante la sintaxis del método, como se muestra en el ejemplo siguiente:

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

Realizar combinaciones internas

En términos de la base de datos relacional, una join interna genera un conjunto de resultados en el que cada elemento de la primera colección aparece una vez para cada elemento coincidente en la segunda colección. Si un elemento de la primera colección no tiene ningún elemento coincidente, no aparece en el conjunto de resultados. El método Join, que se llama mediante la cláusula join de C#, implementa una join interna. En los siguientes ejemplos se muestra cómo realizar cuatro variaciones de una join interna:

  • Una join interna simple que correlaciona elementos de dos orígenes de datos según una clave simple.
  • Una join interna que correlaciona elementos de dos orígenes de datos según una clave compuesta. Una clave compuesta, que es una clave formada por más de un valor, permite correlacionar elementos en función de más de una propiedad.
  • Una join múltiple en la que las sucesivas operaciones de join se anexan entre sí.
  • Una join interna que se implementa mediante una join agrupada.

join de clave única

En el ejemplo siguiente se comparan objetos Teacher con objetos Deparment cuyos elementos TeacherId coinciden con esos objetos Teacher. La cláusula select de C# define el aspecto que tendrán los objetos resultantes. En el ejemplo siguiente, los objetos resultantes son tipos anónimos que constan del nombre del departamento y el nombre del profesor que dirige el 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}");
}

Se obtienen los mismos resultados mediante la sintaxis del 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}");
}

Los profesores que no son jefes de departamento no aparecen en los resultados finales.

join de clave compuesta

En lugar de correlacionar elementos en función de una sola propiedad, puede usar una clave compuesta para comparar elementos según varias propiedades. Especifique la función del selector de claves de cada colección para que devuelva un tipo anónimo que conste de las propiedades que quiere comparar. Si etiqueta las propiedades, deben tener la misma etiqueta de tipo anónimo en cada clave. Las propiedades también deben aparecer en el mismo orden.

En el ejemplo siguiente se usa una lista de objetos Teacher y una lista de objetos Student para determinar qué profesores son también alumnos. Ambos tipos tienen propiedades que representan el nombre y el apellido de cada persona. Las funciones que crean las claves de join de los elementos de cada lista devuelven un tipo anónimo formado por las propiedades. La operación de join compara la igualdad de estas claves compuestas y devuelve pares de objetos de cada lista en los que el nombre y el apellido coinciden.

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

Puede usar el método Join, tal y como se muestra en el siguiente ejemplo:

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

join múltiple

Se puede anexar cualquier número de operaciones de join entre sí para realizar una join múltiple. Cada cláusula join de C# correlaciona un origen de datos especificado con los resultados de la join anterior.

La primera cláusula join correlaciona los alumnos y los departamentos en función de la coincidencia del valor DepartmentID de un objeto Student con el valor ID de un objeto Department. Devuelve una secuencia de tipos anónimos que contienen los objetos Student y Department.

La segunda cláusula join correlaciona los tipos anónimos devueltos por la primera join con objetos Teacher basados en el ID de ese profesor que coinciden con el ID del jefe de departamento. Devuelve una secuencia de tipos anónimos que contienen el nombre del alumno, el nombre del departamento y el nombre del jefe del departamento. Dado que esta operación es una join interna, solo se devuelven los objetos del primer origen de datos que tienen una correspondencia en el segundo origen de datos.

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

El equivalente que usa varios métodos Join usa el mismo enfoque con el 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 mediante join agrupada

El ejemplo siguiente muestra cómo implementar una join interna mediante una join agrupada. La lista de objetos Department forma una combinación agrupada con la lista de objetos Student según el Department.ID que coincide con la propiedad Student.DepartmentID. La join agrupada crea una colección de grupos intermedios, donde cada grupo consta de un objeto Department y una secuencia de objetos Student coincidentes. La segunda cláusula from combina (o acopla) esta secuencia de secuencias en una secuencia más larga. La cláusula select especifica el tipo de elementos de la secuencia final. Ese tipo es un tipo anónimo que consta del nombre del alumno y del nombre del departamento coincidente.

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

Los mismos resultados se pueden lograr mediante el método GroupJoin, como se indica a continuación:

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

El resultado es equivalente al conjunto de resultados obtenido con la cláusula join sin la cláusula into para realizar una join interna. En el código siguiente se muestra 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 el encadenamiento, el método Join único se puede usar como se muestra aquí:

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

Realizar combinaciones agrupadas

La join agrupada resulta útil para generar estructuras de datos jerárquicas. Empareja cada elemento de la primera colección con un conjunto de elementos correlacionados de la segunda colección.

Nota:

Cada elemento de la primera colección aparece en el conjunto de resultados de una join agrupada, independientemente de si se encuentran elementos correlacionados en la segunda colección. En el caso de que no se encuentren elementos correlacionados, la secuencia de elementos correlacionados para ese elemento estaría vacía. Por consiguiente, el selector de resultados tiene acceso a cada uno de los elementos de la primera colección. Esto difiere del selector de resultados en una join no agrupada, que no puede acceder a los elementos de la primera colección que no tienen ninguna coincidencia en la segunda colección.

Advertencia

Enumerable.GroupJoin no tiene ningún equivalente directo en términos de base de datos relacional tradicional. Sin embargo, este método implementa un superconjunto de combinaciones internas y combinaciones externas izquierdas. Ambas operaciones se pueden escribir en términos de una join agrupada. Para más información, consulte Entity Framework Core, GroupJoin.

En el primer ejemplo de este artículo se muestra cómo realizar una join agrupada. En el segundo ejemplo se muestra cómo usar una join agrupada para crear elementos XML.

join agrupada

En el ejemplo siguiente se realiza una join agrupada de objetos de tipo Department y Student basada en la coincidencia de Department.ID con la propiedad Student.DepartmentID. A diferencia de una join no agrupada, que genera un par de elementos para cada coincidencia, la join agrupada produce un único objeto resultante para cada elemento de la primera colección, que en este ejemplo es un objeto Department. Los elementos correspondientes de la segunda colección, que en este ejemplo son objetos Student, se agrupan en una colección. Por último, la función de selector de resultados crea un tipo anónimo para cada coincidencia formada por Department.Name y una colección 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}");
    }
}

En el ejemplo anterior, la variable query contiene la consulta que crea una lista donde cada elemento es un tipo anónimo que contiene el nombre del departamento y una colección de alumnos que estudian en ese departamento.

La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:

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

join agrupada para crear XML

Las combinaciones agrupadas resultan ideales para crear XML con LINQ to XML. El siguiente ejemplo es similar al anterior, pero en lugar de crear tipos anónimos, la función de selector de resultados crea elementos XML que representan los objetos combinados.

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

La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:

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

Realizar operaciones de combinación externa izquierda

Una join externa izquierda es una join en la que se devuelve cada elemento de la primera colección, independientemente de si tiene elementos correlacionados en la segunda colección. Puede usar LINQ para realizar una join externa izquierda llamando al método DefaultIfEmpty en los resultados de una join agrupada.

En el ejemplo siguiente se muestra cómo usar el método DefaultIfEmpty en los resultados de una join agrupada para realizar una join externa izquierda.

El primer paso para generar una join externa izquierda de dos colecciones consiste en realizar una join interna usando una join agrupada. (Vea Realizar combinaciones internas para obtener una explicación de este proceso). En este ejemplo, la lista de objetos Department está unida mediante combinación interna a la lista de objetos Student basándose en el id. de un objeto Department que coincide con el elemento DepartmentID.

El segundo paso consiste en incluir cada elemento de la primera colección (izquierda) en el conjunto de resultados, incluso cuando no haya coincidencias en la colección derecha. Esto se realiza llamando a DefaultIfEmpty en cada secuencia de elementos coincidentes de la join agrupada. En este ejemplo, se llama a DefaultIfEmpty en cada secuencia de objetos Student coincidentes. El método devuelve una colección que contiene un único, valor predeterminado si la secuencia de objetos Student coincidentes está vacía para cualquier objeto Department, con lo que cada objeto Department se representa en la colección de resultados.

Nota:

El valor predeterminado para un tipo de referencia es null; por consiguiente, el ejemplo busca una referencia NULL antes de tener acceso a cada elemento de cada colección de 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}");
}

La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:

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 también