Как реализовать пакетное удаление данных по условию в Entity Framework
Что проверяют
Знаете ли вы, что EF Core умеет удалять одним SQL-запросом, и чем это отличается от RemoveRange. Следом обычно спрашивают, как такое удаление ведёт себя с change tracker, транзакциями и каскадами.
Как отвечать
Классический путь: загрузить и удалить. Where(...).ToList(), затем RemoveRange и SaveChanges. EF отправит DELETE на каждую строку (пачками команд), но сначала поднимет все сущности в память и начнёт их отслеживать. Для сотни строк это нормально, для сотен тысяч нет.
Начиная с EF Core 7 есть ExecuteDelete и ExecuteDeleteAsync:
var deleted = await db.Orders
.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoff)
.ExecuteDeleteAsync(ct);Это один DELETE ... WHERE ... без загрузки данных, метод возвращает число удалённых строк. Особенности:
- выполняется сразу: не ждёт
SaveChangesи не входит в его транзакцию. Если удаление должно быть атомарным вместе с другими изменениями, оберните всё вBeginTransactionAsync; - трекер не в курсе: уже загруженные в контекст сущности остаются в нём как живые, перехватчики
SaveChangesи переопределённыйSaveChangesне срабатывают; - каскады: зависимые строки EF сам не удаляет, работает только
ON DELETE CASCADEна внешнем ключе в базе. Без него получите нарушение ограничения.
Для очень больших объёмов удаляйте порциями: повторяйте ExecuteDeleteAsync по ограниченной выборке, пока он не вернёт ноль. Так не держатся долгие блокировки и не раздувается журнал транзакций. В версиях до седьмой используют параметризованный ExecuteSqlInterpolatedAsync или сторонние библиотеки для массовых операций.
Где спотыкаются
Принимают условие как Func<T, bool> вместо Expression<Func<T, bool>>. Тогда Where уходит в LINQ to Objects: вся таблица загружается в память и фильтруется на клиенте.
Применяют ExecuteDelete там, где аудит или мягкое удаление сделаны через перехватчики SaveChanges. Строки физически исчезают, а журнал аудита об этом не знает.
Удаляют миллионы строк одним запросом в рабочее время. На SQL Server это может закончиться эскалацией блокировки до всей таблицы, а долгая транзакция тормозит реплики.