ExecuteUpdate e ExecuteDelete

Observação

Esse recurso foi introduzido na versão 7.0 do Entity Framework Core.

ExecuteUpdate e ExecuteDelete são uma forma de salvar dados no banco de dados sem usar o controle de alterações tradicional do EF e o método SaveChanges(). Para obter uma comparação introdutória dessas duas técnicas, consulte a página de Visão geral sobre como salvar dados.

ExecuteDelete

Vamos supor que você precise excluir todos os Blogs com uma classificação abaixo de um determinado limite. A abordagem tradicional SaveChanges() exige que você faça o seguinte:

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

context.SaveChanges();

Essa é uma maneira muito ineficiente de executar essa tarefa: consultamos o banco de dados em busca de todos os blogs que correspondem ao nosso filtro e, em seguida, consultamos, materializamos e rastreamos todas essas instâncias; o número de entidades correspondentes pode ser enorme. Em seguida, informamos ao controlador de alterações do EF que cada blog precisa ser removido e aplicamos essas alterações chamando SaveChanges(), o que gera uma instrução DELETE para cada um deles.

Aqui está a mesma tarefa executada por meio da API ExecuteDelete:

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

Ela utiliza os operadores LINQ conhecidos para determinar quais blogs devem ser afetados, como se estivéssemos consultando-os e, em seguida, diz ao EF para executar um DELETE SQL no banco de dados:

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

Além de ser mais simples e mais curta, essa execução é muito eficiente no banco de dados, sem carregar nenhum dado do banco de dados ou envolver o controlador de alterações do EF. Observe que você pode utilizar operadores LINQ arbitrários para selecionar quais blogs deseja excluir; eles são traduzidos para o SQL para execução no banco de dados, exatamente como se você estivesse consultando esses Blogs.

ExecuteUpdate

Em vez de excluir esses blogs, e se você desejasse alterar uma propriedade para indicar que eles devem ser ocultados? ExecuteUpdate fornece uma maneira semelhante de expressar uma instrução SQL UPDATE:

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

Como em ExecuteDelete, primeiro utilizamos a LINQ para determinar quais blogs devem ser afetados; mas com ExecuteUpdate também precisamos expressar a alteração a ser aplicada aos Blogs correspondentes. Isso é feito chamando SetProperty dentro da chamada ExecuteUpdate e fornecendo-lhe dois argumentos: a propriedade que será alterada (IsVisible) e o novo valor que ela deve ter (false). Isso faz com que o SQL a seguir seja executado:

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

Atualizações de várias propriedades

ExecuteUpdate permite a atualização de várias propriedades em uma única invocação. Por exemplo, para definir IsVisible como falso e Rating como zero, basta encadear chamadas SetProperty adicionais:

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

Isso executa o seguinte SQL:

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

Referenciando o valor da propriedade existente

Os exemplos acima atualizaram a propriedade para um novo valor constante. ExecuteUpdate também permite fazer referência ao valor da propriedade existente ao calcular o novo valor; por exemplo, para aumentar a classificação de todos os blogs correspondentes em um, utilize o seguinte:

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

Observe que o segundo argumento de SetProperty agora é uma função lambda e não uma constante como antes. O parâmetro b representa o Blog que está sendo atualizado; dentro desse lambda, b.Rating contém a classificação antes de qualquer alteração ocorrer. Isso executa o seguinte SQL:

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

Atualmente, ExecuteUpdate não dá suporte à referência de navegações dentro do lambda SetProperty. Por exemplo, digamos que desejamos atualizar todas as classificações dos blogs de modo que a nova classificação de cada blog seja a média de todas as classificações das suas postagens. Podemos tentar utilizar ExecuteUpdate da seguinte forma:

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

No entanto, o EF permite executar essa operação usando primeiro Select para calcular a classificação média e projetá-la em um tipo anônimo e, em seguida, usando ExecuteUpdate sobre ela:

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));

Isso executa o seguinte 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]

controle de alterações

Os usuários familiarizados com SaveChanges estão acostumados a executar várias alterações e, em seguida, chamar SaveChanges para aplicar todas essas alterações ao banco de dados; isso é possível graças ao controlador de alterações do EF, que acumula, ou controla, essas alterações.

ExecuteUpdate e ExecuteDelete funcionam de forma bastante diferente: eles entram em vigor imediatamente, no ponto em que são invocados. Isso significa que, embora uma única operação ExecuteUpdate ou ExecuteDelete possa afetar muitas linhas, não é possível acumular várias dessas operações e aplicá-las uma vez, por exemplo, ao chamar SaveChanges. De fato, as funções desconhecem completamente o controlador de alterações do EF e não têm nenhuma interação com ele. Isso tem várias consequências importantes.

Considere o seguinte código:

// 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();

Crucialmente, quando ExecuteUpdate é invocado e todos os Blogs são atualizados no banco de dados, o controlador de alterações do EF não é atualizado, e a instância .NET rastreada ainda tem seu valor de classificação original, a partir do ponto em que foi consultada. Vamos supor que a classificação do Blog era originalmente 5; após a execução da terceira linha, a classificação no banco de dados agora é 6 (por causa do ExecuteUpdate), enquanto a classificação na instância .NET rastreada é 7. Quando SaveChanges é chamado, o EF detecta que o novo valor 7 é diferente do valor original 5 e persiste essa alteração. A alteração realizada por ExecuteUpdate foi substituída e não foi levada em conta.

Como resultado, geralmente é uma boa ideia evitar misturar as modificações SaveChanges rastreadas e as modificações não rastreadas via ExecuteUpdate/ExecuteDelete.

Transactions

Continuando com o que foi mencionado acima, é importante entender que ExecuteUpdate e ExecuteDelete não iniciam implicitamente uma transação quando são invocados. Considere o seguinte código:

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

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

Cada chamada ExecuteUpdate faz com que um único SQL UPDATE seja enviado ao banco de dados. Como nenhuma transação é criada, se algum tipo de falha impedir que a segunda ExecuteUpdate tenha uma conclusão bem-sucedida, os efeitos da primeira ainda serão mantidos no banco de dados. De fato, as quatro operações acima, duas invocações de ExecuteUpdate, uma consulta e SaveChanges, são executadas cada uma na sua própria transação. Para encapsular várias operações em uma única transação, inicie explicitamente uma transação com DatabaseFacade:

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

    ...
}

Para obter mais informações sobre o tratamento de transações, consulte Usando transações.

Controle de simultaneidade e linhas afetadas

SaveChanges fornece Controle de Simultaneidade automático, utilizando um token de simultaneidade para garantir que uma linha não foi alterada entre o momento em que você a carregou e o momento em que você salvou as alterações nela. Como ExecuteUpdate e ExecuteDelete não interagem com o controlador de alterações, eles não podem aplicar automaticamente o controle de simultaneidade.

No entanto, esses dois métodos retornam o número de linhas afetadas pela operação; isso pode ser particularmente útil para implementar o controle de simuntaneidade por conta própria:

// (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!");
}

Neste código, utilizamos um operador LINQ Where para aplicar uma atualização a um Blog específico e somente se o token de simultaneidade tiver um valor específico (por exemplo, o que vimos ao consultar o Blog no banco de dados). Em seguida, verificamos quantas linhas foram realmente atualizadas por ExecuteUpdate; se o resultado for zero, nenhuma linha foi atualizada e o token de simultaneidade provavelmente foi alterado como resultado de uma atualização simultânea.

Limitações

  • No momento, somente a atualização e a exclusão têm suporte; a inserção deve ser feita por meio de DbSet<TEntity>.Add e SaveChanges().
  • Embora as instruções SQL UPDATE e DELETE permitam a recuperação dos valores originais da coluna para as linhas afetadas, isso não tem suporte atualmente por ExecuteUpdate e ExecuteDelete.
  • Várias invocações desses métodos não podem ser colocadas em lote. Cada invocação executa sua própria viagem de ida e volta ao banco de dados.
  • Normalmente, os bancos de dados permitem que apenas uma única tabela seja modificada com UPDATE ou DELETE.
  • Atualmente, esses métodos só funcionam com provedores de bancos de dados relacionais.

Recursos adicionais