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


Функции ExecuteUpdate и ExecuteDelete

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

ExecuteDelete

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

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

await context.SaveChangesAsync();

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

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

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

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

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

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

Выполнить обновление

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

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(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 вызовов:

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(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 также позволяет ссылаться на существующее значение свойства при вычислении нового значения; Например, чтобы увеличить рейтинг всех соответствующих блогов по одному, используйте следующее:

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(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. Например, предположим, что мы хотим обновить все рейтинги блогов, чтобы новый рейтинг каждого блога был средним значением всех оценок его постов. Мы можем попробовать использовать ExecuteUpdate следующим образом:

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

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

await context.Blogs
    .Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
    .ExecuteUpdateAsync(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 = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");

// 2. Increase the rating of all blogs in the database by one. This executes immediately.
await context.Blogs.ExecuteUpdateAsync(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.
await context.SaveChangesAsync();

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

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

Транзакции

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

await context.Blogs.ExecuteUpdateAsync(/* some update */);
await context.Blogs.ExecuteUpdateAsync(/* another update */);

var blog = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");
blog.Rating += 2;
await context.SaveChangesAsync();

Каждый 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 = await context.Blogs
    .Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
    .ExecuteUpdateAsync(/* ... */);
if (numUpdated == 0)
{
    throw new Exception("Update failed!");
}

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

Ограничения

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

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