Operaciones Join en LINQ

Una combinación 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.

La combinación Join es una operación importante en las consultas destinadas a orígenes de datos cuyas relaciones entre sí no se pueden 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 en cada departamento, podría usar una operación de combinación para encontrarlos.

Los métodos de combinación 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. (Para comparar, Transact-SQL admite otros operadores de combinación aparte de equals, por ejemplo, el operador less than). En términos de base de datos relacional, Join implementa una combinación interna, un tipo de combinación en la que solo se devuelven los objetos que tienen una correspondencia 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 combinación externa izquierda es una combinación 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 combinación interna o en una combinación 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 mediante Join 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 mediante Join 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

En los ejemplos siguientes de este artículo se usan los orígenes de datos comunes 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 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.

En el ejemplo siguiente se usa la cláusula join … in … on … equals … para combinar 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 combinar 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 combinación 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 combinación interna. En los ejemplos siguientes se muestra cómo realizar cuatro variaciones de una combinación interna:

  • Una combinación interna simple que correlaciona elementos de dos orígenes de datos según una clave simple.
  • Una combinación 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 combinación múltiple en la que las sucesivas operaciones de combinación se anexan entre sí.
  • Una combinación interna que se implementa mediante una combinación agrupada.

Combinación 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.

Combinación 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 combinación de los elementos de cada lista devuelven un tipo anónimo formado por las propiedades. La operación de combinación 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);
}

Combinación múltiple

Se puede anexar cualquier número de operaciones de combinación entre sí para realizar una combinación múltiple. Cada cláusula join de C# correlaciona un origen de datos especificado con los resultados de la combinación 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 join cláusula correlaciona los tipos anónimos devueltos por la primera combinación con objetos Teacher en función del id. del profesor que coincide con el id. del jefe del 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. Como esta operación es una combinación 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}".""");
}

Combinación interna mediante combinación agrupada

El ejemplo siguiente muestra cómo implementar una combinación interna mediante una combinación 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 combinación 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 mediante la cláusula join sin la cláusula into para realizar una combinación 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 combinación 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 combinación 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 combinación 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 combinación 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 combinación agrupada. En el segundo ejemplo se muestra cómo usar una combinación agrupada para crear elementos XML.

Combinación agrupada

En el ejemplo siguiente se realiza una combinación agrupada de objetos de tipo Department y Student basada en la coincidencia de Deoartment.ID con la propiedad Student.DepartmentID. A diferencia de una combinación no agrupada, que genera un par de elementos para cada coincidencia, la combinación 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}");
    }
}

Combinación 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 combinación externa izquierda es una combinación 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 combinación externa izquierda llamando al método DefaultIfEmpty en los resultados de una combinación agrupada.

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

El primer paso para generar una combinación externa izquierda de dos colecciones consiste en realizar una combinación interna usando una combinación 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 combinación 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, 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}");
}

Consulte también