CodeGym /Các khóa học /C# SELF /Kết hợp các collection với j...

Kết hợp các collection với join ( Join)

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Đôi khi tụi mình phải làm việc với hai collection khác nhau, mà chúng lại liên quan với nhau qua một đặc điểm chung nào đó. Ví dụ, tụi mình có danh sách đơn hàng và một collection khách hàng riêng. Làm sao biết đơn hàng nào thuộc về khách nào? Để làm được vậy, cả hai collection phải liên kết với nhau qua một identifier chung — ví dụ như userId.

Trong cơ sở dữ liệu quan hệ, bài toán này được giải bằng thao tác JOIN, nó kết hợp các dòng từ các bảng khác nhau dựa trên key trùng nhau. Trong LINQ cũng có operator tương tự — nó cũng tên là Join.

Hãy tưởng tượng hai bảng: bảng đầu là danh sách độc giả thư viện với id của họ, bảng thứ hai là danh sách đơn đặt sách, mỗi đơn đều có id độc giả. Dùng join tụi mình "ghép" hai bảng này theo id, và kết quả là các cặp “độc giả + đơn đặt của họ”.

À mà, nếu bạn nghĩ join chỉ dành cho "database" thì bạn bỏ lỡ nhiều thứ lắm! Trong lập trình, việc kết hợp collection diễn ra thường xuyên, nhất là khi làm việc với hệ thống ngoài hoặc file có cấu trúc (JSON, XML hay mấy bảng Excel kế toán gửi qua).

2. Signature method Join và nguyên lý hoạt động

Đây là cách method Join trông như nào:


public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                 // collection đầu (outer)
    IEnumerable<TInner> inner,                      // collection thứ hai (inner)
    Func<TOuter, TKey> outerKeySelector,            // lấy key từ phần tử outer
    Func<TInner, TKey> innerKeySelector,            // lấy key từ inner
    Func<TOuter, TInner, TResult> resultSelector)   // function tạo ra kết quả (phần tử mới)

Nhìn phát tưởng phức tạp, nhưng giờ mình sẽ giải thích từng phần.
Cách hoạt động:
Với mỗi phần tử trong collection đầu (outer), LINQ sẽ tìm các phần tử phù hợp (theo key) từ collection thứ hai (inner). Khi key trùng nhau, resultSelector được gọi, và kết quả được lưu vào collection kết quả.

3. Ví dụ: Kết hợp khách hàng và đơn hàng của họ

Tạo vài class và collection. Ý tưởng này là tiếp nối logic của app học tập tụi mình (cứ coi như là một shop nhỏ tụi mình code trong khoá học này).


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

// Collection khách hàng
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "Vasya" },
    new Customer { Id = 2, Name = "Petya" },
    new Customer { Id = 3, Name = "Masha" },
};

// Collection đơn hàng
var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 2, Product = "Sách" },
    new Order { Id = 102, CustomerId = 1, Product = "Bút" },
    new Order { Id = 103, CustomerId = 2, Product = "Vở" },
    new Order { Id = 104, CustomerId = 3, Product = "Tẩy" },
};

Giờ tụi mình muốn lấy tất cả khách hàng cùng với đơn hàng của họ. Ví dụ, in ra: "Petya đã đặt Sách", "Petya đã đặt Vở" v.v.

Dùng Join


// Kết hợp khách hàng với đơn hàng theo customer.Id và order.CustomerId
var query = customers.Join(
    orders,
    customer => customer.Id,          // Lấy key từ customer
    order => order.CustomerId,        // Lấy key từ order
    (customer, order) => new          // Làm gì với cặp trùng key (tạo object mới)
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);

// In kết quả
foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName} đã đặt {item.Product}");
}

Kết quả sẽ in ra:

Vasya đã đặt Bút
Petya đã đặt Sách
Petya đã đặt Vở
Masha đã đặt Tẩy

Lưu ý: nếu một khách có nhiều đơn, họ sẽ xuất hiện nhiều lần — mỗi đơn một dòng. Đúng như mong đợi luôn!

4. Bảng: So sánh JoinGroupBy + SelectMany

Thao tác Kết quả Kịch bản sử dụng
Join
Danh sách phẳng các cặp Kiểu JOIN trong SQL. Mỗi cặp (trùng key) là một dòng riêng.
GroupBy + SelectMany
Nhóm với sub-collection Muốn lấy "khách hàng + tất cả đơn hàng của họ" dạng "một — nhiều".

Người mới hay dùng Join khi cần cấu trúc “khách hàng → danh sách đơn hàng”. Nhưng Join chỉ hoạt động từng dòng, không group data. Để làm vậy thì dùng GroupJoin — sẽ nói ở bài sau. Hoặc dùng GroupBy.

5. Join với Query Syntax (kiểu SQL trong LINQ)

LINQ hỗ trợ hai kiểu syntax: method-chain (Method Syntax, ví dụ Join(...)) và kiểu SQL (Query Syntax), nhìn giống query SQL truyền thống.

Với một số dev — nhất là ai từng làm database — kiểu này nhìn dễ hiểu hơn:


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} đã đặt {item.Product}");
}

Lưu ý:
Trong Query Syntax dùng từ khoá equals — operator == không chạy ở đây đâu nhé!
Cái này hay bị hỏi khi phỏng vấn, nhất là newbie 😉

6. Những chi tiết quan trọng và bẫy thường gặp

Đôi khi sẽ có trường hợp không phải tất cả phần tử từ các collection đều xuất hiện trong kết quả cuối. Đó là vì method Join thực hiện kiểu «inner join», nghĩa là chỉ kết hợp những phần tử có key trùng nhau. Nếu khách nào không có đơn, sẽ không xuất hiện trong kết quả. Tương tự, nếu có đơn với CustomerId không có trong danh sách khách, đơn đó cũng không vào kết quả.

Vậy nếu muốn lấy tất cả khách, kể cả ai không có đơn thì sao? Trường hợp này dùng "left join", trong LINQ thì kết hợp GroupJoinSelectMany. Bài sau sẽ nói kỹ hơn. Còn với Join truyền thống thì cả hai bên phải có mặt — nếu không trùng thì phần tử sẽ bị loại khỏi kết quả.

7. Làm việc với nhiều key (composite key)

Đôi khi cần kết hợp collection không chỉ theo một mà nhiều field. Ví dụ: ghép sản phẩm và doanh số theo "Mã sản phẩm + Năm bán".

Trong LINQ, giải quyết bằng cách tạo object ẩn danh làm key:


var sales = ...; // doanh số
var products = ...; // sản phẩm

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

Quan trọng là kiểu và tên property trong object ẩn danh này phải giống nhau. Nếu khác, sẽ không có kết quả — dù giá trị thực tế giống nhau.

8. Collection đã kết hợp trông ra sao — sơ đồ

Đây là sơ đồ minh hoạ cách join hoạt động (rất đơn giản hoá thôi nhé):


flowchart LR
    subgraph Customers
        A1["Vasya (Id=1)"]
        A2["Petya (Id=2)"]
        A3["Masha (Id=3)"]
    end
    subgraph Orders
        B1["Bút (CustomerId=1)"]
        B2["Sách (CustomerId=2)"]
        B3["Vở (CustomerId=2)"]
        B4["Tẩy (CustomerId=3)"]
    end

    A1-->|Id=1|B1
    A2-->|Id=2|B2
    A2-->|Id=2|B3
    A3-->|Id=3|B4
Sơ đồ kết nối khách hàng và đơn hàng theo key

Nhìn sơ đồ sẽ thấy mỗi khách được nối với tất cả đơn của họ — miễn là key trùng.

9. Tóm tắt về lỗi và đặc điểm

Một lỗi phổ biến là nhầm thứ tự tham số, nhất là khi kiểu key ở cả hai collection giống nhau. Lúc này compiler hay LINQ không báo lỗi, nhưng kết quả có thể rỗng hoặc sai bất ngờ.

Một bẫy nữa là cố dùng Join để làm "left join", mong lấy tất cả khách kể cả ai không có đơn. Nhưng Join truyền thống chỉ lấy các cặp trùng key, không bao gồm phần tử không có đối ứng.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Gộp danh sách khách hàng và đơn hàng đơn giản
Gộp danh sách khách hàng và đơn hàng đơn giản
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION