Chủ Nhật, 14 tháng 5, 2017

Tìm hiểu LINQ và sử dụng LINQ trên C#


1.  Giới thiệu về LINQ


Language-Integrated Query (LINQ) là một sự đổi mới được giới thiệu trong Visual Studio 2008 và .NET Framework phiên bản 3.5.

Theo truyền thống, các truy vấn dữ liệu được thể hiện dưới dạng các chuỗi mà không cần kiểm tra kiểu tại thời gian biên dịch hoặc hỗ trợ IntelliSense và bạn phải học một ngôn ngữ truy vấn khác nhau cho từng loại nguồn dữ liệu ví dụ như: cơ sở dữ liệu SQL, tài liệu XML, dịch vụ web khác nhau, ... LINQ giúp nhúng truy vấn vào ngôn ngữ lập trình (đúng như tên gọi của nó). Tức là bạn có thể sử dụng C# hay Visual Basic để có thể truy vấn đến mọi nguồn dữ liệu mà không nhất thiết phải biết các loại ngôn ngữ truy vấn riêng biệt để có thể thao tác với chúng. Bạn sẽ viết truy vấn sử dụng từ khóa và toán tử quen thuộc của ngôn ngữ lập trình. Hình minh họa dưới đây cho thấy một truy vấn LINQ kết nối đến 1 cơ sở dữ liệu SQL Server trong C# và Visual Basic:




Trong Visual Studio, bạn có thể viết các truy vấn LINQ trong Visual Basic hoặc C# đến cơ sở dữ liệu SQL Server, các tài liệu XML, tập dữ liệu ADO.NET, và bất kỳ collections của của đối tượng có hỗ trợ interface IEnumerable hoặc IEnumerable<T>. LINQ cũng hỗ trợ cho ADO.NET Entity Framework, và các LINQ provider đang được viết bởi bên thứ ba cho nhiều dịch vụ Web và triển khai cơ sở dữ liệu khác.

Để sử dụng LINQ thì dự án của bạn phải đang chạy trên nền tảng .NET Framework 3.5 hoặc cao hơn.

2.  Sử dụng LINQ trên C#


a. Giới thiệu về LINQ Queries (C#)


Một truy vấn là một biểu thức để lấy dữ liệu từ một nguồn dữ liệu. Các truy vấn thường được biểu thị bằng một ngôn ngữ truy vấn cụ thể. Trước đây với mỗi nguồn dữ liệu thì sẽ đi kèm với ngôn ngữ riêng để truy vấn trên nguồn dữ liệu đó, ví dụ SQL cho cơ sở dữ liệu quan hệ và XQuery cho XML. Do đó, các nhà phát triển đã phải học một ngôn ngữ truy vấn mới cho mỗi loại nguồn dữ liệu. LINQ đơn giản hoá vấn đề này bằng cách tích hợp truy vấn vào trong ngôn ngữ lập. Trong một truy vấn LINQ, bạn luôn làm việc với các đối tượng (object). Bạn sử dụng cùng một truy vấn và chuyển đổi dữ liệu trong các tài liệu XML, cơ sở dữ liệu SQL, ADO.NET Datasets, .NET collections và bất kỳ định dạng nào khác mà LINQ provider có sẵn. 

Truy vấn LINQ chia thành 3 thành phần sau:
  1. Chuẩn bị nguồn dữ liệu
  2. Tạo câu lệnh truy vấn LINQ
  3. Thực thi truy vấn để lấy kết quả

Ví dụ sau đây cho thấy cách hoạt động của 3 thành phần trên. Ví dụ sử dụng một mảng số nguyên như là một nguồn dữ liệu. Ví dụ này được sử dụng trong suốt phần còn lại của chủ đề này.

      
        //  1. Nguồn dữ liệu.
        int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };

        // 2. Tạo câu lệnh truy vấn:
        // Lấy ra các số chẵn trong mảng numbers
        var numQuery =
            from num in numbers
            where (num % 2) == 0
            select num;

        // 3. Thực thi truy vấn.
        foreach (int num in numQuery)
        {
            Console.Write("{0,1} ", num);
        }

Hình minh họa dưới đây cho thấy cách hoạt động của LINQ.



Các biểu thức truy vấn có chứa ba mệnh đề: from, where, select. (Nếu bạn đã quen thuộc với SQL, bạn sẽ nhận thấy rằng cách sắp xếp trật tự của các mệnh đề ngược lại với trật tự trong SQL). Mệnh đề from chỉ ra nguồn dữ liệu, mệnh đề where áp dụng để lọc dữ liệu theo 1 điều kiện nào đó, và mệnh đề select chỉ ra kiểu của kết quả trả về. Những mệnh đề này và một số mệnh đề khác được thảo luận chi tiết trong phần LINQ Query Expressions (C# Programming Guide).

Chú ý: Truy vấn LINQ cũng có thể được xây dựng bằng cách sử dụng phương thức mở rộng (method systax) thay vì  dùng theo kiểu Query syntax như ví dụ. Để biết thêm thông tin, xem Query Syntax and Method Syntax in LINQ (C#).

Trì hoãn thực thi


Các biến truy vấn mặc định chỉ lưu trữ các lệnh truy vấn. Các hoạt động truy vấn được hoãn lại cho đến khi bạn lặp qua các biến truy vấn trong một câu lệnh foreach. Khái niệm này được gọi là deferred execution (trì hoãn thực thi) và được thể hiện trong ví dụ sau:

//Khi vòng lặp được chạy thì truy vấn mới được thực thi và gán kết quả truy vấn qua biến num.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}


Câu lệnh foreach cũng là nơi mà các kết quả truy vấn được lấy ra. Ví dụ, trong truy vấn trên, mỗi lần lặp thì biến num giữ mỗi giá trị khác nhau (tại mỗi thời điểm) bởi vì mỗi lần lặp là mỗi lần trình biên dịch thực thi query và lấy dữ liệu từ CSDL.

Buộc thực thi ngay


Truy vấn thực hiện chức năng thống kê phải duyệt qua những phần tử. Ví dụ như các truy vấn Count, Max, Min, Sum, Average, First. Đó là những truy vấn mà sự thực thi truy vấn đó mà không có một câu lệnh foreach rõ ràng vì các truy vấn tự nó phải sử dụng ngầm foreach để trả lại kết quả. Ví dụ truy vấn dùng mệnh đề Sum sẽ ngầm dùng vòng lặp foreach để duyệt qua tất cả các phần tử để thực hiện tính tổng. Lưu ý rằng các loại truy vấn thống kê sẽ trả về 1 giá trị duy nhất do đó kiểu trả về không phải là 1 queryable. Các truy vấn sau đây trả về số lượng các số chẵn trong mảng numbers:

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

Buộc thực thi ngay và lưu lại kết quả vào 1 collection hoặc generic collection, bạn có thể sử dụng phương thức ToList<TSource> hoặc ToArray<TSource> . Ví dụ:

List<int> numQuery2 =
    (from num in numbers
     where (num % 2) == 0
     select num).ToList();

// hoặc dùng cách này:
// numQuery3 sẽ có kiểu là 1 mảng int[]

var numQuery3 =
    (from num in numbers
     where (num % 2) == 0
     select num).ToArray();


Bằng cách gọi ToList hoặc ToArray bạn sẽ lưu trữ tất cả các dữ liệu trong một đối tượng collection nằm trong bộ nhớ để sử dụng dữ liệu đó và truy vấn tiếp về sau trên collection thay vì cứ phải truy xuất đến dữ liệu mỗi khi thực thi truy vấn.

b. LINQ và kiểu Generic


Truy vấn LINQ dựa trên các kiểu generic, mà được giới thiệu trong .NET Framework 2.0. Bạn không cần 1 kiến thức chuyên sâu về Generic để có thể bắt đầu viết truy vấn LINQ. Tuy nhiên, bạn cần phải biết 2 khái niệm cơ bản sau đây:
  1. Khi bạn tạo một đối tượng của một 1 class Generic Collection như List<T>, bạn sẽ thay thế chữ “T” với chính xác kiểu dữ liệu của đối tượng mà danh sách giữ. Ví dụ, List<string> đại diện cho một danh sách chuỗi hoặc 1 danh sách các Customer có thể được khai báo là List<Customer>. 
  2. IEnumerable<T> là interface cho phép các lớp Collection Generic có thể dùng câu lệnh lặp foreach. Các lớp Generic Collection hỗ trợ IEnumerable<T> cũng giống như các lớp non-generic như ArrayList hỗ trợ IEnumerable .
Để biết thêm thông tin về generics, xem Generics (C# Programming Guide).

c. Sử dụng kiểu var


Nếu bạn thích, bạn có thể tránh được cú pháp generic bằng từ khóa var. Từ khóa var chỉ thị trình biên dịch tự định kiểu theo giá trị được gán. Tuy nhiên nếu truy vấn LINQ trả về 1 kiểu vô danh (anonymos) thì bạn bắt buộc phải dùng kiểu var.

Ví dụ 1 (có thể dùng var):


    var customerQuery =
    from cust in customers
    where cust.City == "London"
    select cust;

Ví dụ 2 (bắt buộc dùng var):


    var customerQuery =
    from cust in customers
    where cust.City == "London"
    selet new { ID = cust.ID, CityName = cust.City };   // truy vấn trả về kiểu vô danh


3. Truy vấn cơ bản LINQ (C#)


Chủ đề này giới thiệu ngắn gọn các biểu thức truy vấn LINQ và một số mệnh đề điển hình mà bạn thực hiện trong một truy vấn. thông tin chi tiết hơn có trong các chủ đề sau:

LINQ Query Expressions (C# Programming Guide)
Standard Query Operators Overview
Walkthrough: Writing Queries in C# (LINQ)

Lấy 1 nguồn dữ liệu 


Trong một truy vấn LINQ, bước đầu tiên là xác định nguồn dữ liệu. Trong C# và hầu hết các ngôn ngữ lập trình khác một biến phải được khai báo trước khi nó có thể được sử dụng. Trong một truy vấn LINQ, mệnh đề from được viết đầu tiên để khai báo nguồn dữ liệu, đi kèm là biến phạm vi.

Trong ví dụ dưới đây nguồn dữ liệu là customers và biến phạm vi là cust: 

//queryAllCustomers is an IEnumerable<Customer>
var queryAllCustomers = from cust in customers
                        select cust;


Biến phạm vi giống như các biến lặp trong một vòng lặp foreach ngoại trừ việc không có 1 sự lặp lại thực sự xảy ra trong một biểu thức truy vấn. Khi truy vấn được thực thi, biến phạm vi sẽ có nhiệm vụ như là một tham chiếu đến từng phần tử kế tiếp trong customers. Bởi vì trình biên dịch có thể suy ra kiểu của biến cust , bạn không cần phải xác định kiểu tường minh.

Lọc (Filter)


Có lẽ 1 trong các hoạt động truy vấn phổ biến nhất là áp dụng một bộ lọc. Bộ lọc khiến cho truy vấn trả về chỉ những phần tử thỏa yêu cầu. Sử dụng mệnh đề where kèm theo một biểu thức boolean để thực hiện lọc. Trong ví dụ sau đây, chỉ những custommers có địa chỉ ở London được trả về.

var queryLondonCustomers = from cust in customers
                           where cust.City == "London"
                           select cust;

Bạn có thể sử dụng toán tử AND (&&)OR (||) quen thuộc trong C# để áp dụng nhiều biểu thức lọc khi cần thiết. Ví dụ, để trả về chỉ có khách hàng từ "London" có tên là "Devon" bạn sẽ viết đoạn code sau:

var queryLondonCustomers = from cust in customers
                           where cust.City=="London" && cust.Name == "Devon"
                           select cust;

Trả về chỉ những khách hàng đến từ London hoặc Pari bạn sẽ viết đoạn code sau:

var queryLondonCustomers = from cust in customers
                                                      where cust.City == "London" || cust.City == "Pari"                               select cust;

Để biết thêm thông tin, xem where clause (C# Reference)

Sắp xếp (Sorting)


Các mệnh đề orderby mặc định sẽ sắp xếp phần tử theo thứ tự tăng dần . Ví dụ, truy vấn sau đây thực hiện sắp xếp danh sách customers theo thứ tự tăng dần theo Name.

    var queryLondonCustomers3 =
    from cust in customers
    where cust.City == "London"
    orderby cust.Name ascending
    select cust;

Ngược lại, để sắp xếp giảm dần sử dụng mệnh đề orderby…descending

    var queryLondonCustomers3 =
    from cust in customers
    where cust.City == "London"
    orderby cust.Name descending
    select cust;

Gom nhóm (Group)


Mệnh đề group cho phép bạn nhóm kết quả trả về dựa trên một khóa (key) mà bạn chỉ định. Ví dụ bạn có thể chỉ định kết quả trả về sẽ được nhóm theo City để có thể phân các khách hàng ở London và Paris thành các nhóm riêng. Trong trường hợp này Cust.City là 1 khóa (key).

  // queryCustomersByCity is an IEnumerable<IGrouping<string, Customer>>
  var queryCustomersByCity =
      from cust in customers
      group cust by cust.City;

  // customerGroup is an IGrouping<string, Customer>
  foreach (var customerGroup in queryCustomersByCity)
  {
      Console.WriteLine(customerGroup.Key);
      foreach (Customer customer in customerGroup)
      {
          Console.WriteLine("    {0}", customer.Name);
      }
  }


Khi bạn kết thúc truy vấn với mệnh đề group, kết quả trả về của bạn mang hình thức như một danh sách của danh sách.

Nếu bạn cần tham chiếu các kết quả của một group operation, bạn có thể sử dụng từ khóa into để tạo ra một định danh có thể được truy vấn thêm. Các chỉ truy vấn sau trả về những nhóm có chứa nhiều hơn hai khách hàng.

// custQuery is an IEnumerable<IGrouping<string, Customer>>
  var custQuery =
    from cust in customers
    group cust by cust.City into custGroup
    where custGroup.Count() > 2
    orderby custGroup.Key
    select custGroup;
  }

Để biết thêm thông tin, xem group clause (C# Reference).

Nối (Joining)


Sử dụng mệnh đề join để có thể trả về 1 kết quả được tổng hợp từ nhiều nguồn lại với nhau. Ví dụ, bạn có thể sử dụng join để tìm tất cả các khách hàng và nhà phân phối có cùng một vị trí.

    var innerJoinQuery =
    from cust in customers
    join dist in distributors on cust.City equals dist.City
    select new { CustomerName = cust.Name, DistributorName = dist.Name };


Để biết thêm thông tin, xem join clause (C# Reference).

Chọn (Selection)


Mệnh đề select xác định kiểu trả về của truy vấn LINQ. Ví dụ, bạn có thể xác định xem kết quả của bạn sẽ trả về 1 đối tượng Customer hoàn chỉnh với đầy đủ các thuộc tính hoặc một số loại kết quả hoàn toàn khác nhau dựa trên tính toán hay tạo đối tượng mới.

Khi bạn dùng mệnh select để trả về 1 kiểu dữ liệu mới thì đó được gọi là chiếu( projection). Việc sử dụng projection để chuyển đổi dữ liệu là một khả năng mạnh mẽ của các biểu thức truy vấn LINQ. 

Để biết thêm thông tin, xem Data Transformations with LINQ (C#)select clause (C# Reference).

4. Lời kết


LINQ là 1 công nghệ rất mạnh trong .NET Framework, nhờ LINQ thì việc truy vấn dữ liệu từ bất cứ nguồn dữ liệu nào đều dễ dàng và thống nhất 1 cú pháp chung. Người ta ví rằng 1 developer học C# nhưng không biết đến LINQ cũng giống như khi bạn sang Trung Quốc nhưng không đến Vạn Lý Trường Thành :)). Hi vọng qua biết viết này đã giúp bạn hiểu rõ về LINQ và sử dụng thành thạo nó trong nhiều dự án sắp tới. 


EmoticonEmoticon