Entity Framework Core (EF Core) là một ORM (Object-Relational Mapper) mạnh mẽ cho phép các nhà phát triển .NET tương tác với cơ sở dữ liệu một cách dễ dàng. Tuy nhiên, việc sử dụng EF Core không đúng cách có thể dẫn đến các vấn đề về hiệu suất. Bài viết này sẽ cung cấp một hướng dẫn toàn diện về cách tối ưu hóa hiệu suất EF Core, từ những kỹ thuật cơ bản đến nâng cao, giúp bạn xây dựng các ứng dụng nhanh chóng và hiệu quả.
Thông thường, việc tạo và hủy một DbContext là một quy trình nhẹ nhàng và không ảnh hưởng đáng kể đến hiệu suất. Tuy nhiên, trong các ứng dụng có hiệu suất cao, việc liên tục tạo mới các DbContext có thể gây ra sự chậm trễ. DbContext Pooling giải quyết vấn đề này bằng cách tái sử dụng các instance của context. Thay vì tạo một instance mới mỗi khi cần, EF Core sẽ lấy một instance từ một pool, reset trạng thái của nó và trả về cho bạn. Điều này giúp giảm đáng kể chi phí khởi tạo context, đặc biệt trong các ứng dụng web có lưu lượng truy cập lớn.
Để sử dụng DbContext Pooling trong ứng dụng ASP.NET Core, bạn chỉ cần thay thế `AddDbContext` bằng `AddDbContextPool` trong `Startup.cs` hoặc `Program.cs`:
builder.Services.AddDbContextPool<YourDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("YourConnectionString")));
Tham số `poolSize` của `AddDbContextPool` xác định số lượng instance tối đa được lưu giữ trong pool (mặc định là 1024). Khi vượt quá `poolSize`, EF Core sẽ quay trở lại hành vi tạo instance theo yêu cầu, không sử dụng pooling.
Khi EF Core nhận một cây truy vấn LINQ để thực thi, nó phải "biên dịch" cây đó, ví dụ: tạo SQL từ nó. Vì đây là một quá trình tốn kém, EF Core lưu trữ các truy vấn theo hình dạng cây truy vấn, để các truy vấn có cùng cấu trúc có thể tái sử dụng các đầu ra biên dịch được lưu trữ trong bộ nhớ cache. Tuy nhiên, EF Core vẫn phải thực hiện một số tác vụ trước khi có thể sử dụng bộ nhớ cache truy vấn nội bộ. Compiled Queries cho phép bạn biên dịch một truy vấn LINQ thành một delegate .NET một cách rõ ràng. Khi delegate này được tạo, nó có thể được gọi trực tiếp để thực thi truy vấn mà không cần cung cấp cây biểu thức LINQ. Điều này giúp bỏ qua việc tra cứu bộ nhớ cache và cung cấp cách tối ưu nhất để thực thi một truy vấn trong EF Core.
Để sử dụng Compiled Queries:
private static readonly Func<YourDbContext, int, Task<List<YourEntity>>> _compiledQuery =
EF.CompileAsyncQuery((YourDbContext context, int parameter) =>
context.YourEntities.Where(e => e.Property == parameter).ToListAsync());
// ...
var results = await _compiledQuery(dbContext, someValue);
Delegate này là thread-safe và có thể được gọi đồng thời trên các instance context khác nhau.
Một trong những cách đơn giản nhất để cải thiện hiệu suất EF Core là chỉ truy xuất dữ liệu bạn thực sự cần. Thay vì tải toàn bộ entity, hãy sử dụng `Select` để chỉ lấy các cột cần thiết. Điều này đặc biệt quan trọng khi bạn chỉ cần một vài thuộc tính từ một entity lớn.
Ví dụ, thay vì:
var blogs = await context.Blogs.ToListAsync();
foreach (var blog in blogs)
{
Console.WriteLine(blog.Url);
}
Hãy làm:
var blogUrls = await context.Blogs.Select(b => b.Url).ToListAsync();
foreach (var url in blogUrls)
{
Console.WriteLine(url);
}
Truy vấn thứ hai chỉ truy xuất cột `Url`, giảm đáng kể lượng dữ liệu được truyền từ cơ sở dữ liệu.
Index là yếu tố quan trọng nhất quyết định tốc độ truy vấn. Hãy đảm bảo rằng bạn đã tạo index cho các cột được sử dụng trong mệnh đề `WHERE` của bạn. Sử dụng các công cụ phân tích truy vấn của cơ sở dữ liệu để xác định các truy vấn chậm và các index còn thiếu. Cân nhắc sử dụng composite index cho các truy vấn lọc trên nhiều cột. Tuy nhiên, cần lưu ý rằng index cũng làm chậm các thao tác cập nhật, do đó, chỉ tạo index khi thực sự cần thiết.
Khi làm việc với các tập dữ liệu lớn, hãy sử dụng phân trang để chỉ truy xuất một phần dữ liệu tại một thời điểm. Sử dụng các toán tử `Skip` và `Take` để thực hiện phân trang. Tuy nhiên, cần lưu ý rằng việc sử dụng `Skip` và `Take` có thể không hiệu quả đối với các tập dữ liệu rất lớn. Trong trường hợp đó, hãy cân nhắc sử dụng **keyset pagination**.
Khi làm việc với các entity có quan hệ với nhau, hãy sử dụng **eager loading** (`Include`) khi bạn biết rằng bạn sẽ cần các entity liên quan. Sử dụng **explicit loading** hoặc **lazy loading** khi bạn chỉ cần các entity liên quan trong một số trường hợp nhất định. Tuy nhiên, cần cẩn thận với **lazy loading**, vì nó có thể dẫn đến vấn đề N+1, trong đó bạn thực hiện một truy vấn cho entity chính và sau đó thực hiện N truy vấn bổ sung để tải các entity liên quan.
Trong một số trường hợp, EF Core có thể không tạo ra truy vấn SQL tối ưu nhất. Trong những trường hợp này, bạn có thể sử dụng truy vấn SQL thô (`FromSqlRaw`) để có được hiệu suất tốt hơn. Tuy nhiên, cần lưu ý rằng việc sử dụng SQL thô có thể làm giảm tính di động của ứng dụng và khiến việc bảo trì trở nên khó khăn hơn.
Cách bạn thiết kế mô hình entity cũng có thể ảnh hưởng đến hiệu suất. Ví dụ, sử dụng các kiểu dữ liệu phù hợp cho các thuộc tính của bạn có thể giúp giảm kích thước cơ sở dữ liệu và cải thiện hiệu suất truy vấn. Tránh sử dụng các cột `nvarchar(max)` cho các thuộc tính chỉ chứa dữ liệu ngắn. Sử dụng các ràng buộc cơ sở dữ liệu để đảm bảo tính nhất quán của dữ liệu, điều này có thể giúp giảm gánh nặng cho ứng dụng.
Luôn sử dụng các phương thức bất đồng bộ (`ToListAsync`, `SaveChangesAsync`) thay vì các phương thức đồng bộ để tránh chặn luồng và cải thiện khả năng mở rộng của ứng dụng.
Đối với các ứng dụng có mô hình lớn (hàng trăm hoặc hàng nghìn entity), thời gian khởi động EF Core có thể đáng kể. Compiled Models cho phép bạn biên dịch mô hình entity thành mã C#, giúp giảm đáng kể thời gian khởi động.
Để tạo Compiled Models, hãy sử dụng công cụ dòng lệnh `dotnet ef`:
dotnet ef dbcontext optimize
Sau đó, cấu hình `DbContext` của bạn để sử dụng mô hình đã biên dịch:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseModel(YourCompiledModel.Instance)
.UseSqlServer("YourConnectionString");
Bằng cách áp dụng các kỹ thuật tối ưu hóa được trình bày trong bài viết này, bạn có thể cải thiện đáng kể hiệu suất EF Core và xây dựng các ứng dụng .NET nhanh chóng và hiệu quả. Hãy nhớ rằng việc tối ưu hóa hiệu suất là một quá trình liên tục. Hãy luôn theo dõi hiệu suất ứng dụng của bạn và thực hiện các điều chỉnh khi cần thiết. Chúc bạn thành công!
Bài viết liên quan