Truy xuất dữ liệu hàng loạt với EF Core: 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 tiếp cận này hoạt động tốt với các tập hợp nhỏ, nhưng nó tạo ra những vấn đề đáng kể khi xử lý các tập dữ liệu lớn:

  • Suy 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ể giải quyết bằng cách 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 khả thi khi sử dụng EF Core, đặc biệt trong các tình huống phức tạp với các bộ lọc và join bổ sung.
  • 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 cơ sở dữ liệu mở lâu hơn, có khả năng gây cạn kiệt connection pool.

Điều này trở thành một vấn đề thực sự trong các tình huống như đồng bộ hóa 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 việc truy xuất dữ liệu hàng loạt, giải quyết tất cả những 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 gặp phải giới hạn tham số hoặc hy sinh hiệu suất.

Hãy cùng tìm hiể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 được 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 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 giới hạn quan 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 duy nhất. Nếu bạn cố gắng truy vấn hơn 2.100 sản phẩm, EF Core sẽ ném ra một 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 thay thế, 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 để giải quyết 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 các truy vấn kiểu mạnh (strongly-typed), theo dõi thay đổi (change tracking), và hỗ trợ navigation property của EF Core.

Đối với các tình huống đồng bộ hóa 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 một hệ thống bên ngoài, điều nà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 hàng loạt hiệu suất cao. Mặc dù nó nổi tiếng với các thao tác chèn hàng loạt, cập nhật hàng loạt, và xóa hàng loạt, nó cũng cung cấp các phương thức mạnh mẽ cho việc 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 bên trong để 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ả đã được 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 việc truy xuất dữ liệu hàng loạt:

  • WhereBulkContains – lọc các thực thể với truy vấn LINQ bằng cách sử dụng tất cả các mục từ một danh sách có sẵn. 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 cá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 có sẵn.
  • BulkRead – lọc cá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 có sẵn và sau đó 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 ngược lại 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 hàng loạt được sử dụng phổ biến nhất. Nó truy xuất các thực thể nơi 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ộ hóa sản phẩm nơi 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ừ một hệ thống bên ngoài, API, hoặc nhập tệp.

// @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à một 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>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ễ xảy ra lỗi.

Phương thức 2: WhereBulkNotContains

Phương thức WhereBulkNotContains là phương thức ngược lại của WhereBulkContains. Nó truy xuất các thực thể nơi 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ả các sản phẩm ngoại trừ những sản phẩm trong tập hợp discontinuedProductIds, mà không gặp phải 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 async 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 trả về chỉ những 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 từ 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 chuỗi
var existingProducts = context.Products
    .WhereBulkContainsFilterList(deserializedProducts, "ProductCode")
    .ToList();

// Hoặc sử dụng danh sách chuỗi
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 ngược lại của WhereBulkContainsFilterList. Nó lọc danh sách đầu vào của bạn bằng cách trả về chỉ những 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ừ tệp nhập của mình 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 sử 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 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 sử 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ể cơ sở dữ liệu.

Khi nào sử 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ể 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 đủ để trang trải chi phí giấy phép 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 hữu ích. Hẹn gặp lại lần sau.

Chỉ mục