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 |
|---|---|---|
|
Płaska lista par | Klasyczny SQL JOIN. Każda para (dopasowanie) — osobny wiersz. |
|
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
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ń.
GO TO FULL VERSION