EF Core Bulk Data Retrieval: 5 Phương Pháp Bạn Nên Biết

Khi làm việc với Entity Framework Core, các nhà phát triển thường cần truy xuất nhiều thực thể từ cơ sở dữ liệu dựa trên một tập hợp các ID hoặc giá trị.

Cách tiếp cận điển hình là sử dụng phương thức Contains:

var productIds = GetProductIdsForSync();

var products = await dbContext.Products
    .Where(p => productIds.Contains(p.Id))
    .ToListAsync();

Cách này hoạt động tốt với các tập hợp nhỏ, nhưng tạo ra vấn đề đáng kể khi xử lý dữ liệu lớn:

  • Giảm hiệu suất – Ngay cả với các index phù hợp, mệnh đề SQL WHERE IN trở nên chậm hơn khi số lượng tham số tăng lên.
  • Vượt quá giới hạn tham số – SQL Server có giới hạn cứng là 2.100 tham số cho mỗi truy vấn. Mặc dù bạn có thể chia nhỏ truy vấn thành các batch trong SQL, nhưng điều này không phải lúc nào cũng thực tế khi sử dụng EF Core, đặc biệt trong các tình huống phức tạp với nhiều bộ lọc và join.
  • Vấn đề về bộ nhớ và kết nối – Nhiều lượt truy vấn đến cơ sở dữ liệu tiêu tốn nhiều bộ nhớ hơn và giữ kết nối database mở lâu hơn, có khả năng gây cạn kiệt connection pool.

Đây thực sự là vấn đề trong các tình huống như đồng bộ danh mục sản phẩm, xử lý đơn hàng số lượng lớn, hoặc cập nhật tồn kho cho hàng nghìn mặt hàng.

Có một giải pháp tốt hơn: thư viện Entity Framework Extensions cung cấp các phương thức chuyên biệt cho truy xuất dữ liệu hàng loạt, giải quyết tất cả các vấn đề này.

Trong bài viết này, bạn sẽ khám phá 5 phương thức cho phép bạn truy xuất hiệu quả các tập dữ liệu lớn mà không chạm đến giới hạn tham số hay hy sinh hiệu suất.

Hãy cùng bắt đầu.

Vấn đề với Phương thức Contains

Tất cả các ví dụ trong bài viết này đã được kiểm thử trên cơ sở dữ liệu SQL Server.

Phương thức Contains trong EF Core dịch thành mệnh đề SQL WHERE IN.

Khi bạn truyền một tập hợp 10.000 ID sản phẩm, EF Core sẽ tạo ra một truy vấn với 10.000 tham số:

var productIds = await GetProductIdsFromExternalSystem();

var products = await dbContext.Products
    .Where(p => productIds.Contains(p.Id))
    .ToListAsync();
SELECT * FROM Products
WHERE Id IN (@p0, @p1, @p2, ... @p9999)

Cách tiếp cận này có những hạn chế nghiêm trọng:

Giới hạn Tham số SQL Server – SQL Server cho phép tối đa 2.100 tham số trong một truy vấn. Nếu bạn cố gắng truy vấn hơn 2.100 sản phẩm, EF Core sẽ ném ra ngoại lệ:

System.Data.SqlClient.SqlException: The incoming request has too many parameters. 
The server supports a maximum of 2100 parameters.

Như một giải pháp tạm thời, bạn có thể chia nhỏ truy vấn thành các batch:

var batchSize = 2000;
var allProducts = new List<Product>();

for (int i = 0; i < productIds.Count; i += batchSize)
{
    var batch = productIds.Skip(i).Take(batchSize).ToList();
    var products = await dbContext.Products
        .Where(p => batch.Contains(p.Id))
        .ToListAsync();
    
    allProducts.AddRange(products);
}

Tuy nhiên, giải pháp này tạo ra nhiều lượt truy vấn đến cơ sở dữ liệu, giữ kết nối mở lâu hơn, và gây khó khăn khi áp dụng các bộ lọc hoặc include bổ sung trên tất cả các batch.

Trong SQL, bạn có thể sử dụng bảng tạm thời hoặc tham số kiểu bảng để vượt qua giới hạn. Tuy nhiên, các cách tiếp cận này yêu cầu SQL thuần và mất đi lợi ích của truy vấn kiểu mạnh, theo dõi thay đổi, và hỗ trợ navigation property của EF Core.

Đối với các tình huống đồng bộ sản phẩm nơi bạn cần truy xuất 50.000 sản phẩm từ cơ sở dữ liệu để so sánh với hệ thống bên ngoài, đây trở thành một nút thắt cổ chai đáng kể.

Entity Framework Extensions là một thư viện mở rộng EF Core với các thao tác bulk hiệu suất cao. Mặc dù nổi tiếng với các thao tác bulk insert, update, và delete, nó cũng cung cấp các phương thức mạnh mẽ cho truy xuất dữ liệu hàng loạt.

Để bắt đầu với Entity Framework Extensions, hãy cài đặt gói NuGet sau:

dotnet add package Z.EntityFramework.Extensions.EFCore

Thư viện cung cấp các phương thức chuyên biệt sử dụng bảng tạm thời nội bộ để vượt qua giới hạn tham số và cải thiện hiệu suất. Thay vì truyền hàng nghìn tham số trong mệnh đề WHERE IN, các phương thức này:

  • Tạo một bảng tạm thời trong cơ sở dữ liệu
  • Chèn các giá trị bộ lọc của bạn vào bảng tạm thời đó
  • Join bảng thực thể của bạn với bảng tạm thời
  • Trả về kết quả đã lọc
  • Tự động dọn dẹp bảng tạm thời

Cách tiếp cận này cải thiện đáng kể hiệu suất truy vấn cho các tập dữ liệu lớn.

Thư viện Entity Framework Extensions cung cấp năm phương thức chính cho truy xuất dữ liệu hàng loạt:

  • WhereBulkContains – lọc thực thể với truy vấn LINQ sử dụng tất cả các mục từ một danh sách hiện có. Chỉ các mục trong cơ sở dữ liệu khớp với các mục trong danh sách của bạn mới được trả về.
  • WhereBulkNotContains – lọc thực thể với truy vấn LINQ bằng cách loại trừ tất cả các mục từ một danh sách hiện có.
  • BulkRead – lọc thực thể với truy vấn LINQ bằng cách bao gồm tất cả các mục từ một danh sách hiện có và trả về kết quả ngay lập tức.
  • WhereBulkContainsFilterList – trả về các mục từ danh sách đã tồn tại trong cơ sở dữ liệu.
  • WhereBulkNotContainsFilterList – trả về các mục từ danh sách không tồn tại trong cơ sở dữ liệu. Đây là phương thức nghịch đảo của WhereBulkContainsFilterList.

Hãy cùng khám phá các phương thức này chi tiết hơn.

Phương thức 1: WhereBulkContains

Phương thức WhereBulkContains là phương thức truy xuất bulk được sử dụng phổ biến nhất. Nó truy xuất các thực thể mà một thuộc tính được chỉ định khớp với bất kỳ giá trị nào trong một tập hợp.

Phương thức này đặc biệt hữu ích cho các tình huống đồng bộ sản phẩm khi bạn cần truy xuất hàng nghìn sản phẩm từ cơ sở dữ liệu để so sánh với dữ liệu từ hệ thống bên ngoài, API, hoặc import tệp tin.

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var productIds = await GetProductIdsFromExternalSystem();

var products = await dbContext.Products
    .WhereBulkContains(productIds)
    .ToListAsync();

Phương thức tự động sử dụng khóa chính của thực thể để khớp các mục. Trong ví dụ này, nó join trên Id.

Bạn cũng có thể lọc bằng cách chỉ định bất kỳ thuộc tính nào làm tham số thứ 2:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var products = await GetProductsFromExternalSystem();

var products = await dbContext.Products
    .WhereBulkContains(products, x => x.Id)
    .ToListAsync();

Tham số đầu tiên là tập hợp các giá trị để lọc, và tham số thứ hai là biểu thức lambda chỉ định thuộc tính nào để khớp.

Tất cả các loại danh sách đều được hỗ trợ. Yêu cầu duy nhất là danh sách của bạn phải là kiểu cơ bản hoặc chứa một thuộc tính có cùng tên với khóa:

  • Kiểu cơ bản như List<int> và List<Guid>
  • Kiểu thực thể như List<Customer>
  • Kiểu ẩn danh (Anonymous Type)
  • Danh sách Expando Object

Lọc theo Thuộc tính Không phải Khóa Chính:

Bạn có thể sử dụng WhereBulkContains với bất kỳ thuộc tính nào, không chỉ khóa chính:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var productCodes = new List<string> 
{ 
    "SKU-001", "SKU-002", "SKU-003", // ...
};

var products = await dbContext.Products
    .WhereBulkContains(productCodes, x => x.ProductCode)
    .ToListAsync();

Bạn có thể lọc theo nhiều thuộc tính cùng lúc:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var products = context.Products
    .WhereBulkContains(deserializedProducts, x => new { x.SupplierCode, x.ProductCode })
    .ToListAsync();

WhereBulkContains trả về IQueryable<T> có thể được kết hợp với các phương thức LINQ khác như Where, Select, và Include:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var productIds = GetProductIdsForSync();

var activeProducts = await dbContext.Products
    .Where(p => p.IsActive && p.Stock > 0)
    .WhereBulkContains(productIds, x => x.Id)
    .Select(p => new
    {
        p.Id,
        p.Name,
        p.Price,
        CategoryName = p.Category.Name
    })
    .ToListAsync();

Điều này linh hoạt hơn nhiều so với việc chia nhỏ truy vấn thành các batch, nơi việc áp dụng các bộ lọc và include bổ sung trên các batch trở nên phức tạp và dễ gây lỗi.

Phương thức 2: WhereBulkNotContains

Phương thức WhereBulkNotContains là phương thức đối lập của WhereBulkContains. Nó truy xuất các thực thể mà một thuộc tính được chỉ định không khớp với bất kỳ giá trị nào trong tập hợp.

Phương thức này hữu ích khi bạn cần tìm các thực thể bị thiếu từ một hệ thống bên ngoài hoặc khi bạn muốn loại trừ một tập hợp lớn các mục khỏi kết quả truy vấn của mình.

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var discontinuedProductIds = await GetDiscontinuedProductIdsFromExternalSystem();

var activeProducts = await dbContext.Products
    .WhereBulkNotContains(discontinuedProductIds, x => x.Id)
    .ToListAsync();

Điều này truy xuất tất cả sản phẩm ngoại trừ những sản phẩm trong tập hợp discontinuedProductIds, mà không chạm đến giới hạn tham số.

Giống như WhereBulkContains, bạn có thể kết hợp phương thức này với các thao tác LINQ khác:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var processedProductIds = await GetProcessedProductIds();

var remainingProducts = await dbContext.Products
    .Where(p => p.RequiresProcessing)
    .WhereBulkNotContains(processedProductIds, x => x.Id)
    .OrderBy(p => p.Priority)
    .Take(1000)
    .ToListAsync();

Phương thức 3: BulkRead

Phương thức BulkRead là một phương thức tiện lợi kết hợp WhereBulkContains với thực thi ngay lập tức.

Nó truy xuất các thực thể từ cơ sở dữ liệu khớp với các mục trong danh sách của bạn và trả về kết quả ngay lập tức dưới dạng List<T>.

Nó hoạt động giống như WhereBulkContains nhưng không yêu cầu bạn phải kết hợp với các phương thức khác để trả về kết quả:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var deserializedProducts = DeserializeProductsFromJson(); // Trả về 10.000 sản phẩm

// Truy xuất các sản phẩm khớp từ cơ sở dữ liệu ngay lập tức
var products = context.Products.BulkRead(deserializedProducts);

Sự khác biệt là BulkRead trả về kết quả ngay lập tức, trong khi WhereBulkContains trả về IQueryable<T> mà bạn có thể kết hợp với các thao tác LINQ bổ sung.

Phiên bản bất đồng bộ cũng có sẵn:

var deserializedProducts = DeserializeProductsFromJson();
var products = await context.Products.BulkReadAsync(deserializedProducts);

Phương thức 4: WhereBulkContainsFilterList

Phương thức WhereBulkContainsFilterList lọc danh sách đầu vào của bạn bằng cách chỉ trả về các mục đã tồn tại trong cơ sở dữ liệu.

Điều này khác với WhereBulkContains, vốn trả về các thực thể từ cơ sở dữ liệu. Phương thức này trả về các mục từ danh sách đầu vào của bạn (không phải thực thể cơ sở dữ liệu).

Phương thức này đặc biệt hữu ích khi bạn cần tách dữ liệu của mình thành các mục hiện có và không tồn tại, chẳng hạn như phân tách các bản ghi cần được chèn khỏi những bản ghi cần được cập nhật.

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var deserializedProducts = DeserializeProductsFromJson(); // Trả về 20.000 sản phẩm

// Trả về các mục từ deserializedProducts đã tồn tại trong cơ sở dữ liệu
var existingProducts = context.Products
    .WhereBulkContainsFilterList(deserializedProducts)
    .ToList();

Phương thức trả về các mục từ danh sách đầu vào của bạn (không phải thực thể cơ sở dữ liệu) khớp với các dòng trong cơ sở dữ liệu.

Bạn có thể chỉ định thuộc tính nào để sử dụng cho việc khớp:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var existingProducts = context.Products
    .WhereBulkContainsFilterList(deserializedProducts, x => x.ProductCode)
    .ToList();

// Hoặc sử dụng string
var existingProducts = context.Products
    .WhereBulkContainsFilterList(deserializedProducts, "ProductCode")
    .ToList();

// Hoặc sử dụng danh sách các string
var existingProducts = context.Products
    .WhereBulkContainsFilterList(deserializedProducts, new List<string> { "ProductCode" })
    .ToList();

Phương thức này hỗ trợ lọc theo nhiều thuộc tính.

Phương thức 5: WhereBulkNotContainsFilterList

Phương thức WhereBulkNotContainsFilterList là phương thức nghịch đảo của WhereBulkContainsFilterList. Nó lọc danh sách đầu vào của bạn bằng cách chỉ trả về các mục không tồn tại trong cơ sở dữ liệu.

Phương thức này trả về các mục từ danh sách đầu vào của bạn (không phải thực thể cơ sở dữ liệu).

Phương thức này hữu ích khi bạn muốn xác định các bản ghi mới cần được chèn hoặc khi bạn cần tìm các mục từ dữ liệu import bị thiếu trong cơ sở dữ liệu.

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var deserializedProducts = DeserializeProductsFromJson(); // Trả về 20.000 sản phẩm

// Trả về các mục từ deserializedProducts không tồn tại trong cơ sở dữ liệu
var notExistingProducts = context.Products
    .WhereBulkNotContainsFilterList(deserializedProducts)
    .ToList();

Phương thức trả về các mục từ danh sách đầu vào của bạn (không phải thực thể cơ sở dữ liệu) không khớp với các dòng trong cơ sở dữ liệu.

Bạn có thể chỉ định thuộc tính nào để sử dụng cho việc khớp.

Phương thức này hỗ trợ lọc theo nhiều thuộc tính.

Tổng kết

Khi nào nên dùng BulkRead vs WhereBulkContains:

Sử dụng BulkRead khi:

  • Bạn muốn kết quả ngay lập tức dưới dạng List<T>
  • Bạn không cần áp dụng các thao tác LINQ bổ sung
  • Bạn đang làm việc với dữ liệu đã được giải tuần tự hóa cần được đối khớp với cơ sở dữ liệu

Sử dụng WhereBulkContains khi:

  • Bạn cần kết hợp các thao tác LINQ bổ sung như Where, Select, hoặc Include
  • Bạn muốn xây dựng một truy vấn trước khi thực thi nó
  • Bạn cần sự linh hoạt của IQueryable<T>

Khi nào nên dùng WhereBulkContainsFilterList vs WhereBulkContains:

  • Sử dụng WhereBulkContainsFilterList khi bạn muốn lọc danh sách của mình.
  • Sử dụng WhereBulkContains khi bạn muốn truy xuất các thực thể từ cơ sở dữ liệu.

Khi nào nên dùng WhereBulkNotContainsFilterList vs WhereBulkNotContains:

  • Sử dụng WhereBulkNotContainsFilterList khi bạn muốn lọc danh sách của mình.
  • Sử dụng WhereBulkNotContains khi bạn muốn truy xuất các thực thể từ cơ sở dữ liệu.

Entity Framework Extensions là một thư viện thương mại có bản dùng thử miễn phí, bạn có thể sử dụng để xem nó có phù hợp với dự án của mình hay không.

Thư viện yêu cầu giấy phép cho mục đích sử dụng trong sản xuất. Xem thêm thông tin về cấp phép tại đây.

Đối với mục đích thương mại, thời gian tiết kiệm được trong phát triển và hiệu suất ứng dụng được cải thiện thường bù đắp chi phí bản quyền trong tháng đầu tiên sử dụng. Hãy dùng thử bản dùng thử miễn phí để xem các phương thức này có thể biến đổi các thao tác dữ liệu hàng loạt của bạn từ một nút thắt cổ chai về hiệu suất thành một trong những phần nhanh nhất của ứng dụng như thế nào.

Xin chân thành cảm ơn ZZZ Projects đã tài trợ cho bài viết này.

Hy vọng bạn thấy bài viết này hữu ích. Hẹn gặp lại bạn lần sau.

Bạn có thể tải mã nguồn cho bài viết này miễn phí

Tải mã nguồn

Thấy bài viết hữu ích? Hãy chia sẻ với cộng đồng của bạn

LinkedIn | X (Twitter) | Facebook