.NET · Entity Framework · Full Text Search

Entity Framework Core 2.0: Những điều bên trong bộ mã nguồn mở EF Core 2.0

Sau gần 2 năm tham gia phát triển ứng dụng iOS, mình được yêu cầu chuyển qua nhóm Backend để mở rộng và cải tiến API cho các ứng dụng mobile của công ty. Nhiệm vụ đầu tiên là phải tìm hiểu sâu về Entity Framework do các yêu cầu đặc thù của dự án. Đây là một framework ra đời từ nhiều năm trước và thực sự mang lại nhiều lợi ích cho các ứng dụng .NET, nên có vô số bài viết về nó bằng Tiếng Anh cũng như Tiếng Việt. Nhưng hầu hết các bài viết đều chỉ dừng ở mức hướng dẫn sử dụng EF, rất hiếm khi viết sâu về những điều bên trong EF, kể cả bằng Tiếng Anh, hoặc cũng có thể do khả năng tìm kiếm của mình chưa đủ cảnh giới. Nhưng may là hiện tại thì cả EF 6 và Core 2.0 đều đã được open source, nên chúng ta có thể tải source code về nghiên cứu và debug. Sau gần nửa tháng đắm say với cả 2 bộ mã nguồn này, mình quyết định viết một bài cho các bạn muốn tìm hiểu sâu hơn về EF. Sau khi đọc bài viết này, mình hi vọng các bạn có thể nắm được điều gì xảy ra bên trong EF khi chúng ta thực thi một câu truy vấn.

Trước khi vào phần nội dung chính, mình giải thích tại sao mình lại nói là 2 bộ mã nguồn mở EF6 và EF Core 2.0. Ban đầu EF Core được gọi là EF7, trong quá trình phát triển thì EF7 được đổi tên thành EF Core. Về ý tưởng và cách sử dụng thì EF 6 và EF Core khá giống nhau, nhưng về cách thực hiện bên trong thì hoàn toàn khác nhau, EF Core được viết lại gần như toàn bộ với mục tiêu giúp cộng đồng có thể dễ dàng hơn trong việc thêm mới một Database Provider. Hơn nữa, việc sử dụng Remotion.Linq giúp EF Core có thể giảm thiểu viết code cho quá trình compile, translate và generate câu truy vấn. Bài viết này mình chỉ đề cập tới EF Core 2.0, còn EF6 sẽ được viết ở một bài khác trong tương lai.

  1. Các bước thực thi câu truy vấn dữ liệu trong EF Core 2.0

Phần tiếp theo của bài viết này, mình sẽ giới thiệu về các bước chính sẽ xảy ra khi chúng ta viết một câu lệnh truy xuất hoặc cập nhật dữ liệu trong EF Core 2.0. Ví dụ:

dbConext.Product.Where(x => x.Title.CompareTo(“Book”) > 0)

Các bước thực thi trong Entity Framework
Các bước thực thi câu truy vấn trong Entity Framework

1.1. Expression, IModel (Metadata): Linh hồn của Entity Framework.

Trước khi tìm hiểu EF, chúng ta phải hiểu về vai trò và chức năng của Expression và Metadata trong EF. Quay trở lại thời điểm trước khi ORM ra đời, ứng dụng của chúng ta có những class để mô hình hóa các đối tượng và các table để đặc tả dữ liệu lưu trữ trong cơ sở dữ liệu. Khi đó, chúng ta phải viết các phần trung gian (ADO.NET chẳng hạn) để chuyển đổi giữa đối tượng này với nhau. Câu hỏi đặt ra là có cách nào để viết các câu lệnh truy vấn trên các class và sau đó chuyển đổi thành các câu lệnh truy vấn dưới CSDL?

Câu hỏi trên chứa đựng hai ý chính:

  • Câu truy vấn (Expression)?
  • Chuyển đổi Expression thành các câu lệnh SQL? 

Để hiện thực ý thứ nhất, .NET sử dụng Expression Tree (abstract syntax tree, Expression Tree, hay chúng thường gọi là Linq). Ví dụ: Product.Where(x => x.Title.CompareTo(‘Book’) > 0) sẽ được phân rã thành Expression Tree như sau:

Expression Tree trong Entity Framework
Cây nhị phân trong Entity Framework

Để có thể chuyển đổi Expression thành các câu truy vấn dưới CSDL, .NET kết hợp Expression với Metadata. Thực chất Metadata là tập hợp những đặc tả, mô tả mối quan hệ giữa các class với các table, giữa các property với column và data type trong C# với data type trong CSDL, …. Sở dĩ chúng ta cần Metadata là vì tên bảng và column trong CSDL có thể không trùng với tên class và property trên .NET, và tất nhiên tên các data type không giống nhau giữa CSDL và .NET. Ngoài ra, thì nó còn giúp cho EF có thể được áp dụng được với nhiều hệ CSDL khác nhau như MySQL, SQL Server, …

Câu hỏi là: Metadata nằm ở đâu trong mô hình Code First (cả EF Core và EF 6)? Phải chăng Metadata không cần thiết trong Code First?

Câu trả lời là EF khởi tạo Metatata tại thời điểm runtime dựa vào Naming Convention, Attribute và FluentAPI. Và Metadata được lưu trữ tại MetadataWorkspace (EF6) và Model (EF Core) trong DbContext. Ví dụ: EF qui định cách đặt tên Id cho khóa chính, hoặc chỉ định Attribute [Key] ,..

Với mô hình Database First (EF6) thì Metadata được mô tả khi xây dựng hệ thống trong file *.EDMX. Tại thời điểm runtime, những file này sẽ được tải lên bộ nhớ. Kể từ đó, không có sự khác biệt nào giữa Code First và Database First.

Về cơ bản Metadata(Entity Data Model) chứa ba thành phần chính:

  • CSDL: lưu trữ đặc tả entity class, property
  • MSL: mô tả mối quan hệ giữa các đối tượng trong CSDL với SSDL
  • SSDL: lưu trữ thông tin về database, table, column, datatype của database.

Trong EF6, chúng có thể được gọi là DataSpace, tương ứng với: C-Space, CS-Space và S-Space.

Entity Data Model
Kiến trúc Entity Framework

Như vậy, chúng ta có thể thấy Expression và Metadata không thể thiếu trong Entity Framework, nó được coi là linh hồn của Entity Framework. Mục tiêu chính của EF là thực thi các Expression. Và Metadata là gia vị, là thông tin bổ trợ cần thiết cho quá trình chuyển đổi Expression thành câu lệnh SQL.

1.2 Bước 1: Khởi tạo Query Provider

Xem xét câu lệnh:

dbContext.Products.Where(x => x.Title.CompareTo(‘Book’) > 0).ToList().

Như chúng ta đã biết thì dbContext.Products là một DbSet thừa kế từ IQueryable. Chúng ta cùng xem xét IQueryable bao gồm những thành phần gì:

public interface IQueryable : IEnumerable
{
Expression Expression { get; }
Type ElementType { get; }
IQueryProvider Provider { get; }
}

Chúng ta có thể dễ dàng nhận ra: Expression: x => x.Title.CompareTo(‘Book’) > 0, Type: Product. Vậy IQueryProvider là gì, khởi tạo ở đâu và khi nào?

IQueryProvider: Defines methods to create and execute queries that are described by an System.Linq.IQueryable

Như vậy, IQuery Provider là một đối tượng có nhiệm vụ thực thi câu lệnh Linq ở trên dựa vào DbProvider cụ thể, có thể là MemoryProvider, SqlServerProvider, MySqlProvider, … Dựa vào các giá trị trong DbContextOptionBuilder, DbContext sẽ khỏi tạo QueryProvider theo sơ đồ sau đây:

Khởi tại Query Provider

1.3. Bước 2: Thực thi Expression

Sau khi đã khởi tạo thành công QueryProvider, thì QueryProvider sẽ thực thì Expression. Các bước tiếp theo là trình tự thực hiện một Expression, bao gồm: Translate, Compile và Generate SQL.

1.4 Bước 3: Chuyển đổi Expression

Trong phần nầy, chúng ta sẽ tìm hiểu các bước EF chuyển đổi các câu lệnh truy vấn, đặc biệt là các .NET Built-In method như: CompareTo, Contains, Equals, …

Ví dụ: Product.Where(x => x.Title.CompareTo(‘Book’) > 0), thì Expression Tree mô tả biểu thức này sẽ như hình bên trái, và hình bên phải là biểu thức sau khi được translate.

Chuyển đổi Expression trong Entity Framework
Chuyển đổi Expression trong Entity Framework

Câu hỏi: Tại sao chúng ta cần quá trình biến đổi này?

Với cùng một câu lệnh Linq thì sẽ có những biểu thức khác nhau giữa các CSDL khác nhau. Nên chúng ta cần chuyển đổi câu lệnh Linq thành các Expression khác nhau tương ứng với từng CSDL. Chẳng hạn: Product.Where(x => x.Title.Contains(“Book”)), với Sql Server, sẽ chuyển thành CharIndex, với hệ CSDL khác có thể chuyển đổi thành LIKE.

Câu hỏi: Tại sao cần phải chia quá trình chuyển đổi này thành hai bước: Hình bên trái và hình bên phải? 

Bước đầu tiên được hỗ trợ bởi Compiler tại thời điểm building time, không phụ thuộc vào DbProvider. Hay nói cách khác x => x.Title.Contains(“Book”) là một cú pháp biểu diễn Expression Tree, được hỗ trợ bởi Compiler, trong quá trình compile nó sẽ được dịch sang dạng cây như hình bên trái.

Bước thứ hai được chuyển đổi tại thời điểm running time, tùy thuộc vào DbProvider, EF sẽ chuyển sang các cây khác nhau. Và để tăng performance thì bước thứ 2 sẽ được hỗ trợ cache.

Quá trình chuyển đổi Expression

Với mỗi DbProvider, EF sẽ có một TranslatingVisitor, nó chứa danh sách các Translators. Mỗi Translator thường chỉ làm nhiệm vụ chuyển đổi một dạng Expression nhất định ( ngoại trừ các CompositeTranslators, nó chứa danh sách các Translators).

EF Core 2.0 sử dụng Remotion.Linq, các câu lệnh phổ biến sẽ được xử lý bởi Remotion.Linq. EF Core, chỉ viết Translators cho mục đính optimize.

Chúng ta không cần quá quan tâm tới việc liệu quá trình này có ảnh hưởng tới performance của ứng dụng không. Vì toàn bộ quá trình chuyển đổi (bao gồm cả translate, compile và generate SQL) chỉ diễn ra một lần duy nhất tại thời điểm câu truy vấn được thực thi lần đầu.  Chiến lược cache của EF cũng khá hiệu quả, tất cả câu query chỉ được biên dịch một lần duy nhất. Ví dụ: Product.Where(x => x.Title.CompareTo(keyword) > 0), sẽ chỉ chuyển đổi tại lần đầu, các lần sau sẽ không cần chuyển đổi, cho dù giá trị của keyword có thay đổi. Tóm lại, việc cache không phụ thuộc vào giá trị của các biến trong expression, chỉ phụ thuộc vào cấu trúc của expression và tên biến.

1.5: Bước 4: Chuyển đổi Expression thành câu truy vấn

GenerateSQL

2. Ví dụ.

Như chúng ta thấy thì EF Core hiện tại chưa hỗ trợ Full Text Search. Để thực hiện Full Text Search trong EF Core 2.0 thì có nhiều cách. Chẳng hạn sử dụng hàm FromSql của EF Core 2.0 hoặc sử dụng table-value function.

Trong bài này mình sẽ giới thiệu cách khác nhằm mục đích giúp chúng ta hiểu rõ hơn về Entity Framework.

Đầu tiên, chúng ta viết 2 hàm:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StudyEfCore.Utilities
{
public static class SqlFunctions
{
public static bool Contains(string text, string word)
{
throw new NotSupportedException("This is a SQL method which does not support call directly.");
}
public static bool Freetext(string text, string word)
{
throw new NotSupportedException("This is a SQL method which does not support call directly.");
}
}
}

view raw
SqlFunctions.cs
hosted with ❤ by GitHub

Tiếp theo tạo một Expression mới:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Query.Expressions;
namespace StudyEfCore.Expressions
{
public class FtsExpression : SqlFunctionExpression
{
public FtsExpression(string method, IEnumerable<Expression> arguments) : base(method, typeof(bool), arguments) { }
}
}

view raw
FtsExpression.cs
hosted with ❤ by GitHub

Tiếp theo, tạo một Tranlator để chuyển đổi Contains, Freetext thành các biểu thức FtsExpression

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
using StudyEfCore.Expressions;
using StudyEfCore.Utilities;
namespace StudyEfCore.Translators
{
public class FtsTranslator : IMethodCallTranslator
{
private static readonly MethodInfo ContainsMethodInfo
= typeof(SqlFunctions).GetMethod(nameof(SqlFunctions.Contains), new[] { typeof(string), typeof(string) });
private static readonly MethodInfo FreetextMethodInfo
= typeof(SqlFunctions).GetMethod(nameof(SqlFunctions.Freetext), new[] { typeof(string), typeof(string) });
public virtual Expression Translate(MethodCallExpression expr)
=> Equals(expr.Method, ContainsMethodInfo)
? new FtsExpression("CONTAINS", new[] { expr.Arguments.First(), expr.Arguments.Last() })
: Equals(expr.Method, FreetextMethodInfo)
? new FtsExpression("FREETEXT", new[] { expr.Arguments.First(), expr.Arguments.Last() })
: null;
}
}

view raw
FtsTranslator.cs
hosted with ❤ by GitHub

Các bạn có thể tải ví dụ trên Github cá nhân của mình. Ví dụ này chỉ nhằm mục đích giới thiệu các thành phần bên trong EF Core.

Full Text Search trong Entity Framework

 

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s