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
Navegações e entidades relacionadas
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
eExecuteDelete
. - 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
- Sessão de Standup da Comunidade de Acesso a Dados .NET em que discutimos
ExecuteUpdate
eExecuteDelete
.