Giới thiệu LINQ

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ó 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 có khả năng duyệt qua nó, cụ thể để sử dụng LINQ thì nạp hai thư viện GenericLinq:

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

Nguồn dữ liệu phục vụ cho LINQ, phải là các từ các lớp triển khai giao diện IEnumerable, IEnumerable<T> tức là các mảng, danh sách thuộc Collection đã biết ở phần trước. Trước tiên, tạo ra lớp Product để thực hành, lớp này có tạo một danh sách sản phẩm ở thành viên tĩnh products.

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 của sản phẩm
    public int Brand {set; get;}           // 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;
    }
    override public string ToString() => $"ID {ID} - {Name}, giá {Price}";

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

} 

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

Từ đối tượng products là danh sách các sản phẩm, lọc ra những sản phẩm có giá bằng 500.

var products =  Product.products;

var ketqua = from product in products
     where product.Price == 400
     select product;

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

// 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.

Câu truy vấn LINQ thường bắt đầu bằng mệnh đề from 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

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à giải những phần tử trong những biến kiểu lớp triển khai giao diện IEnumerable ví dụ mảng, 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

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 kiểu dữ liệu kết quả của câu truy vấn.

Ví dụ trả về các đối tượng Product

select product;

Ví dụ, chỉ lấy về một thuộc tính nào đó, như tên:

var ketqua = from product in products
            where product.Price == 400
            select product.Name;

foreach (var name in ketqua) Console.WriteLine(name);
// Bàn trà
// Tranh treo

Hoặc tạo các đối tượng kiểu vô danh (xem thêm kiểu vô danh C# ) trả về với toán tử new {}

var ketqua = from product in products
            where product.Price == 400 
            select new {
                ten = product.Name,
                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

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

Mệnh đề where để lọc dữ liệu, sau nó là các 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ả

Để sắp xếp kết quả sử dụng orderby, 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 ...

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, 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ụ, các sản phẩm nhóm lại theo giá

var ketqua = from product in products
            where product.Price >=400 && product.Price <= 500
            group product by product.Price;
foreach (var group in ketqua)
{
    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ả

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 biến thêm vào mệnh đề 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
            group product by product.Price into gr
            let count = gr.Count()
            select new {
                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.

Để thực hành, xây dựng thêm lớp Brand và khởi tạo một danh sách các nhãn hàng sau:

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

    static List<Brand> _brands;
    public static List<Brand> brands {
        get {
            if (_brands == null) {
                _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"},
                };
            }
            return _brands;
        }
    }
}

Các nhãn sử dụng bằng biến brands

var brands = Brand.brands;

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 khi truy vấn có sự liên kết kết nối giữa sản phẩm và nhãn

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 của nó - lấy tên Brand tương ứng

var products =  Product.products;
var brands   =  Brand.brands;

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

CODE VÍ DỤ LINQ
Đăng ký theo dõi ủng hộ kênh