Có gì mới trong EF Core 10: Toán tử LeftJoin và RightJoin trong LINQ

Nếu bạn đã từng làm việc với cơ sở dữ liệu, chắc hẳn bạn biết về LEFT JOIN (và ngược lại là RIGHT JOIN). Đây là một trong những thao tác chúng ta thường xuyên sử dụng, nếu không muốn nói là sử dụng mọi lúc. Nhưng trong Entity Framework Core, việc thực hiện left join luôn là… một chút khó khăn.

Tôi thích các phép join mà đọc lên giống như những gì chúng thực hiện. Thật không may, cho đến nay, LINQ không có cách trực tiếp để thể hiện left/right joins. Bạn phải sử dụng GroupJoinDefaultIfEmpty, điều này làm cho code khó đọc và khó bảo trì hơn.

Nhưng .NET 10 cuối cùng đã khắc phục điều này với các phương thức mới LeftJoinRightJoin.

LEFT JOIN là gì (bằng ngôn ngữ đơn giản)?

Một LEFT JOIN trả về tất cả các hàng từ phía bên tráicác hàng khớp từ phía bên phải. Nếu không có sự khớp, phía bên phải là null. Lý do chúng ta sử dụng nó: để giữ “chủ sở hữu” ngay cả khi họ không có các hàng liên quan (ví dụ: hiển thị tất cả sản phẩm ngay cả khi một số không có đánh giá).

Cách cũ (GroupJoin + DefaultIfEmpty)

Trước .NET 10, một left join trong LINQ cần một group join (GroupJoin), sau đó là DefaultIfEmpty để giữ các hàng bên trái không có sự khớp. Nó hoạt động, nhưng ý định bị chôn vùi trong sự phức tạp.

Có hai cách bạn có thể viết nó: cú pháp truy vấncú pháp phương thức.

Cú pháp truy vấn

var query =
    from product in dbContext.Products
    join review in dbContext.Reviews on product.Id equals review.ProductId into reviewGroup
    from subReview in reviewGroup.DefaultIfEmpty()
    orderby product.Id, subReview.Id
    select new
    {
        ProductId = product.Id,
        product.Name,
        product.Price,
        ReviewId = (int?)subReview.Id ?? 0,
        Rating = (int?)subReview.Rating ?? 0,
        Comment = subReview.Comment ?? "N/A"
    };

Đây là SQL được tạo bởi EF Core cho truy vấn trên:

SELECT
    p."Id" AS "ProductId",
    p."Name",
    p."Price",
    COALESCE(r."Id", 0) AS "ReviewId",
    COALESCE(r."Rating", 0) AS "Rating",
    COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Products" AS p
LEFT JOIN "Reviews" AS r ON p."Id" = r."ProductId"
ORDER BY p."Id", COALESCE(r."Id", 0)

Cú pháp phương thức

var query = dbContext.Products
    .GroupJoin(
        dbContext.Reviews,
        product => product.Id,
        review => review.ProductId,
        (product, reviewList) => new { product, subgroup = reviewList })
    .SelectMany(
        joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
        (joinedSet, review) => new
        {
            ProductId = joinedSet.product.Id,
            joinedSet.product.Name,
            joinedSet.product.Price,
            ReviewId = (int?)review!.Id ?? 0,
            Rating = (int?)review!.Rating ?? 0,
            Comment = review!.Comment ?? "N/A"
        })
    .OrderBy(result => result.ProductId)
    .ThenBy(result => result.ReviewId);

Tại sao cách này hoạt động: GroupJoin khớp các hàng, DefaultIfEmpty chèn một giá trị mặc định (null) khi không có sự khớp nào tồn tại, vì vậy hàng bên trái vẫn xuất hiện. Sau đó chúng ta làm phẳng với SelectMany.

Tôi nghĩ tất cả chúng ta đều đồng ý rằng điều này quá dài dòng cho một thao tác phổ biến như left join.

Cách mới trong EF 10: LeftJoin

Bây giờ chúng ta có thể viết những gì chúng ta muốn nói. LeftJoinLINQ hạng nhất và EF Core dịch nó thành LEFT JOIN trong SQL.

var query = dbContext.Products
    .LeftJoin(
        dbContext.Reviews,
        product => product.Id,
        review => review.ProductId,
        (product, review) => new
        {
            ProductId = product.Id,
            product.Name,
            product.Price,
            ReviewId = (int?)review.Id ?? 0,
            Rating = (int?)review.Rating ?? 0,
            Comment = review.Comment ?? "N/A"
        })
    .OrderBy(x => x.ProductId)
    .ThenBy(x => x.ReviewId)

SQL được tạo ra giống hệt với ví dụ trước.

Tại sao cách này tốt hơn:

  • Ý định rõ ràng: bạn thấy LeftJoin, bạn biết điều gì sẽ xảy ra.
  • Ít code hơn, ít thành phần di chuyển hơn (không có GroupJoin, không có DefaultIfEmpty, không có SelectMany).
  • Kết quả giống nhau: tất cả sản phẩm được giữ lại, đánh giá có thể là null.

Lưu ý: Tại thời điểm viết bài viết này, cú pháp truy vấn C# (from … select …) chưa có từ khóa left join hoặc right join. Bạn nên sử dụng cú pháp phương thức như đã hiển thị ở trên.

Cũng mới: RightJoin

RightJoin giữ tất cả các hàng từ phía bên phải và chỉ các hàng khớp từ phía bên trái. EF Core dịch nó thành RIGHT JOIN. Nó tiện dụng khi phía “phải giữ” là chuỗi thứ hai.

Về mặt khái niệm:

var query = dbContext.Reviews
    .RightJoin(
        dbContext.Products,
        review => review.ProductId,
        product => product.Id,
        (review, product) => new
        {
            ProductId = product.Id,
            product.Name,
            product.Price,
            ReviewId = (int?)review.Id ?? 0,
            Rating = (int?)review.Rating ?? 0,
            Comment = review.Comment ?? "N/A"
        });

Tại sao sử dụng RightJoin: khi báo cáo của bạn bắt đầu từ Reviews (giữ tất cả), và đưa vào các Products khớp nếu chúng tồn tại.

Đây là SQL được tạo ra:

SELECT
    p."Id" AS "ProductId",
    p."Name",
    p."Price",
    COALESCE(r."Id", 0) AS "ReviewId",
    COALESCE(r."Rating", 0) AS "Rating",
    COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Reviews" AS r
RIGHT JOIN "Products" AS p ON r."ProductId" = p."Id"

Tổng kết

Hãy nghĩ về tần suất bạn cần left joins. Hiển thị tất cả người dùng với cài đặt hồ sơ tùy chọn. Tất cả sản phẩm với đánh giá tùy chọn. Tất cả đơn hàng với thông tin vận chuyển tùy chọn. Nó có mặt ở khắp mọi nơi!

Trước đây, các nhà phát triển đôi khi bỏ qua left join thích hợp và thực hiện hai truy vấn riêng biệt thay thế. Hoặc tệ hơn, họ sử dụng inner join và bỏ lỡ dữ liệu. Bây giờ không có lý do gì để bỏ qua – nó dễ dàng như bất kỳ phương thức LINQ nào khác.

Một vài mẹo nhanh khi viết truy vấn LINQ:

  • Trong phép chiếu, bảo vệ phía nullable: review.Comment ?? "N/A"
  • Giữ phép chiếu nhỏ để tránh kéo nhiều cột hơn mức cần thiết
  • Thêm chỉ mục trên khóa join để có kế hoạch truy vấn tốt hơn

Đó là tất cả. Với LeftJoinRightJoin, code cuối cùng khớp với mô hình tư duy.

Chỉ mục