CodeGym /Kursy /C# SELF /Łączenie kolekcji za pomocą ...

Łączenie kolekcji za pomocą join ( Join)

C# SELF
Poziom 33 , Lekcja 0
Dostępny

1. Wprowadzenie

Czasem musimy pracować z dwiema różnymi kolekcjami, które są ze sobą powiązane jakimś wspólnym atrybutem. Na przykład mamy listę zamówień i osobną kolekcję klientów. Jak określić, które zamówienie należy do którego klienta? Do tego potrzebujemy, żeby obie kolekcje były powiązane wspólnym identyfikatorem — na przykład przez userId.

W relacyjnych bazach danych takie zadanie rozwiązuje operacja JOIN, która łączy wiersze z różnych tabel po pasujących kluczach. W LINQ jest analogiczny operator — też nazywa się Join.

Wyobraź sobie dwie tabele: w pierwszej — lista czytelników biblioteki z ich identyfikatorami, a w drugiej — lista zamówień książek, gdzie przy każdym zamówieniu jest podany id czytelnika. Za pomocą join "sklejamy" te tabele po id i na wyjściu dostajemy pary „czytelnik + jego zamówienie”.

A tak w ogóle, jeśli myślałeś, że join jest tylko dla "baz danych", to sporo tracisz! W programowaniu łączenie kolekcji przydaje się często, zwłaszcza gdy pracujesz z zewnętrznymi systemami albo plikami strukturalnymi (JSON, XML i różnymi tabelkami Excel z księgowości).

2. Sygnatura metody Join i zasada działania

Tak wygląda metoda Join:


public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                 // pierwsza kolekcja (zewnętrzna)
    IEnumerable<TInner> inner,                      // druga kolekcja (wewnętrzna)
    Func<TOuter, TKey> outerKeySelector,            // jak pobrać klucz z elementu zewnętrznej kolekcji
    Func<TInner, TKey> innerKeySelector,            // jak pobrać klucz z wewnętrznej
    Func<TOuter, TInner, TResult> resultSelector)   // funkcja generująca wynik (nowy element)

Na pierwszy rzut oka może wyglądać na skomplikowaną, ale zaraz wszystko rozłożymy na czynniki pierwsze.
Jak to działa:
Dla każdego elementu z pierwszej kolekcji (outer) LINQ szuka pasujących (po kluczu) elementów z drugiej (inner). Gdy klucze się zgadzają, wywoływany jest resultSelector i wynik trafia do końcowej kolekcji.

3. Przykład: Łączymy klientów i ich zamówienia

Stwórzmy parę klas i kolekcji. Ten pomysł to logiczna kontynuacja naszej aplikacji edukacyjnej (niech to będzie sklepik, który robimy w trakcie kursu).


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

// Kolekcja klientów
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "Jan" },
    new Customer { Id = 2, Name = "Piotr" },
    new Customer { Id = 3, Name = "Maria" },
};

// Kolekcja zamówień
var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 2, Product = "Książka" },
    new Order { Id = 102, CustomerId = 1, Product = "Długopis" },
    new Order { Id = 103, CustomerId = 2, Product = "Zeszyt" },
    new Order { Id = 104, CustomerId = 3, Product = "Gumka" },
};

Teraz chcemy dostać wszystkich klientów z ich zamówieniami. Na przykład wypisać: "Piotr zamówił Książka", "Piotr zamówił Zeszyt" itd.

Stosujemy Join


// Łączymy klientów z zamówieniami po customer.Id i order.CustomerId
var query = customers.Join(
    orders,
    customer => customer.Id,          // Jak pobrać klucz z klienta
    order => order.CustomerId,        // Jak pobrać klucz z zamówienia
    (customer, order) => new          // Co zrobić z pasującymi parami (tworzymy nowy obiekt)
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);

// Wyświetlamy wynik
foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName} zamówił {item.Product}");
}

Co zostanie wyświetlone:

Jan zamówił Długopis
Piotr zamówił Książka
Piotr zamówił Zeszyt
Maria zamówiła Gumka

Zwróć uwagę: jeśli klient ma kilka zamówień, pojawi się na liście kilka razy — po jednym na każde zamówienie. Tak właśnie ma być!

4. Tabela: Porównanie Join i GroupBy + SelectMany

Operacja Wynik Scenariusz użycia
Join
Płaska lista par Klasyczny SQL JOIN. Każda para (dopasowanie) — osobny wiersz.
GroupBy + SelectMany
Grupy z podkolekcjami Chcesz dostać "klient + wszystkie jego zamówienia" jako strukturę "jeden — do wielu".

Początkujący często używają Join tam, gdzie potrzeba struktury „klient → lista zamówień”. Ale Join działa wiersz po wierszu i nie grupuje danych. Do tego jest GroupJoin — o nim w następnym wykładzie. Albo użyj GroupBy.

5. Join w Query Syntax (SQL-style LINQ)

LINQ wspiera dwa style składni: łańcuchy metod (Method Syntax, np. Join(...)) i tzw. styl SQL (Query Syntax), który wizualnie przypomina zwykłe zapytania SQL.

Dla niektórych programistów — zwłaszcza tych, którzy wcześniej pracowali z bazami danych — taka składnia bywa bardziej czytelna:


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} zamówił {item.Product}");
}

Zwróć uwagę:
W Query Syntax używa się słowa kluczowego equals — operator == tutaj nie działa!
To częsta pułapka na rozmowach kwalifikacyjnych, szczególnie dla początkujących 😉

6. Ważne szczegóły i pułapki

Czasem zdarza się, że nie wszystkie elementy z kolekcji trafiają do końcowego wyniku. To dlatego, że metoda Join realizuje tzw. „inner join”, czyli łączy tylko te elementy, których klucze się zgadzają. Jeśli jakiś klient nie ma zamówień, po prostu nie będzie go w końcowej liście. Podobnie, jeśli jest zamówienie z CustomerId, którego nie ma wśród klientów, takie zamówienie też nie trafi do wyniku.

Ale co zrobić, jeśli chcesz dostać wszystkich klientów, nawet tych bez zamówień? Do takich przypadków jest „lewy” join, który w LINQ robi się przez kombinację GroupJoin i SelectMany. O tym pogadamy w następnym wykładzie. W klasycznym Join obie strony muszą być obecne — inaczej nie będzie dopasowania i element wypadnie z wyniku.

7. Praca z wieloma kluczami (composite key)

Czasem trzeba łączyć kolekcje nie po jednym, a po kilku polach. Na przykład: połączyć produkty i ich sprzedaż po "Kod-produktu + Rok-sprzedaży".

W LINQ robi się to przez tworzenie anonimowych obiektów jako kluczy:


var sales = ...; // sprzedaż
var products = ...; // produkty

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

Ważne, żeby typy i nazwy właściwości w tych anonimowych obiektach się zgadzały. Jeśli się różnią, nie będzie dopasowań — nawet jeśli wartości są logicznie takie same.

8. Jak wygląda połączona kolekcja — schemat

Oto schemat ilustrujący działanie join (bardzo uproszczony):


flowchart LR
    subgraph Customers
        A1["Jan (Id=1)"]
        A2["Piotr (Id=2)"]
        A3["Maria (Id=3)"]
    end
    subgraph Orders
        B1["Długopis (CustomerId=1)"]
        B2["Książka (CustomerId=2)"]
        B3["Zeszyt (CustomerId=2)"]
        B4["Gumka (CustomerId=3)"]
    end

    A1-->|Id=1|B1
    A2-->|Id=2|B2
    A2-->|Id=2|B3
    A3-->|Id=3|B4
Schemat połączenia klientów i zamówień po kluczu

Ze schematu widać, że każdy klient łączy się ze wszystkimi swoimi zamówieniami — pod warunkiem zgodności kluczy.

9. Krótko o błędach i szczegółach

Jednym z najczęstszych błędów jest pomylenie kolejności parametrów, zwłaszcza jeśli typy kluczy w obu kolekcjach są takie same. Kompilator ani LINQ nie zgłoszą błędu, ale wynik może być pusty albo dziwny.

Jeszcze jedna popularna pułapka — próba zrobienia za pomocą Join „lewego” połączenia, żeby w wyniku zostali wszyscy klienci, nawet bez zamówień. Jednak klasyczny Join działa tylko na pasujących parach i nie obejmuje elementów bez dopasowań.

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