Compartir a través de


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.

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 desea encontrar todos los alumnos de cada departamento, puede usar una operación de unión para encontrarlos.

Los métodos de combinación proporcionados en el marco LINQ se 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. (En comparación, Transact-SQL admite operadores de combinación distintos de equals, por ejemplo, el operador less than). En términos de base de datos relacionales, Join implementa una combinación interna, un tipo de combinación en el que solo se devuelven los 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 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 siguiente ilustración se muestra una vista conceptual de dos conjuntos y los elementos de esos conjuntos que se incluyen en una unión interna o una unió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 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 combinar dos secuencias basadas en 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 basadas en un valor específico y agrupa las coincidencias 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}");
    }
}

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 base de datos relacionales, una combinación interna genera un conjunto de resultados en el que cada elemento de la primera colección aparece una vez por cada elemento coincidente de 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 unión interna:

  • Combinación interna simple que correlaciona elementos de dos orígenes de datos en función de una clave simple.
  • Unión interna que correlaciona elementos de dos fuentes de datos basándose en una clave compuesta de . 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 Department 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 a partir de los elementos de cada lista devuelven un tipo anónimo que consta de las propiedades. La operación de unión compara estas claves compuestas para determinar la igualdad y devuelve pares de objetos de cada lista donde coinciden tanto el nombre como el apellido.

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

Cualquier número de operaciones de combinación se puede anexar 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 Student de un objeto DepartmentID con el valor Department de un objeto ID. 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 unión con objetos Teacher, basándose en que el identificador del profesor coincida con el identificador 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. Dado que esta operación es una combinación interna, solo se devuelven los objetos del primer origen de datos que tienen una coincidencia 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

En el ejemplo siguiente se muestra cómo implementar una combinación interna mediante una combinación de grupo. 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 de grupo 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 unión de grupo es ú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 de grupo, independientemente de si los elementos correlacionados se encuentran 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 que no es de grupo, que no puede tener acceso 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 unión a un grupo. En el segundo ejemplo se muestra cómo usar una combinación de grupo para crear elementos XML.

Unirse a un grupo

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

Unirse a un grupo 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, departmentList) => new { student, subgroup = departmentList })
    .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