Truy vấn dữ liệu trong Entity Framework với LINQ to Entities

    0

    Có hai công việc phải làm đối với mọi chương trình có sử dụng cơ sở dữ liệu: (1) lấy dữ liệu từ cơ sở dữ liệu; (2) lưu thay đổi ngược trở lại cơ sở dữ liệu.

    Đối với việc đọc dữ liệu, bạn cần sử dụng đến LINQ to Entities. Để lưu thay đổi, bạn cần sử dụng đến DbContext API.

    Trong bài học này chúng ta sẽ làm quen với LINQ to Entities và sử dụng nó để viết các truy vấn cơ bản. Trong các bài học tiếp theo của phần này bạn sẽ lần lượt học các kỹ thuật phức tạp hơn như sắp xếp, lọc, truy vấn dữ liệu quan hệ.

    Chuẩn bị project thử nghiệm

    Để thực hiện các ví dụ trong bài học này và các bài tiếp theo chúng ta sử dụng solution đã tạo ở bài học Thực hành xây dựng EDM.

    1. Bước 1. Bổ sung project P01_SimpleQueries (ConsoleApp .NET Framework) vào solution.
    2. Bước 2. Tham chiếu sang thư viện Models và DataAccess.
    3. Bước 3. Cài đặt Entity Framework.
    4. Bước 4. Bổ sung node connectionStrings vào App.config:
    <connectionStrings>
      <add name="UniversityContext" providerName="System.Data.SqlClient" connectionString="Data Source=(localdb)\mssqllocaldb;Initial Catalog=UniversityDatabase;Integrated Security=True"/>
    </connectionStrings>

    Cuối cùng, thiết lập P01_SimpleQueries làm Startup project.

    LINQ to Entities

    LINQ (Language Integrated Query) là một framework chuyên dụng cho truy vấn dữ liệu trong .NET nói chung chứ không phải là một thành phần dành riêng cho Entity Framework. LINQ có thể xem là một ngôn ngữ thống nhất để truy vấn dữ liệu, bất kể nguồn dữ liệu là gì.

    Tùy thuộc vào từng loại nguồn dữ liệu, người ta phải xây dựng các provider riêng hỗ trợ LINQ. Do đó, có nhiều “loại” LINQ khác nhau, được gọi theo tên của provider.

    Ví dụ, trong các khóa học lập trình C# cơ bản bạn hẳn đã được học qua về LINQ. Nguồn dữ liệu trong trường hợp này là các object trong bộ nhớ của chương trình. Provider dành cho loại nguồn dữ liệu này được gọi là LINQ to Objects. Do đó, loại LINQ đó cũng được gọi là LINQ to Objects. Bạn dùng ngôn ngữ LINQ để truy vấn các object trong bộ nhớ.

    Đối với Entity Framework, loại provider này có tên gọi là LINQ to Entities. Loại provider này phức tạp hơn nhiều vì nó phải chuyển đổi truy vấn viết bằng LINQ sang truy vấn SQL (để thực thi trên cơ sở dữ liệu). Trong trường hợp này bạn sử dụng ngôn ngữ LINQ để truy vấn tới cơ sở dữ liệu (thông qua hỗ trợ của Entity Framework).

    Do ngôn ngữ LINQ là thống nhất, những gì bạn đã biết khi học LINQ to Objects có thể tiếp tục áp dụng trên LINQ to Entities. Bạn không cần biết những gì diễn ra phía sau vì provider tương ứng đã lo hết giúp bạn.

    Lưu ý rằng LINQ có hai cú pháp khác nhau: query syntax và method syntax. Các ví dụ ở đây đều sử dụng method syntax. Bạn có thể tự mình tìm hiểu cú pháp query syntax. Việc sử dụng cú pháp nào hoàn toàn là sở thích cá nhân.

    Cơ bản về LINQ to Entities

    Truy vấn cơ bản mà bạn đã từng gặp là lấy toàn bộ dữ liệu từ một bảng. Nó tương đương với truy vấn SELECT * FROM … của SQL.

    Ví dụ

    Hãy cùng code ví dụ sau:

    using DataAccess;
    using static System.Console;
    
    namespace P01_SimpleQueries
    {
        class Program
        {
            static void Main(string[] args)
            {
                Title = "Simple queries";
                using (var context = new UniversityContext())
                {
                    PrintAllCourses(context);
                }
    
                ReadLine();
            }
    
            static void PrintAllCourses(UniversityContext context)
            {
                WriteLine("### COURSES ###");
                foreach (var c in context.Courses)
                {
                    WriteLine($"({c.Id}) {c.Name}");
                }
            }
        }
    }

    Kết quả chạy chương trình:

    Dễ dàng thấy, để lấy toàn bộ dữ liệu từ bảng bạn không thực sự cần viết truy vấn riêng. Chỉ cần duyệt qua DbSet tương ứng thì Entity Framework sẽ tự sinh ra và gửi truy vấn đến cơ sở dữ liệu để lấy tất cả dữ liệu cho bạn.

    Giờ bạn đặt dấu break ở dòng WriteLine($"({c.Id}) {c.Name}");, khi chạy chương trình sẽ tạm dừng ở đây. Khi chương trình đang tạm dừng, bạn đặt con trỏ chuột lên trên “Courses” và mở xem property Sql bạn sẽ nhìn thấy truy vấn như sau:

    SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[Credit] AS [Credit], 
        [Extent1].[Description] AS [Description], 
        [Extent1].[Teacher_Id] AS [Teacher_Id]
        FROM [dbo].[Courses] AS [Extent1]

    Đây chính là truy vấn SQL do Entity Framework tự động sinh ra để lấy dữ liệu giúp bạn. Vì là truy vấn sinh tự động, nó không hoàn toàn giống loại truy vấn do bạn tự mình viết.

    Truy vấn này được gửi tới cơ sở dữ liệu khi chương trình cần sử dụng dữ liệu, trong trường hợp này chính là lúc in kết quả đầu tiên ra màn hình trong vòng lặp foreach.

    Cơ chế thực thi truy vấn trong Entity Framework

    Khi bắt đầu đi sâu vào truy vấn LINQ to Entity, bạn cần lưu ý một số vấn đề.

    Thứ nhất, Entity Framework không hề lấy tất cả kết quả về cùng lúc. Thay vào đó, Entity Framework duy trì kết nối và lần lượt đọc dữ liệu về nếu có yêu cầu từ chương trình. Chỉ khi nào vòng lặp foreach kết thúc, toàn bộ dữ liệu mới được đọc hết.

    Thứ hai, kết quả thu về là read-only và forward-only, giống như khi bạn sử dụng DataReader với SqlCommand.

    Thứ ba, kết quả của truy vấn như trên không được lưu lại trong chương trình. Mỗi khi bạn duyệt DbSet như trên, Entity Framework sẽ phát truy vấn tới cơ sở dữ liệu. Ví dụ, nếu bạn viết 3 vòng lặp foreach như trên, Entity Framework sẽ phát đi 3 truy vấn. Kết quả từ các truy vấn trước đó không hề được lưu lại để tái sử dụng.

    Do đó, nếu muốn lưu kết quả lại để tái sử dụng, bạn hãy gọi phương thức LINQ ToList() hoặc ToArray():

    static void PrintAllCoursesX2(UniversityContext context)
    {
        WriteLine("### COURSES ###");
        var courses = context.Courses.ToList();
    
        foreach (var c in courses)
        {
            WriteLine($"({c.Id}) {c.Name}");
        }
    
        foreach (var c in courses)
        {
            WriteLine($"({c.Id}) {c.Name}");
        }
    }

    Giải pháp trên cũng giúp Entity Framework không cần phát đi nhiều truy vấn giống hệt nhau đến cơ sở dữ liệu. Qua đó giúp nâng cao hiệu suất của chương trình.

    Thứ tư, truy vấn không được thực hiện ngay lập tức tại nơi phát lệnh. Trái lại, chỉ khi nào chương trình thực sự cần đến dữ liệu truy vấn mới được thực hiện.

    Trong ví dụ trên, lúc bạn in kết quả ra sẽ cần đến dữ liệu. Do vậy truy vấn sẽ được thực hiện ở chu kỳ đầu tiên của vòng lặp foreach. Cơ chế này có tên gọi là deferred execution (thực thi trễ).

    Sắp xếp dữ liệu với LINQ to Entities

    Lọc và sắp xếp là những khả năng đặc trưng của LINQ.

    Xây dựng phương thức sau và gọi nó trong hàm Main():

    static void PrintAllCoursesSorted(UniversityContext context)
    {
        var query = context.Courses.OrderBy(c => c.Name);
        WriteLine("### COURSES ###");
        foreach (var c in query)
        {
            WriteLine($"({c.Id}) {c.Name}");
        }
    }

    Đoạn code trên sử dụng LINQ để viết một truy vấn lấy tất cả dữ liệu về Course đồng thời sắp xếp theo thứ tự tăng dần của tên gọi.

    Bạn dễ dàng để ý thấy sự khác biệt so với ở trên là chúng ta đã sử dụng phương thức OrderBy. Phương thức này nhận một hàm lambda làm tham số nhằm chỉ định property nào của Course sẽ được dùng làm tiêu chí sắp xếp.

    OrderBy luôn sắp xếp dữ liệu theo thứ tự tăng dần. Nếu cần sắp xếp theo thứ tự giảm dần, bạn viết truy vấn với OrderByDescending như sau:

    var query = context.Courses.OrderByDescending(c => c.Name);

    Nếu cần sắp xếp theo nhiều tiêu chí, bạn có thể ghép nối OrderBy (OrderByDescending) với ThenBy (ThenByDescending) như sau:

    var query = context.Courses
                    .OrderBy(c => c.Name)
                    .ThenBy(c=>c.Credit)
                    .ThenByDescending(c=>c.Description);

    Trong truy vấn trên, bạn sắp xếp (tăng dần) theo tên, sau đó tới số tín chỉ, và cuối cùng (giảm dần) theo mô tả.

    Lọc dữ liệu với LINQ to Entities

    Lọc dữ liệu nghĩa là bạn chỉ chọn lấy những object nào phù hợp với tiêu chí của mình. Các truy vấn lọc trong LINQ to Entities sẽ được chuyển thành SELECT … FROM … WHERE của SQL.

    Nếu bạn đã từng học SQL bạn hẳn còn nhớ Where là clause lằng nhằng phức tạp nhất.

    LINQ to Entities cũng dùng phương thức có tên gọi Where để lọc dữ liệu:

    static void PrintAllCoursesFiltered(UniversityContext context)
    {
        var query = context.Courses
            .Where(c => c.Credit >= 4);
        WriteLine("### COURSES ###");
        foreach (var c in query)
        {
            WriteLine($"({c.Id}) {c.Name} ({c.Credit} credits)");
        }
    }

    Trong ví dụ trên bạn viết truy vấn để chỉ lấy những khóa học từ 4 tín chỉ trở lên:

    Phương thức Where nhận tham số đầu vào là một phương thức có giá trị trả về kiểu bool (Func<T, bool>), với T là kiểu cơ sở của kết quả truy vấn.

    Trong ví dụ trên chúng ta sử dụng một hàm lambda c => c.Credit >= 4. Tham số đầu vào c có kiểu là Course, vì đây là kiểu dữ liệu cơ sở của kết quả truy vấn.

    Thay vì tạo biểu thức logic phức tạp trong một phương thức Where, bạn cũng có thể ghép nối nhiều phương thức Where với nhau:

    var query = context.Courses
        .Where(c => c.Credit >= 4)
        .Where(c => c.Name.Contains("magic"));

    Bạn có thể ghép nối cả Where và OrderBy (OrderByDescending) để vừa lọc vừa sắp xếp:

    var query = context.Courses
        .Where(c => c.Credit >= 4) //.Where(c => c.Name.Contains("magic"));
        .OrderBy(c => c.Credit)
        .ThenBy(c => c.Name);

    Thông thường người ta sẽ thực hiện lọc trước rồi mới sắp xếp.

    Projection với LINQ to Entities

    Trong các nội dung trên, kết quả của truy vấn là danh sách các object thuộc kiểu cơ sở của DbSet (Course).

    Tuy nhiên, trong nhiều trường hợp bạn không cần sử dụng trọn vẹn cả object thuộc kiểu cơ sở. Ví dụ, nếu bạn chỉ cần thông tin về tên và số tín chỉ để in ra thì không cần sử dụng đến những trường còn lại của Course.

    Nếu sử dụng các truy vấn như bên trên, bạn đang lấy ra những dữ liệu dư thừa. Dĩ nhiên, lấy ra nhiều dữ liệu thừa không có gì tốt đẹp cho chương trình.

    LINQ to Entities cho phép bạn chỉ định những trường dữ liệu cần lấy ra thông qua cơ chế projection.

    Hãy cùng thực hiện ví dụ:

    static void CourseProjection(UniversityContext context)
    {
        var query = context.Courses
            .Where(c => c.Credit > 3)
            .OrderBy(c => c.Name)
            .Select(c => c.Name);
        foreach (var c in query)
        {
            WriteLine($"{c}");
        }
    }

    Trong truy vấn trên, ngoài hai phương thức Where và OrderBy bạn đã biết, bạn thấy có phương thức lạ Select. Select chính là phương thức giúp bạn thực hiện projection – lựa chọn những thông tin cần thiết để trả về trong kết quả của truy vấn.

    Phương thức Select ở trên lựa chọn duy nhất giá trị Name (kiểu string) của Course (bỏ qua hết các property còn lại). Do vậy, kết quả trả về của query giờ sẽ là IQueryable<string>, thay vì IQueryable<Course> như bình thường.

    Nói cách khác, khi bạn duyệt kết quả trong vòng lặp foreach, mỗi phần tử của nó là kiểu string chứ không phải là kiểu Course như trước nữa.

    Giờ hãy thực hiện một truy vấn khác:

    static void CourseProjectionAnonymousType(UniversityContext context)
    {
        var query = context.Courses
            .Where(c => c.Credit > 3)
            .OrderBy(c => c.Name)
            .Select(c => new { c.Name, c.Credit });
    
        WriteLine("### COURSES ###");
        foreach (var c in query)
        {
            WriteLine($"({c.Id}) {c.Name} ({c.Credit} credits)");
        }
    }

    Truy vấn này khác biệt ở chỗ trong phương thức Select bạn thực hiện projection từ kiểu Course sang một kiểu vô danh (anonymous type). Trong đó, kiểu vô danh chứa giá trị Name và Credit của Course.

    Nếu bạn đặt con trỏ lên trên “query” sẽ thấy thông tin như sau:

    Như vậy, kiểu của query giờ là IQueryable<'a>, mỗi object trong danh sách kết quả trả về giờ thuộc một kiểu vô danh chỉ chứa hai property Name (string) và Credit (int).

    Dĩ nhiên bạn có thể truy xuất các property này như đối với các object bình thường:

        foreach (var c in query)
        {
            WriteLine($"({c.Name} ({c.Credit} credits)");
        }

    Tìm kiếm object với LINQ to Entities

    Tìm kiếm khác biệt với lọc bạn đã học ở phần trên ở chỗ nó trả về một object duy nhất (nếu có) thay vì một danh sách object.

    Bạn cần tìm kiếm object trong trường hợp cần cập nhật hoặc xóa một object xác định.

    Để tìm kiếm object, LINQ to Entities cung cấp một số phương pháp khác nhau: (1) sử dụng phương thức Single hoặc SingleOrDefault; (2) Sử dụng phương thức First hoặc FirstOrDefault; (3) Sử dụng phương thức Find.

    Sử dụng Single và SingleOrDefault

    static void GetSingleCourse(UniversityContext context)
    {
        var query = context.Courses.Where(c => c.Id == 13);
        var course = query.Single();// hoặc query.SingleOrDefault();
    
        WriteLine($"Course found: {course.Name} ({course.Credit} credits)");
    }

    Khi sử dụng Single (hoặc SingleOrDefault), bạn vẫn xây dựng truy vấn lọc dữ liệu LINQ như bình thường. Tuy nhiên, Single báo cho Entity Framework biết rằng bạn đang chờ đợi MỘT kết quả trả về duy nhất.

    Do vậy, nếu truy vấn trả về nhiều hơn một kết quả, hoặc không có kết quả nào, Single sẽ phát ra ngoại lệ System.InvalidOperationException: 'Sequence contains more than one element'.

    Ví dụ sau đây sẽ báo lỗi lúc chạy vì có nhiều hơn một kết quả trả về trong truy vấn:

    static void GetSingleCourse(UniversityContext context)
    {
        var query = context.Courses.Where(c => c.Name.Contains("magic"));
        var course = query.Single();
    
        WriteLine($"Course found: {course.Name} ({course.Credit} credits)");
    }

    Single và SingleOrDefault hoạt động cơ bản là giống nhau. Sự khác biệt là nếu trong trường hợp không có kết quả trả về, Single sẽ phát ra ngoại lệ trong khi SingleOrDefault đơn giản là trả về null. Nếu có nhiều hơn 1 kết quả trả về, cả Single và SingleOrDefault đều phát ra ngoại lệ.

    Bạn cũng có thể sử dụng cách viết tắt của Single (SingleOrDefault) như sau:

    var course = context.Courses.Single(c => c.Id == 13);

    Sử dụng First hoặc FirstOrDefault

    Single tương đối “cực đoan” khi đòi hỏi bạn phải đảm bảo rằng truy vấn chỉ trả về một kết quả duy nhất. Nó đặc biệt phù hợp khi bạn tìm kiếm object theo Id.

    Nếu muốn tìm một object bất kỳ phù hợp với yêu cầu, bạn có thể sử dụng First hoặc FirstOrDefault.

    Sự khác biệt nằm ở chỗ, nếu có nhiều kết quả trả về, First (FirstOrDefault) sẽ lấy object đầu tiên trong danh sách. Nếu không có kết quả trả về, First sẽ phát ra ngoại lệ, trong khi FirstOrDefault trả lại null (không phát ra ngoại lệ).

    Cú pháp sử dụng First (FirstOrDefault) giống hệt như Single (SingleOrDefault):

    static void GetFirstCourse(UniversityContext context)
    {
        //var query = context.Courses.Where(c => c.Id == 13);
        //var query = context.Courses.Where(c => c.Name.Contains("magic"));
        //var course = query.First();
    
        //var course = context.Courses.First(c => c.Id == 13);
        var course = context.Courses.First(c => c.Name.Contains("magic"));
    
        WriteLine($"Course found: {course.Name} ({course.Credit} credits)");
    }

    Sử dụng Find

    Find hoạt động khác biệt so với Single hoặc First ở các điểm sau:

    1. Find tìm kiếm trong những object đã tải từ cơ sở dữ liệu hoặc những object đã gắn với context (nhưng chưa lưu vào cơ sở dữ liệu). Nói chung là Find tìm kiếm những object đang nằm trong bộ nhớ.
    2. Nếu object chưa tải vào bộ nhớ, Find cũng tìm kiếm trong cơ sở dữ liệu.

    Như vậy, Find hiệu quả hơn vì nó không nhất thiết phải thực hiện truy vấn tới cơ sở dữ liệu.

    Ở khía cạnh khác, Single và First luôn tạo ra truy vấn tới cơ sở dữ liệu để tìm bản ghi phù hợp. Những object mới đang được context quản lý nhưng chưa lưu vào cơ sở dữ liệu sẽ không xuất hiện trong kết quả tìm kiếm.

    Cú pháp sử dụng Find rất đơn giản. Bạn chỉ cần truyền giá trị khóa làm tham số:

    static void FindCourse(UniversityContext context)
    {
        var course = context.Courses.Find(13); // tìm kiếm theo Id = 13
        WriteLine($"Course found: {course.Name} ({course.Credit} credits)");            
    }

    Lưu ý rằng, mặc dù Find chấp nhận không giới hạn tham số đầu vào nhưng bạn chỉ có thể truyền giá trị khóa cho Find. Nếu bạn truyền những giá trị khác Find sẽ báo lỗi khi chạy (mặc dù vẫn dịch thành công).

    Nhóm dữ liệu với LINQ to Entities

    Khi nhóm dữ liệu, bạn chia các kết quả thành từng nhóm theo một tiêu chí nào đó. Ví dụ, bạn muốn chia các khóa học theo số tín chỉ. Khi đó các khóa học 2 tín chỉ sẽ nằm cùng nhóm, các khoác học 3 tín chỉ sẽ nằm cùng nhóm, v.v..

    Cách thực hiện nhóm dữ liệu trong LINQ to Entities khá đơn giản với phương thức GroupBy. Hãy xem cách sử dụng của phương thức này qua ví dụ sau:

    static void GroupCourses(UniversityContext context)
    {
        var result = context.Courses.GroupBy(c => c.Credit);
        foreach(var r in result)
        {
            WriteLine($"{r.Key}-Credit Courses:");
            foreach(var c in r)
            {
                WriteLine($"  -> {c.Name}");
            }
        }
    }

    Kết quả thu được như sau:

    Khi sử dụng GroupBy bạn cần cung cấp property được sử dụng cho tiêu chí phân nhóm. Trong ví dụ trên số tín chỉ (Credit) được dùng làm tiêu chí phân nhóm.

    Mỗi nhóm trong kết quả thực hiện truy vấn có kiểu IGrouping, bao gồm một giá trị khóa (Key) và danh sách object chứa trong nhóm. Do đó trong ví dụ trên chúng ta có thể dễ dàng duyệt qua từng nhóm (vòng foreach thứ nhất). Trong mỗi nhóm có thể in ra giá trị khóa và danh sách object trong nhóm (vòng foreach thứ hai).

    Bạn có thể kết hợp cả lọc, sắp xếp và projection trong truy vấn chia nhóm. Ví dụ sau đây thực hiện sắp xếp các nhóm theo thứ tự tăng dần của số tín chỉ. Trong mỗi nhóm lại sắp xếp tăng dẫn theo tên của khóa học.

    static void GroupCoursesOrdered(UniversityContext context)
    {
        var result = context.Courses                
            .GroupBy(c => c.Credit)
            .OrderByDescending(g => g.Key)
            .Select( g=> new { GroupName = g.Key, GroupItems = g.OrderBy(c=>c.Name)})
            ;
    
        foreach (var r in result)
        {
            WriteLine($"{r.GroupName}-Credit Courses:");
            foreach (var c in r.GroupItems)
            {
                WriteLine($"  -> {c.Name}");
            }
        }
    }

    Ở đây chúng ta đã vận dụng projection cho nhóm để dễ dàng xử lý kết quả.

    Kết luận

    Trong bài học này chúng ta đã cùng học cách viết các truy vấn cơ bản nhất sử dụng LINQ to Entities.

    Thực tế cách viết truy vấn ở đây không có gì khác biệt so với LINQ to Objects (nếu bạn đã từng học). Tuy nhiên, cơ chế thực thi truy vấn của LINQ to Entities lại rất khác biệt mà bạn cần lưu ý khi sử dụng trong chương trình.

    Bạn có thể tải mã nguồn từ link dưới đây để tham khảo thêm.

    Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần Bình luận ở cuối trang. Nếu cần trao đổi riêng, hãy gửi email hoặc nhắn tin qua form liên hệ. Nếu bài viết hữu ích với bạn, hãy giúp chúng tôi chia sẻ tới mọi người. Cảm ơn bạn!

    Bình luận

    avatar
      Đăng ký theo dõi  
    Thông báo về