C# cơ bản .NET Core

Giới thiệu LINQ, LINQ là gì?

LINQ (Language Integrated Query) - ngôn ngữ truy vấn tích hợp - nó tích hợp cú pháp truy vấn (gần giống các câu lệnh SQL) vào bên trong ngôn ngữ lập trình C#, cho nó C# khả năng truy cập các nguồn dữ liệu khác nhau (SQL Db, XML, List ...) với cùng cú pháp.

Phần này trình bày LINQ trên dữ liệu đơn giản, mục đích để hiểu khả năng của LINQ trước khi sử dụng nó ở các chuyên đề chuyên sâu trong truy vấn cơ sở dữ liệu, truy vấn dữ liệu XML ...

LINQ hoạt động trên những kiểu tập hợp có khả năng duyệt qua nó (xem thêm Collection, List trong C#). Để sử dụng LINQ thì nạp hai thư viện GenericLinq:

using System.Collections.Generic;
using System.Linq;

Video bài học về LINQ

Nguồn dữ liệu phục vụ cho LINQ, phải là các đối tượng lớp triển khai giao diện (interface) IEnumerable và IEnumerable<T> tức là các mảng, danh sách thuộc Collection đã biết ở phần trước.

Để thực hành những vấn đề về LINQ trong bài viết này thông qua các ví dụ, ta tạo ra một nguồn dữ liệu đó là một danh sách (sẽ dùng lớp List) các sản phẩm (các sản phẩm sẽ sử dụng lớp Product tự xây dựng). Xây dựng lớp Product như sau:

public class Product
{
    public int ID {set; get;}
    public string Name {set; get;}         // tên
    public double Price {set; get;}        // giá
    public string[] Colors {set; get;}     // các màu sắc
    public int Brand {set; get;}           // ID Nhãn hiệu, hãng
    public Product(int id, string name, double price, string[] colors, int brand)
    {
        ID = id; Name = name; Price = price; Colors = colors; Brand = brand;
    }
    // Lấy chuỗi thông tin sản phẳm gồm ID, Name, Price
    override public string ToString()
       => $"{ID,3} {Name, 12} {Price, 5} {Brand, 2} {string.Join(",", Colors)}";

}

Xây dựng lớp Brand để biểu diễn nhãn hiệu hàng hóa:

public class  Brand {
    public string Name {set; get;}
    public int ID {set; get;}
}

Khởi tạo ra 2 đối tượng danh sách dùng để thực hiện các truy vấn linq: danh sách sản phẩm products, danh sách nhãn hiệu brands

var brands = new List<Brand>() {
    new Brand{ID = 1, Name = "Công ty AAA"},
    new Brand{ID = 2, Name = "Công ty BBB"},
    new Brand{ID = 4, Name = "Công ty CCC"},
};

var products = new List<Product>()
{
    new Product(1, "Bàn trà",    400, new string[] {"Xám", "Xanh"},         2),
    new Product(2, "Tranh treo", 400, new string[] {"Vàng", "Xanh"},        1),
    new Product(3, "Đèn trùm",   500, new string[] {"Trắng"},               3),
    new Product(4, "Bàn học",    200, new string[] {"Trắng", "Xanh"},       1),
    new Product(5, "Túi da",     300, new string[] {"Đỏ", "Đen", "Vàng"},   2),
    new Product(6, "Giường ngủ", 500, new string[] {"Trắng"},               2),
    new Product(7, "Tủ áo",      600, new string[] {"Trắng"},               3),
};

Như vậy bạn đã có danh sách các sản phẩm mẫu và nhãn hiệu, giờ là lúc dùng cú pháp LINQ để truy vấn đến tập dữ liệu này.

Viết câu truy vấn LINQ đầu tiên

Hãy thử câu lệnh LINQ: lọc ra những sản phẩm có giá bằng 400.

public class Products
{
    // ...

    // In ra các sản phẩm có giá 400
    public static void ProductPrice400()
    {
        // Viết câu quy vấn, lưu vào ketqua
        var ketqua = from product in products
                     where product.Price == 400
                     select product;

        foreach (var product in ketqua)
            Console.WriteLine(product.ToString());
    }
    // ...
}

Trong hàm Main chạy thử:

Products.ProductPrice400();

// Kết quả
// ID 3 - Bàn trà, giá 400
// ID 4 - Tranh treo, giá 400

Trong câu truy vấn bạn thấy xuất hiện các từ from, where, select đó là những từ khóa của C# để viết mệnh đề truy vấn LINQ, chúng khá giống SQL.

Câu truy vấn LINQ thường bắt đầu bằng mệnh đề from và kết thúc bằng mệnh đề select hoặc group, giữa chúng là những mệnh đề where, orderby, join, let

Mệnh đề from ...in ...

Mệnh đề from để xác định nguồn dữ liệu mà truy vấn sẽ thực hiện, nguồn dữ liệu là tập hợp những phần tử lưu trong đối tượng có kiểu lớp triển khai giao diện IEnumerable như mảng Array, List ...

Ví dụ, products ở trên là kiểu List (nó triển khai IEnumerable), thì một giải (một đoạn, hoặc tất cả) các phần tử trong nó (ví dụ product in products) có thể làm nguồn, vậy mở đầu truy vấn bằng mệnh đề from sẽ là:

from product in produts
  • products là nguồn dữ liệu
  • product là tên bạn tùy đặt đại diện cho phần tử trong nguồn, tên này sẽ được dùng bởi các mệnh đề theo sau.

Mệnh đề select

Một câu truy vấn phải kết thúc bằng mệnh đề select hoặc group. Mệnh đề select chỉ ra các dữ liệu được lấy ra (xuất ra) của câu lệnh LINQ.

Ví dụ:

  • var ketqua = from product in products
    select product;

    Có nghĩa là với mỗi product trong products, dữ liệu lấy ra là các product đó (trả về collection gồm các phần tử Product).

  • Bạn có thể chỉ lấy ra một loại dữ liệu nào đó của phần tử , ví dụ:

    var ketqua = from product in products
    select product.Name;
    
    foreach (var name in ketqua) Console.WriteLine(name);
    // Bàn trà
    // Túi da
    //...

    Câu LINQ trên có nghĩa là với mỗi product có trong products lấy ra tên (Name) của nó (trả về một collection gồm các phần tử chuỗi)

  • Bạn có thể trả về những đối tượng phức tạp mà dữ liệu được trích xuất ra from ... in ... hay những dữ liệu do tính toán ... Ví dụ trả về kiểu vô danh chứa các trường thông tin (xem thêm kiểu vô danh C# ) trả về với toán tử new {}

    var ketqua = from product in products
                 select new {
                    ten = product.Name.ToUpper(),
                    mausac = string.Join(',', product.Colors)
                 };
    
    foreach (var item in ketqua) Console.WriteLine(item.ten + " - " + item.mausac);
    // Bàn trà - Xám,Xanh
    // Tranh treo - Vàng,Xanh
    

    Có nghĩa là với mỗi sản phẩm lấy ra, trả về một đối tượng chứa ten lấy bằng tên sản phẩm chuyển hết thành in hoa, mausac là chuỗi nối các màu sắc của sản phẩm.

Mệnh đề where lọc dữ liệu trong LINQ

Mệnh đề where để lọc dữ liệu, sau từ khóa where là biểu thức logic xác định các phần tử lọc ra. Ví dụ:

where product.Price == 500

Bạn có thể viết các điều kiện phức tạp

where (product.Price >= 600 && product.Price < 700) || product.Name.StartsWith("Bàn")

Thậm chí, trong một truy vấn có thể viết nhiều mệnh đề where

var ketqua = from product in products
     where product.Price >= 500
     where product.Name.StartsWith("Giường")
     select product;

// ID 6 - Giường ngủ, giá 500

from kết hợp

Để lọc dữ liệu phức tạp hơn, có thể dùng From kết hợp để lọc phức tạp và chi tiết hơn. Ví dụ, mỗi tên sản phẩm nó có một mảng màu. Lọc ra những sản phẩm có chứa màu nào đó.

var ketqua = from product in products
             from color in product.Colors
             where product.Price < 500
             where color == "Vàng"
             select product;

foreach (var product in ketqua) Console.WriteLine(product.ToString());

// ID 2 - Túi da, giá 300
// ID 4 - Tranh treo, giá 400

Mệnh đề orderby sắp xếp kết quả trong LINQ

Để sắp xếp kết quả sử dụng orderby viết sau mệnh đề where nếu có, ví dụ sắp xếp tăng dần (từ nhỏ đến lớn) theo thuộc tính dữ liệu (hoặc thuộc tính) thuoctinh thì viết

orderby thuoctinh

Nếu muốn xếp theo thứ tự giảm dần (lớn đến bé)

orderby thuoctinh descending

Ví dụ:

var ketqua = from product in products
            where product.Price <= 300
            orderby product.Price descending
            select product;
foreach (var product in ketqua) Console.WriteLine($"{product.Name} - {product.Price}");
// Túi da - 300
// Bàn học - 200

Cũng có thể sắp xếp theo nhiều dữ liệu, viết cách nhau bởi ,

orderby thuoctinh1 descending, thuoctinh2, thuoctinh3 descending ...

Mệnh đề group ... by

Cuối truy vấn có thể sử dụng group thay cho select, nếu sử dụng group thì nó trả về theo từng nhóm (nhóm lại theo trường dữ liệu nào đó), mỗi phần tử của cấu truy vấn trả về là kiểu IGrouping<TKey,TElement>, chứa các phần tử thuộc một nhóm.

Ví dụ, lấy sản phẩm có giá từ 400 - 500, nhóm lại theo giá (cùng giá cho vào một nhóm)

var ketqua = from product in products
             where product.Price >=400 && product.Price <= 500
             group product by product.Price;
foreach (var group in ketqua)
{
    // Key là giá trị dùng để phân loại (nhóm): là giá
    Console.WriteLine(group.Key);
    foreach (var product in group)
    {
        Console.WriteLine($"    {product.Name} - {product.Price}");
    }

}
// 400
//     Bàn trà - 400
//     Tranh treo - 400
// 500
//     Đèn trùm - 500
//     Giường ngủ - 500

Bạn có thể lưu tạm group trong truy vấn vào một biến bằng cách sử dụng into, sau đó thi hành các mệnh đề khác trên biến tạm và dùng mệnh đề select để trả về kết quả

// Kết quả giống câu truy vấn trên, nhưng các nhóm xếp từ lớn xuống nhỏ
var ketqua = from product in products
             where product.Price >=400 && product.Price <= 500
             group product by product.Price into gr
             orderby gr.Key descending
             select gr;

// 500
//     Đèn trùm - 500
//     Giường ngủ - 500
// 400
//     Bàn trà - 400
//     Tranh treo - 400

let - dùng biến trong truy vấn

Dùng thêm biến vào LINQ, lưu kết quả của một biểu thức tính toán nào đó, thêm vào mệnh đề bằng cách viết let tenvien = biểu_thức, ví dụ với mỗi loại giá - có bao nhiêu sản phẩm

var ketqua = from product in products                  // các sản phẩm trong products
             group product by product.Price into gr    // nhóm thành gr theo giá
             let count = gr.Count()                    // số phần tử trong nhóm
             select new {                              // trả về giá và số sản phầm có giá này
                price = gr.Key,
                number_product = count
             };

foreach (var item in ketqua)
{
    Console.WriteLine($"{item.price} - {item.number_product}");
} 
// 200 - 1
// 300 - 1
// 400 - 2
// 500 - 2
// 600 - 1

Mệnh đề join - khớp nối nguồn truy vấn LINQ

join là thực hiện kết hợp hai nguyền dữ liệu lại với nhau để truy vấn thông tin, ví dụ trong mỗi sản phẩm đều có Brand nó là ID của nhãn - vậy mỗi sản phẩm dùng thông tin này để có được thông tin chi tiết về nhãn hàng mà nhãn hàng lại lưu ở nguồn khác.

Các sản phẩm ở ví dụ phía trên, sản phẩm nào có Brand là 2 thì tra cứu ở Brand.brands thì tên nhãn là "Công ty AAA"

Giờ muốn thực hành truy vấn kết hợp khớp nối giữa hai nguồn dữ liệu danh sách sản phẩm products và danh sách nhãn hàng brands

Inner join

Để kết nối, dùng mệnh đề join để chỉ ra nguồn (nguồn bên phải join) sẽ kết nối với nguồn của from (nguồn bên trái join), tiếp theo chỉ ra sự dàng buộc các phần tử bằng từ khóa on

Ví dụ, truy vấn sản phẩm, mỗi sản phẩm căn cứ vào Brand ID của nó - lấy tên Brand tương ứng

var ketqua = from product in products
             join brand in brands on product.Brand equals brand.ID
             select new {
                 name  = product.Name,
                 brand = brand.Name,
                 price = product.Price
             };

foreach (var item in ketqua)
{
    Console.WriteLine($"{item.name,10} {item.price, 4} {item.brand,12}");
}

//     Túi da  300  Công ty BBB
//    Bàn trà  400  Công ty BBB
// Tranh treo  400  Công ty AAA
// Giường ngủ  500  Công ty BBB

Phân tích truy vấn trên ta thấy

  • nguồn bên trái join là : product in products
  • nguồn bên phải join là : brand in brands
  • liên kệ kết nối là: on product.Brand equals brand.ID (Brand trong product bằng (equals) ID của brand)

Với truy vấn join trên, chỉ những dữ liệu product mà có brand tương ứng mới trả về, nên nó gọi là inner join (tức giá trị product.brand hay brand.ID có ở cả 2 nguồn). Kết quả trả về có 4 dòng như trên. Để ý trong danh sách sản phẩm có sản phẩm "Tủ áo", sản phẩm này có brand là 3, nhưng ở danh sách brands lại không có phần tử nào ID bằng 3 nên truy vấn trên sẽ bỏ sản phẩm "Tủ áo"

Kỹ thuật inner join trên giống với inner join trong SQL, bạn có thể xem hình vẽ mô tả trực quan tại Khớp nối bảng SQL

Left join

Trong truy vấn trên, sản phẩm nào không tìm được thông tin brand ở ngồn bên phải join thì sẽ bỏ qua. Giờ muốn, các sản phẩm kể cả không thấy brand cũng trả về - có nghĩa nguồn bên trái lấy không phụ thuộc vào bên phải thì dùng kỹ thuật left join như sau:

var ketqua = from product in products
             join brand in brands on product.Brand equals brand.ID into t
             from brand in t.DefaultIfEmpty()
             select new {
                 name  = product.Name,
                 brand = (brand == null) ? "NO-BRAND" : brand.Name,
                 price = product.Price
             };

foreach (var item in ketqua)
{
    Console.WriteLine($"{item.name,10} {item.price, 4} {item.brand,12}");
}

//         Bàn học  200  Công ty AAA
//     Túi da  300  Công ty BBB
//    Bàn trà  400  Công ty BBB
// Tranh treo  400  Công ty AAA
//   Đèn trùm  500     NO-BRAND
// Giường ngủ  500  Công ty BBB
//      Tủ áo  600     NO-BRAND

Khi truy vấn, các brand tương ứng với product gồm cả nhưng brand bằng null tương ứng với sản phẩm không thấy brand được lưu vào nguồn tạm đặt tên là t. Sau đó truy vấn lấy brand trên DefaultIfEmpty() nguồn tạm này, đảm bảo các sản phẩm kể cả không thấy brand (brand bằng null) cũng trả về.

Khi lấy tên brand, nếu null thì thay bằng chữ "NO-BRAND" brand = (brand == null) ? "NO-BRAND" : brand.Name

Mã nguồn ví dụ tại CS018_LINQ (git) hoặc tải về ex018


Đăng ký nhận bài viết mới