Что такое ExecuteUpdate и как он повышает производительность?
Что проверяют
Знаете ли вы, что современный EF Core умеет менять данные одним запросом, минуя материализацию сущностей, и понимаете ли, чем платите за этот обход. Вопрос хорошо разделяет тех, кто знает ORM как набор методов, и тех, кто представляет, какой SQL уходит в базу.
Как отвечать
ExecuteUpdate транслирует запрос LINQ в один оператор UPDATE с условием WHERE и выполняет его сразу, без SaveChanges. Парный метод ExecuteDelete делает то же для удаления.
await db.Orders
.Where(o => o.Status == OrderStatus.New && o.CreatedAt < border)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, now));Выигрыш понятен, если вспомнить альтернативу: обычный путь это выборка строк в память, построение снимка для отслеживания изменений, изменение свойств и пакет команд по одной на строку. На тысячах записей уходит и трафик, и память, и время. Здесь же один запрос, и работу делает база.
Важно сразу назвать обратную сторону. Запрос выполняется мимо единицы работы: он не видит несохранённые изменения контекста, а уже загруженные сущности остаются со старыми значениями в памяти. Если дальше в том же контексте вы читаете те же строки, их нужно перечитать. Атомарность с последующим SaveChanges даёт только явно открытая транзакция.
Где спотыкаются
Рассчитывают, что отработает привычная обвязка: аудит, мягкое удаление, переопределённый SaveChanges, перехватчики. Ничего этого не будет, изменения идут напрямую в базу, и колонка с датой правки останется старой, если не задать её вручную.
Забывают про оптимистичную блокировку. Проверка версии строки здесь не выполняется, исключения о конкурентном изменении вы не получите и тихо затрёте чужую правку.
Ставят подряд два таких вызова и считают их одной операцией. Без явной транзакции это две отдельные, и сбой между ними оставляет данные наполовину изменёнными.