CodeGym /Cursos /C# SELF /Unir colecciones con join

Unir colecciones con join ( Join)

C# SELF
Nivel 33 , Lección 0
Disponible

1. Introducción

A veces tenemos que currar con dos colecciones distintas que están relacionadas por alguna característica común. Por ejemplo, tenemos una lista de pedidos y otra colección de clientes. ¿Cómo saber qué pedido pertenece a qué cliente? Para eso, ambas colecciones tienen que estar conectadas por un identificador común — por ejemplo, por userId.

En bases de datos relacionales, esta tarea la resuelve la operación JOIN, que une filas de diferentes tablas por claves coincidentes. En LINQ hay un operador igual — también se llama Join.

Imagínate dos tablas: en la primera — una lista de lectores de biblioteca con sus identificadores, y en la segunda — una lista de pedidos de libros, donde cada pedido tiene el id del lector. Con join "pegamos" estas tablas por el id, y al final obtenemos pares «lector + su pedido».

Por cierto, si pensabas que join es solo para "bases de datos", ¡te has perdido mucho! En programación, unir colecciones es algo muy común, sobre todo cuando curras con sistemas externos o archivos estructurados (JSON, XML y esas tablas de Excel de contabilidad).

2. Signatura del método Join y cómo funciona

Así es como se ve el método Join:


public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                 // primera colección (externa)
    IEnumerable<TInner> inner,                      // segunda colección (interna)
    Func<TOuter, TKey> outerKeySelector,            // cómo sacar la clave del elemento externo
    Func<TInner, TKey> innerKeySelector,            // cómo sacar la clave del interno
    Func<TOuter, TInner, TResult> resultSelector)   // función para generar el resultado (nuevo elemento)

A primera vista parece un poco tocho, pero ahora lo desglosamos.
¿Cómo funciona?
Para cada elemento de la primera colección (outer), LINQ busca los elementos que coincidan (por clave) en la segunda (inner). Cuando las claves coinciden, se llama a resultSelector y el resultado se mete en la colección final.

3. Ejemplo: Unimos clientes y sus pedidos

Vamos a crear un par de clases y colecciones. Esta idea es la continuación lógica de nuestra app de ejemplo (pongamos que es una tiendecita que estamos desarrollando durante el curso).


public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Product { get; set; } = "";
}

// Colección de clientes
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "Vasya" },
    new Customer { Id = 2, Name = "Petya" },
    new Customer { Id = 3, Name = "Masha" },
};

// Colección de pedidos
var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 2, Product = "Libro" },
    new Order { Id = 102, CustomerId = 1, Product = "Bolígrafo" },
    new Order { Id = 103, CustomerId = 2, Product = "Cuaderno" },
    new Order { Id = 104, CustomerId = 3, Product = "Goma" },
};

Ahora queremos sacar todos los clientes con sus pedidos. Por ejemplo, mostrar: "Petya pidió Libro", "Petya pidió Cuaderno", etc.

Aplicamos Join


// Unimos clientes con pedidos por customer.Id y order.CustomerId
var query = customers.Join(
    orders,
    customer => customer.Id,          // Cómo sacar la clave del cliente
    order => order.CustomerId,        // Cómo sacar la clave del pedido
    (customer, order) => new          // Qué hacer con los pares que coinciden (creamos nuevo objeto)
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);

// Mostramos el resultado
foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName} pidió {item.Product}");
}

¿Qué se mostrará?

Vasya pidió Bolígrafo
Petya pidió Libro
Petya pidió Cuaderno
Masha pidió Goma

Ojo: si un cliente tiene varios pedidos, aparecerá varias veces — una por cada pedido. ¡Así es como debe ser!

4. Tabla: Comparación de Join y GroupBy + SelectMany

Operación Resultado Cuándo usar
Join
Lista plana de pares El clásico SQL JOIN. Cada par (coincidencia) — una fila aparte.
GroupBy + SelectMany
Grupos con subcolecciones Quieres obtener "cliente + todos sus pedidos" como estructura "uno a muchos".

Los que empiezan suelen usar Join donde hace falta la estructura «cliente → lista de pedidos». Pero Join funciona fila a fila y no agrupa los datos. Para eso está GroupJoin — de eso hablamos en la próxima lección. O puedes usar GroupBy.

5. Join en Query Syntax (LINQ estilo SQL)

LINQ soporta dos sintaxis: en cadena de métodos (Method Syntax, por ejemplo Join(...)) y la llamada estilo SQL (Query Syntax), que visualmente se parece a las consultas SQL normales.

Para algunos devs — sobre todo los que han currado antes con bases de datos — este estilo puede ser más claro:


var query2 =
    from customer in customers
    join order in orders
        on customer.Id equals order.CustomerId
    select new
    {
        CustomerName = customer.Name,
        Product = order.Product
    };

foreach (var item in query2)
{
    Console.WriteLine($"{item.CustomerName} pidió {item.Product}");
}

Ojo aquí:
En Query Syntax se usa la palabra clave equals — ¡el operador == aquí no funciona!
Es una trampa típica en entrevistas, sobre todo para los que empiezan 😉

6. Detalles importantes y trampas

A veces pasa que no todos los elementos de las colecciones acaban en el resultado final. Esto es porque el método Join implementa lo que se llama «inner join», o sea, solo une los elementos cuyas claves coinciden. Si un cliente no tiene pedidos, simplemente no estará en la lista final. Igual, si hay un pedido con CustomerId que no existe entre los clientes, ese pedido tampoco sale en el resultado.

¿Pero qué hacer si quieres sacar todos los clientes, incluso los que no tienen pedidos? Para eso existe el join "izquierdo", que en LINQ se hace combinando GroupJoin y SelectMany. De eso hablamos en la próxima lección. En el Join clásico, es obligatorio que ambos lados estén presentes — si no, no hay coincidencia y el elemento se queda fuera.

7. Trabajar con varias claves (composite key)

A veces hay que unir colecciones no solo por un campo, sino por varios. Por ejemplo: unir productos y sus ventas por "Código de producto + Año de venta".

En LINQ esto se hace creando objetos anónimos como claves:


var sales = ...; // ventas
var products = ...; // productos

var query = products.Join(
    sales,
    prod => new { prod.Code, prod.Year },
    sale => new { sale.ProductCode, sale.Year },
    (prod, sale) => new { prod.Name, sale.Amount }
);

Es importante que los tipos y nombres de las propiedades en estos objetos anónimos coincidan. Si son distintos, no habrá coincidencias — aunque los valores sean iguales en sentido.

8. Cómo se ve la colección unida — esquema

Aquí tienes un esquema que ilustra cómo funciona el join (muy simplificado):


flowchart LR
    subgraph Customers
        A1["Vasya (Id=1)"]
        A2["Petya (Id=2)"]
        A3["Masha (Id=3)"]
    end
    subgraph Orders
        B1["Bolígrafo (CustomerId=1)"]
        B2["Libro (CustomerId=2)"]
        B3["Cuaderno (CustomerId=2)"]
        B4["Goma (CustomerId=3)"]
    end

    A1-->|Id=1|B1
    A2-->|Id=2|B2
    A2-->|Id=2|B3
    A3-->|Id=3|B4
Esquema de unión de clientes y pedidos por clave

En el esquema se ve que cada cliente se une con todos sus pedidos — siempre que las claves coincidan.

9. Breve sobre errores y particularidades

Uno de los errores más típicos es confundir el orden de los parámetros, sobre todo si los tipos de clave en ambas colecciones son iguales. El compilador o LINQ no te dará error, pero el resultado puede ser vacío o raro.

Otra trampa común — intentar hacer un "join izquierdo" con Join para que salgan todos los clientes, incluso los que no tienen pedidos. Pero el Join clásico solo funciona con pares que coinciden y no incluye elementos sin correspondencia.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION