Поделиться через


ExecuteUpdate и ExecuteDelete

Примечание.

Эта функция появилась в EF Core 7.0.

ExecuteUpdate и ExecuteDelete это способ сохранения данных в базе данных без использования традиционного отслеживания изменений и SaveChanges() метода EF. Вводное сравнение этих двух методов см. на странице "Обзор" для сохранения данных.

ExecuteDelete

Предположим, что необходимо удалить все блоги с рейтингом ниже определенного порогового значения. Традиционный SaveChanges() подход требует выполнения следующих действий.

foreach (var blog in context.Blogs.Where(b => b.Rating < 3))
{
    context.Blogs.Remove(blog);
}

context.SaveChanges();

Это довольно неэффективный способ выполнения этой задачи: мы запрашиваем базу данных для всех блогов, соответствующих нашему фильтру, а затем запрашиваем, материализуем и отслеживаем все эти экземпляры; количество соответствующих сущностей может быть огромным. Затем мы сообщаем средству отслеживания изменений EF, что каждый блог должен быть удален, и применить эти изменения путем вызова SaveChanges(), который создает DELETE инструкцию для каждого из них.

Ниже приведена та же задача, выполняемая через ExecuteDelete API:

context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();

При этом используются знакомые операторы LINQ, чтобы определить, какие блоги должны быть затронуты - так же, как если бы мы запрашивали их, а затем сообщает EF выполнить SQL DELETE в базе данных:

DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Помимо упрощения и короче, это выполняется очень эффективно в базе данных, не загружая данные из базы данных или отслеживая изменения EF. Обратите внимание, что можно использовать произвольные операторы LINQ, чтобы выбрать, какие блоги вы хотите удалить, — они переводятся в SQL для выполнения в базе данных, как если бы вы запросили эти блоги.

ExecuteUpdate

Вместо удаления этих блогов, что если бы мы хотели изменить свойство, чтобы указать, что они должны быть скрыты вместо этого? ExecuteUpdate предоставляет аналогичный способ выражения инструкции SQL UPDATE :

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));

Как и в случае ExecuteDeleteс , мы сначала используем LINQ для определения того, какие блоги должны быть затронуты; но с ExecuteUpdate нами также необходимо выразить изменение, которое будет применяться к соответствующим блогам. Это делается путем вызова SetProperty в ExecuteUpdate вызове и предоставления ему двух аргументов: свойства для изменения (IsVisible), а новое значение должно иметь (false). Это приводит к выполнению следующего SQL:

UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Обновление нескольких свойств

ExecuteUpdate позволяет обновлять несколько свойств в одном вызове. Например, чтобы задать IsVisible значение false и задать Rating значение нулю, просто выполните цепочку дополнительных SetProperty вызовов:

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters
        .SetProperty(b => b.IsVisible, false)
        .SetProperty(b => b.Rating, 0));

Выполняется следующий SQL:

UPDATE [b]
SET [b].[Rating] = 0,
    [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Ссылка на существующее значение свойства

Приведенные выше примеры обновили свойство до нового значения константы. ExecuteUpdate также позволяет ссылаться на существующее значение свойства при вычислении нового значения; Например, чтобы увеличить рейтинг всех соответствующих блогов по одному, используйте следующее:

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

Обратите внимание, что второй аргумент SetProperty теперь является лямбда-функцией, а не константой, как и раньше. Его b параметр представляет обновляемый блог; в этой лямбда-лямбда-коде, таким образом, b.Rating содержит рейтинг перед любым изменением. Выполняется следующий SQL:

UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

ExecuteUpdate в настоящее время не поддерживает ссылки на навигации в лямбда-лямбда-интерфейсе SetProperty . Например, предположим, что мы хотим обновить все рейтинги блогов, чтобы каждый блог новый рейтинг был средним из всех его рейтингов post. Мы можем попытаться использовать ExecuteUpdate следующее:

context.Blogs.ExecuteUpdate(
    setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));

Однако EF позволяет выполнять эту операцию сначала с помощью Select вычисления средней оценки и проецирования его на анонимный тип, а затем использовать ExecuteUpdate его для этого:

context.Blogs
    .Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
    .ExecuteUpdate(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));

Выполняется следующий SQL:

UPDATE [b]
SET [b].[Rating] = CAST((
    SELECT AVG(CAST([p].[Rating] AS float))
    FROM [Post] AS [p]
    WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]

Отслеживание изменений

Пользователи, знакомые с SaveChanges разными изменениями, используются для выполнения нескольких изменений, а затем вызова SaveChanges для применения всех этих изменений к базе данных. Это возможно с помощью средства отслеживания изменений EF, который накапливает или отслеживает эти изменения.

ExecuteUpdate и ExecuteDelete работают совершенно по-другому: они вступают в силу немедленно, в точке, в которой они вызываются. Это означает, что хотя одна ExecuteUpdate или ExecuteDelete операция может повлиять на множество строк, невозможно накапливать несколько таких операций и применять их одновременно, например при вызове SaveChanges. На самом деле, функции полностью не знают об отслеживании изменений EF и не имеют никакого взаимодействия с ним. Это имеет несколько важных последствий.

Рассмотрим следующий код:

// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");

// 2. Increase the rating of all blogs in the database by one. This executes immediately.
context.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;

// 4. Persist tracked changes to the database.
context.SaveChanges();

Важно отметить, что при ExecuteUpdate вызове и обновлении всех блогов в базе данных средство отслеживания изменений EF не обновляется, а отслеживаемый экземпляр .NET по-прежнему имеет исходное значение оценки с момента запроса. Предположим, что рейтинг блога был первоначально 5; После выполнения 3-й строки оценка в базе данных теперь составляет 6 (из-за ExecuteUpdate), а рейтинг в отслеживаемом экземпляре .NET равен 7. При SaveChanges вызове EF обнаруживает, что новое значение 7 отличается от исходного значения 5 и сохраняет это изменение. Изменение, выполняемое ExecuteUpdate перезаписывается и не учитывается.

В результате обычно рекомендуется избежать смешивания отслеживаемых SaveChanges изменений и незаслеченных изменений с помощью/ExecuteUpdateExecuteDelete.

Транзакции

Продолжая описанные выше действия, важно понимать, что ExecuteUpdate и ExecuteDelete неявно запускать транзакцию, которую они вызывают. Рассмотрим следующий код:

context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);

var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();

Каждый ExecuteUpdate вызов приводит к отправке одного SQL UPDATE в базу данных. Так как транзакция не создается, если какой-либо сбой препятствует успешному выполнению второго ExecuteUpdate , последствия первого из них по-прежнему сохраняются в базе данных. На самом деле, четыре операции выше - два вызова ExecuteUpdateзапроса и SaveChanges - каждый выполняется в рамках собственной транзакции. Чтобы упаковать несколько операций в одну транзакцию, явно запустите транзакцию с DatabaseFacadeпомощью:

using (var transaction = context.Database.BeginTransaction())
{
    context.Blogs.ExecuteUpdate(/* some update */);
    context.Blogs.ExecuteUpdate(/* another update */);

    ...
}

Дополнительные сведения об обработке транзакций см. в разделе "Использование транзакций".

Затронутые элемент управления параллелизмом и строки

SaveChanges предоставляет автоматический элемент управления параллелизмом, используя маркер параллелизма, чтобы убедиться, что строка не была изменена между моментом загрузки и моментом сохранения изменений в нем. Так как ExecuteUpdate и ExecuteDelete не взаимодействуйте с средство отслеживания изменений, они не могут автоматически применять элемент управления параллелизмом.

Однако оба этих метода возвращают количество строк, затронутых операцией; Это может быть особенно удобно для реализации контроля параллелизма самостоятельно:

// (load the ID and concurrency token for a Blog in the database)

var numUpdated = context.Blogs
    .Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
    .ExecuteUpdate(/* ... */);
if (numUpdated == 0)
{
    throw new Exception("Update failed!");
}

В этом коде мы используем оператор LINQ Where для применения обновления к определенному блогу, и только если его токен параллелизма имеет определенное значение (например, тот, который мы видели при запросе блога из базы данных). Затем мы проверка, сколько строк фактически обновленоExecuteUpdate; если результат равен нулю, строки не были обновлены, и маркер параллелизма, скорее всего, изменился в результате параллельного обновления.

Ограничения

  • В настоящее время поддерживается только обновление и удаление; Необходимо выполнить вставку с помощью DbSet<TEntity>.Add и SaveChanges().
  • Хотя инструкции SQL UPDATE и DELETE позволяют извлекать исходные значения столбцов для затронутых строк, это в настоящее время не поддерживается ExecuteUpdate и ExecuteDelete.
  • Несколько вызовов этих методов не могут быть пакетными. Каждое вызов выполняет собственную круглую попытку в базу данных.
  • Базы данных обычно позволяют изменять только одну таблицу с помощью UPDATE или DELETE.
  • Эти методы в настоящее время работают только с поставщиками реляционных баз данных.

Дополнительные ресурсы

  • Сеанс стенда сообщества доступа к данным .NET, где мы обсудим ExecuteUpdate и ExecuteDelete.