Особенности Bulk-Операций в EF Core
Когда вы имеете дело с тысячами или даже миллионами записей, эффективность имеет решающее значение. В EF Core 7 представлены два новых метода: ExecuteUpdate и ExecuteDelete (и их асинхронные перегрузки), предназначенные для упрощения Bulk-операций в БД. Однако они обходят трекер изменений EF Core, что может привести к неожиданному поведению, если вы об этом не знаете.
Трекер Изменений
Когда вы загружаете объекты из БД с помощью EF Core, трекер изменений начинает их отслеживать. Когда вы обновляете свойства, удаляете объекты или добавляете новые, он записывает эти изменения:
using (var context = new AppDbContext())
{
// Загрузка
var pr = context.Products
.FirstOrDefault(p => p.Id == 1);
// Изменение
pr.Price = 99.99;
// Здесь трекер знает, что pr изменён
// Добавление
var newPr = new Product {
Name = "New Gadget", Price = 129.99 };
context.Products.Add(newPr);
// Удаление
context.Products.Remove(pr);
// Сохранение всех изменений в БД
context.SaveChanges();
}
Когда вы вызываете SaveChanges, EF Core использует трекер изменений, чтобы определить, какие команды SQL следует выполнить. Это гарантирует, что БД будет синхронизирована с вашими изменениями.
Bulk-операции и трекер изменений
Bulk-операции не используют трекер изменений. Это решение может показаться нелогичным, но за ним стоит веская причина: производительность. Непосредственно выполняя инструкции SQL в БД, EF Core устраняет накладные расходы на отслеживание изменений отдельных объектов.
using (var context = new AppDbContext())
{
// Увеличиваем цену всех продуктов в категории электроника на 10%
context.Products
.Where(p => p.Category == "Electronics")
.ExecuteUpdate(
s => s.SetProperty(p => p.Price, p => p.Price * 1.10));
// Все объекты типа Product в памяти по-прежнему будут иметь старые цены
}
В этом примере метод ExecuteUpdate эффективно преобразует операцию в одну инструкцию SQL UPDATE:
UPDATE [p]
SET [p].[Price] = [p].[Price] * 1.10
FROM [Products] as [p];
Но, если вы проверите экземпляры Product, которые EF Core уже загрузил в память, вы обнаружите, что их свойства Price не изменились. Это может показаться удивительным, если вы не знаете, как массовые обновления взаимодействуют с системой отслеживания изменений. Это же применимо и к методу ExecuteDelete.
Перехватчики EF Core также не запускаются для операций ExecuteUpdate и ExecuteDelete. Если нужно отслеживать или изменять Bulk-операции, вы можете создать триггеры в БД на обновление или удаление.
Проблема: поддержание согласованности
Если ExecuteUpdate завершается успешно, изменения сразу фиксируются в БД, т.к. Bulk-операции обходят трекер изменений и не участвуют в обычной транзакции, управляемой SaveChanges. Если впоследствии SaveChanges завершится сбоем, вы окажетесь в несогласованном состоянии. Изменения, внесенные с помощью ExecuteUpdate, уже сохранятся. Любые изменения, сделанные «в памяти», потеряются.
Самый надежный способ обеспечить согласованность — обернуть в транзакцию и ExecuteUpdate, и операции, которые приводят к SaveChanges:
using (var context = new AppDbContext())
using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Products
.Where(p => p.Category == "Electronics")
.ExecuteUpdate(
s => s.SetProperty(p => p.Price, p => p.Price * 1.10));
// … другие изменения сущностей
context.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
}
}
Если SaveChanges завершится неудачно, транзакция будет отменена с отменой изменений, внесенных как ExecuteUpdate, так и любыми другими операциями внутри транзакции. Это сохранит вашу БД в согласованном состоянии.
Источник: https://www.milanjovanovic.tech/blog/what-you-need-to-know-about-ef-core-bulk-updates