JavaRush /Курси /C# SELF /Об’єднання колекцій за допомогою

Об’єднання колекцій за допомогою join ( Join)

C# SELF
Рівень 33 , Лекція 0
Відкрита

1. Вступ

Іноді нам потрібно працювати з двома різними колекціями, пов’язаними спільною ознакою. Наприклад, у нас є список замовлень і окрема колекція покупців. Як зрозуміти, яке замовлення належить якому покупцеві? Для цього треба, щоб обидві колекції були пов’язані спільним ідентифікатором — наприклад, userId.

У реляційних базах даних таке завдання розв’язує операція JOIN, яка об’єднує рядки з різних таблиць за збігом ключів. У LINQ є аналогічний оператор — він теж називається Join.

Уявіть дві таблиці: у першій — список читачів бібліотеки з їхніми ідентифікаторами, а у другій — список замовлень книжок, де в кожному замовленні вказано id читача. За допомогою join ми «склеюємо» ці таблиці за id і в підсумку отримуємо пари «читач + його замовлення».

До речі, якщо ви вважали, що join — це лише «для баз даних», ви чимало втрачали. У програмуванні об’єднувати колекції доводиться часто, особливо коли працюєте із зовнішніми системами або структурованими файлами (JSON, XML чи таблицями Excel з бухгалтерії).

2. Сигнатура методу Join і принцип роботи

Ось як виглядає метод Join:


public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                 // перша колекція (зовнішня)
    IEnumerable<TInner> inner,                      // друга колекція (внутрішня)
    Func<TOuter, TKey> outerKeySelector,            // як отримати ключ із елемента зовнішньої колекції
    Func<TInner, TKey> innerKeySelector,            // як отримати ключ із внутрішньої колекції
    Func<TOuter, TInner, TResult> resultSelector)   // функція формування результату (новий елемент)

На перший погляд це може здатися громіздким, але зараз усе розкладемо по поличках.
Як це працює:
Для кожного елемента з першої колекції (outer) LINQ шукає відповідні (за ключем) елементи з другої (inner). Коли ключі збігаються, викликається resultSelector, і результат додається до фінальної колекції.

3. Приклад: Об’єднуємо покупців і їхні замовлення

Створимо пару класів і колекцій. Ця ідея — логічне продовження нашого навчального застосунку (нехай це буде невеликий магазин, який ми пишемо впродовж курсу).


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

// Колекція покупців
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "Василь" },
    new Customer { Id = 2, Name = "Петро" },
    new Customer { Id = 3, Name = "Марко" },
};

// Колекція замовлень
var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 2, Product = "Книгу" },
    new Order { Id = 102, CustomerId = 1, Product = "Ручку" },
    new Order { Id = 103, CustomerId = 2, Product = "Зошит" },
    new Order { Id = 104, CustomerId = 3, Product = "Гумку" },
};

Тепер ми хочемо отримати всіх покупців із їхніми замовленнями. Наприклад, вивести: «Петро замовив Книгу», «Петро замовив Зошит» тощо.

Застосовуємо Join


// Об’єднуємо покупців із замовленнями за customer.Id і order.CustomerId
var query = customers.Join(
    orders,
    customer => customer.Id,          // як отримати ключ із покупця
    order => order.CustomerId,        // як отримати ключ із замовлення
    (customer, order) => new          // що робити з парами, що збіглися (створюємо новий об’єкт)
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);

// Виводимо результат
foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName} замовив {item.Product}");
}

Що буде виведено:

Василь замовив Ручку
Петро замовив Книгу
Петро замовив Зошит
Марко замовив Гумку

Зверніть увагу: якщо в покупця кілька замовлень, він з’явиться у списку кілька разів — по одному на кожне замовлення. Саме так і має бути!

4. Таблиця: Порівняння Join і GroupBy + SelectMany

Операція Результат Сценарій використання
Join
Плоский список пар Класичний SQL JOIN. Кожен збіг — окремий рядок.
GroupBy + SelectMany
Групи з підколекціями Потрібно отримати «покупець + всі його замовлення» у вигляді структури «один — до багатьох».

Новачки часто використовують Join там, де потрібна структура «покупець → список замовлень». Але Join працює поелементно і не групує дані. Для цього є GroupJoin — про нього в наступній лекції. Або ж використовуйте GroupBy.

5. Join у Query Syntax (SQL-стиль LINQ)

LINQ підтримує два синтаксиси: метод-ланцюжки (Method Syntax, наприклад Join(...)) і так званий SQL-стиль (Query Syntax), який візуально нагадує звичайні SQL-запити.

Для деяких розробників — особливо тих, хто раніше працював із базами даних — такий синтаксис часто здається наочнішим:


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} замовив {item.Product}");
}

Зверніть увагу:
У Query Syntax використовується ключове слово equals — оператор == тут не працює!
Це поширений підступ на співбесідах, особливо для початківців 😉

6. Важливі деталі та підводні камені

Іноді виникає ситуація, коли не всі елементи з колекцій потрапляють у фінальний результат. Це пов’язано з тим, що метод Join реалізує так званий «inner join», тобто він об’єднує лише ті елементи, у яких ключі збігаються. Якщо в якогось покупця немає замовлень, його просто не буде у фінальному списку. Аналогічно, якщо є замовлення з CustomerId, якого немає серед покупців, таке замовлення теж не потрапить у результат.

Що робити, якщо потрібно отримати всіх покупців, навіть тих, у кого немає замовлень? Для таких випадків існує «лівий» join, який у LINQ реалізується за допомогою комбінації GroupJoin і SelectMany. Про це поговоримо в наступній лекції. У класичному ж Join обов’язково мають бути представлені обидві сторони — інакше збігу не буде, і елемент випаде з результату.

7. Робота з кількома ключами (складений ключ)

Іноді об’єднувати колекції треба не за одним, а за кількома полями. Наприклад: з’єднати товари та їхні продажі за «код товару + рік продажу».

У LINQ це вирішується за допомогою створення анонімних об’єктів як ключів:


var sales = ...; // продажі
var products = ...; // товари

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

Важливо, щоб типи та імена властивостей у цих анонімних об’єктах збігалися. Якщо вони різні, збігів не буде — навіть якщо значення по суті однакові.

8. Як виглядає об’єднана колекція — схема

Ось спрощена схема, що ілюструє роботу join:

Схема з’єднання покупців і замовлень за ключем

Із схеми видно, що кожен покупець з’єднується з усіма своїми замовленнями — за умови збігу ключів.

9. Коротко про помилки та особливості

Одна з найчастіших помилок — переплутати порядок параметрів, особливо якщо типи ключів в обох колекціях однакові. При цьому компілятор або LINQ не повідомлять про помилку, але результат може бути порожнім або неочікуваним.

Ще одна поширена пастка — спроба за допомогою Join зробити «ліве» об’єднання, щоб у результаті залишилися всі покупці, навіть якщо у них немає замовлень. Але класичний Join працює тільки з парами, що збігаються, і не включає елементи без відповідників.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ