Partilhar via


ExecuteUpdate e ExecuteDelete

ExecuteUpdate e ExecuteDelete são uma maneira de salvar dados no banco de dados sem usar o método e o controle SaveChanges() de alterações tradicionais do EF. Para obter uma comparação introdutória dessas duas técnicas, consulte a página 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:

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

await context.SaveChangesAsync();

Esta é uma maneira bastante ineficiente de executar essa tarefa: consultamos o banco de dados para 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, dizemos ao rastreador de alterações da EF que cada Blog precisa ser removido e aplicamos essas alterações chamando SaveChanges(), o que gera uma declaração para cada um DELETE deles.

Aqui está a mesma tarefa executada através da ExecuteDelete API:

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

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

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

Além de ser mais simples e curto, ele é executado de forma muito eficiente no banco de dados, sem carregar nenhum dado do banco de dados ou envolver o rastreador de alterações do EF. Observe que você pode usar operadores LINQ arbitrários para selecionar quais Blogs você gostaria de excluir - eles são traduzidos para SQL para execução no banco de dados, como se você estivesse consultando esses Blogs.

ExecutarAtualização

Em vez de excluir esses blogs, e se quiséssemos alterar uma propriedade para indicar que eles deveriam ser ocultos? ExecuteUpdate fornece uma maneira semelhante de expressar uma instrução SQL UPDATE :

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

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

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

Atualização de várias propriedades

ExecuteUpdate Permite atualizar várias propriedades em uma única invocação. Por exemplo, para definir IsVisible como false e para definir Rating como zero, basta encadear chamadas adicionais SetProperty juntas:

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

Fazendo referência ao valor da propriedade existente

Os exemplos acima atualizaram a propriedade para um novo valor constante. ExecuteUpdate também permite referenciar o valor da propriedade existente ao calcular o novo valor; por exemplo, para incrementar em mais um a classificação de todos os blogs correspondentes, use o seguinte:

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

Observe que o segundo argumento para SetProperty agora é uma função lambda, e não uma constante como antes. Seu b parâmetro representa o Blog que está sendo atualizado, dentro dessa lambda, portanto, 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

ExecuteUpdate Atualmente, não oferece suporte a navegações de referência dentro do SetProperty Lambda. Por exemplo, digamos que queremos atualizar todas as classificações dos Blogs para que a nova classificação de cada Blog seja a média de todas as classificações de suas Postagens. Podemos tentar usar ExecuteUpdate da seguinte forma:

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

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

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

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]

Acompanhamento de alterações

Os usuários familiarizados com SaveChanges estão habituados a realizar várias alterações e, em seguida, chamar SaveChanges para aplicar todas essas alterações ao banco de dados; isto é possível graças ao rastreador de alterações do EF, que acumula - ou rastreia - essas alterações.

ExecuteUpdate e ExecuteDelete funcionam de forma bem diferente: produzem efeitos imediatos, no momento em que são invocados. Isso significa que, embora uma única ExecuteUpdate ou ExecuteDelete operação possa afetar muitas linhas, não é possível acumular várias dessas operações e aplicá-las ao mesmo tempo, por exemplo, ao chamar SaveChanges. Na verdade, as funções desconhecem completamente o rastreador de mudanças do EF e não têm nenhuma interação com ele. Isto 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 = 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();

Crucialmente, quando ExecuteUpdate é invocado e todos os blogs são atualizados na base de dados, o controlador de alterações do EF não é atualizado, e a instância .NET rastreada ainda mantém o seu valor de classificação original, desde o momento em que foi consultada. Vamos supor que a classificação do Blog era originalmente 5; após a execução da 3ª 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 deteta que o novo valor 7 é diferente do valor original 5 e persiste essa alteração. A alteração realizada por ExecuteUpdate é sobreposta e não considerada.

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

Transações

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

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

Cada ExecuteUpdate chamada faz com que um único SQL UPDATE seja enviado para o banco de dados. Como nenhuma transação é criada, se qualquer tipo de falha impedir que a segunda ExecuteUpdate seja concluída com êxito, os efeitos da primeira ainda persistem no banco de dados. Na verdade, as quatro operações acima - duas invocações de ExecuteUpdate, uma consulta e SaveChanges - executam cada uma dentro de 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, usando 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 salvou as alterações nela. Como ExecuteUpdate e ExecuteDelete não interagem com o rastreador de alterações, eles não podem aplicar automaticamente o controlo de simultaneidade.

No entanto, ambos os métodos retornam o número de linhas que foram afetadas pela operação; Isso pode ser particularmente útil para implementar o controle de simultaneidade por conta própria:

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

Neste código, usamos um operador LINQ Where para aplicar uma atualização a um Blog específico, e somente se seu token de simultaneidade tiver um valor específico (por exemplo, aquele que vimos ao consultar o Blog do 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

  • Atualmente, apenas a atualização e a exclusão são suportadas; A inserção deve ser feita via DbSet<TEntity>.Add e SaveChanges().
  • Embora as instruções SQL UPDATE e DELETE permitam recuperar valores de coluna originais para as linhas afetadas, isso não é suportado atualmente por ExecuteUpdate e ExecuteDelete.
  • Várias invocações desses métodos não podem ser agrupadas em lote. Cada invocação executa sua própria viagem de ida e volta ao banco de dados.
  • Os bancos de dados normalmente permitem que apenas uma única tabela seja modificada com UPDATE ou DELETE.
  • Atualmente, esses métodos só funcionam com provedores de banco de dados relacionais.

Recursos adicionais