В общем случае для обновления или удаления объектов в Entity Framework нам необходимо было сначала получить эти объекты.
Контекст данных в EF Core по умолчанию отслеживает изменения в загруженных сущностях, а затем отправляет запросы на обновление в базу данных при вызове метода SaveChange()
.
Запросы на обновление отправляются только для тех свойств, которые действительно изменились. Кроме того, отслеживаемые объекты синхронизируются с
изменениями, отправленными в базу данных. При этом изменения также группируются, чтобы уменьшить количество обращений к базе данных. Подобный механизм является довольно
эффективным и удобным способом для работы с базой данных.
Однако нередко требуется просто обновить или удалить объекты без необходимости загружать их в контекст. В этом случае мы могли бы использовать sql-запросы. Но начиная с версии 7.0 в Entity Framework для типа IQueryable были добавлены два новых метода: ExecuteUpdate и ExecuteDelete и их асинхронные двойники ExecuteUpdateAsync и ExecuteDeleteAsync. Рассмотрим, в чем их преимущество.
Для рассмотрения возьмем следующий класс User
и контекст данных ApplicationContext
:
using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } = null!; public ApplicationContext() => Database.EnsureCreated(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Data Source=helloapp.db"); } } public class User { public int Id { get; set; } public string? Name { get; set; } public int Age { get; set; } }
В примерах выше, чтобы обновить объекты, сначала надо было их получить, установить у их свойств новые значения и затем сохранить новые значения в бд. И тут мы вынуждены совершать
промежуточную операцию - получение объекта. Но он не всегда может быть нужен, возможно, просто надо обновить некоторые его свойства. И метод ExecuteUpdate()
решает эту проблему.
Допустим, нам надо у всех пользователей увеличить свойство Age на 1:
using (ApplicationContext db = new ApplicationContext()) { db.Users.ExecuteUpdate(s=>s.SetProperty(u=>u.Age, u=>u.Age +1)); }
Метод ExecuteUpdate()
принимает делегат, параметр которого представляет набор выражений установки свойств (в примере выше представлен буквой s
).
Через этот параметр вызываем метод SetProperty(), в котором устанавливаем новое значение свойства.
Вызов метода SetProperty() принимает два параметра. Первый параметр определяет обновляемое свойство. В примере выше это выражение u=>u.Age
. То есть обновляем свойство Age.
Второй параметр метода определяет, как изменяется значение свойства. В примере выше это выражение u=>u.Age + 1
. То есть берем свойство Age и прибавляем к нему 1.
Логика обновления может быть самой разной.
При выполнении такого вызова метода будет формироваться и выполняться следующий SQL-запрос:
UPDATE [u] SET [u].[Age] = [b].[Age] + 1 FROM [Users] AS [u]
Подобным образом можно использовать асинхронную версию метода:
using (ApplicationContext db = new ApplicationContext()) { await db.Users.ExecuteUpdateAsync(s=>s.SetProperty(u=>u.Age, u=>u.Age +1)); }Таким образом, нам не надо получать объекты для установки, не надо прибегать к SQL-запросам, а можно в такой простой форме сразу обновить все необходимые объекты.
Если надо обновить не все, а какие-то определенные объекты, то перед вызовом ExecuteUpdate()
определяем выборку. Например, обновим возраст только у тех объектов,
у которых Name="Tom":
using (ApplicationContext db = new ApplicationContext()) { // обновляем только объекты, у которых имя Tom db.Users.Where(u=>u.Name=="Tom") .ExecuteUpdate(s=>s.SetProperty(u=>u.Age, u=>u.Age +1)); }
В этом случае формируется следующий sql-запрос:
UPDATE "Users" AS "u" SET "Age" = "u"."Age" + 1 WHERE "u"."Name" = 'Tom'
С помощью дополнительных вызывов SetProperty()
по цепочке можно обновлять несколько свойств одновременно. Например:
using (ApplicationContext db = new ApplicationContext()) { // обновляем только объекты, у которых имя Tom db.Users.Where(u =>u.Name == "Tom") .ExecuteUpdate(s => s .SetProperty(u => u.Age, u => u.Age + 1) // Age = Age + 1 .SetProperty(u=>u.Name, u => "Tomas")); // Name = "Tomas }
В данном случае к возрасту прибавляем 1 и изменяем имя на "Tomas".
В этом случае формируется следующий SQL-запрос:
UPDATE "Users" AS "u" SET "Name" = 'Tomas', "Age" = "u"."Age" + 1 WHERE "u"."Name" ='Tom'
Метод ExecuteDelete() позволяет избежать ненужного промежуточного получения объектов для удаления. Мы сразу можем удалить их одним запросом. Например, удалим всех пользователей, у которых Name="Tom":
using (ApplicationContext db = new ApplicationContext()) { // обновляем только объекты, у которых имя Tom db.Users.Where(u => u.Name == "Tom").ExecuteDelete(); }
Такой метод будет формировать следующий запрос:
DELETE FROM [u] FROM [Users] AS [u] WHERE [u].[Text] LIKE N'Tom'
Использование асинхронной версии:
using (ApplicationContext db = new ApplicationContext()) { // обновляем только объекты, у которых имя Bob await db.Users.Where(u => u.Name == "Bob").ExecuteDeleteAsync(); }
Можно сразу удалить все объекты:
using (ApplicationContext db = new ApplicationContext()) { db.Users.ExecuteDelete(); // или асинхронно await db.Users.ExecuteDeleteAsync(); }
Такой метод будет формировать следующий запрос:
DELETE FROM [u] FROM [Users] AS [u]
Выше говорилось о преимуществах методов ExecuteUpdate/ExecuteDelete. Но они также имеют недостатки. В частности, Любые отслеживаемые объекты в контексте данных не будут синхронизированы с теми изменениями, которые вызывают методы методов ExecuteUpdate/ExecuteDelete. Соответственно после изменения потребуется перезагружать данные из бд. Кроме того, возможно, потребуется отправить команды в правильном порядке, чтобы не нарушать ограничения базы данных. Например, удаленить зависимые сущности в БД до удаления привязанной главной сущности.