Atualização Eficiente de dados com EF Core

This post has been republished via RSS; it originally appeared at: Microsoft Tech Community - Latest Blogs - .

Entenda o processo de modificação dos dados

O que está por traz de um “DbContext.SaveChanges”

Como discutido nos artigos anteriores, o Entity Framework é uma camada de programação que fica acima da camada de acesso a dados, e seu objetivo é gerenciar a interação entre o sistema e o banco de dados. É importante ressaltar que a gestão dos dados em si é função do sistema gerenciador de banco de dados (SGBD), como o SQL Server, MySQL ou CosmosDB, entre outros.

Tanto consultas quanto atualizações de dados são realizadas através de comandos da linguagem SQL, que é implementada no SGBD. O papel do EF Core é criar e executar os comandos necessários para consultar ou atualizar os dados na base de dados.

No entanto, cada SGBD tem suas particularidades e, por isso, o mapeamento entre operações e comandos SQL é implementado na camada do provedor de acesso a dados, que está dentro da arquitetura do EF Core.

Em geral, ao trabalhar com o EF Core, o fluxo consiste em:

  1. Carregar os dados no contexto usando DbSet
  2. Incluir, modificar ou excluir objetos
  3. Executar SaveChanges ou SaveChangesAsync no “DbContext”

No momento em que se executa o SaveChanges/SaveChangesAsync o EF verifica quais foram as alterações (baseado nas informações de tracking) e transforma isso em uma série de comandos que são enviados ao SGBD para atualizar os dados. O que o EF faz aqui, a grosso modo, é gerar Inserts, Updates e Deletes a partir das modificações feitas nos itens.

Entender o processo ajuda a escrever código mais eficiente na hora de atualizar as informações no banco de dados.

Alterações unitárias

Se o código altera uma única entidade dentro do contexto um único comando é enviado ao banco de dados para efetuar as atualizações necessárias aos dados, como nesse exemplo:

    Blog blog = new("Novo blog");
    
    context.Blogs.Add(blog);
    
    context.SaveChanges();

Esse trecho de código gera o seguinte SQL:

      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);

É interessante notar aqui o uso de SET IMPLICIT_TRANSACTIONS OFF uma vez que uma única instrução SQL é executada e essa instrução é executada de forma atômica. Esse comando evita uma segunda a ida ao banco de dados para efetivar a transação que teria sido aberta implicitamente.

Como evitar “round trips” é sempre recomendável, vale a pena mencionar que não é necessário carregar o contexto antes de se inserir/alterar/excluir entidades. Por exemplo, para atualizar ou excluir uma entidade basta anexar o objeto diretamente ao contexto, desde que o objeto contenha a chave primária:

    Blog blog = new("Novo nome"){Id=1};
    
    context.Blogs.Update(blog);
    
    context.SaveChanges();
    Executed DbCommand (122ms) [Parameters=[@p1='1', @p0='Novo nome' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
    SET IMPLICIT_TRANSACTIONS OFF;
    SET NOCOUNT ON;
    UPDATE [Blogs] SET [Name] = @p0
    OUTPUT 1
    WHERE [Id] = @p1;

Atualizações em lote

Evitar “round trips” (requisições ao banco de dados) sempre é uma recomendação importante quando se trata de desempenho no acesso a dados, e pensando desta forma faz total sentido que as atualizações sejam enviadas em lote ao banco de dados ao invés de uma a uma.

O EF Core procura agrupar as alterações em batches, múltiplas instruções em uma única chamada, de forma a reduzir as ineficiências da comunicação remota.

Tomando como exemplo o código abaixo que insere 3 posts em um blog:

    Blog? blog = context.Blogs.Find(1);
    
    for (int i = 0; i < 3; i++)
    {
        Post newPost = new Post($"New Post {i}", $"New content {i}", DateTime.Today);
        blog?.Posts.Add(newPost);
    }
    
    context.SaveChanges();

Para este código temos o seguinte comando SQL e parâmetros nos logs:

    Executed DbCommand (11ms) [Parameters=[@p0=NULL (DbType = Int32), @p1='1', @p2='New content 0' (Nullable = false) (Size = 4000),@p3='Post' (Nullable = false) (Size = 4000), @p4='2023-01-01T00:00:00.0000000-03:00', @p5='New Post 0' (Nullable = false) (Size = 4000), @p6=NULL (DbType = Int32), @p7='1', @p8='New content 1' (Nullable = false) (Size = 4000), @p9='Post' (Nullable = false) (Size = 4000), @p10='2023-01-01T00:00:00.0000000-03:00', @p11='New Post 1' (Nullable = false) (Size = 4000), @p12=NULL (DbType = Int32), @p13='1', @p14='New content 2' (Nullable = false) (Size = 4000), @p15='Post' (Nullable = false) (Size = 4000), @p16='2023-01-01T00:00:00.0000000-03:00', @p17='New Post 2' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Posts] USING (
      VALUES (@p0, @p1, @p2, @p3, @p4, @p5, 0),
      (@p6, @p7, @p8, @p9, @p10, @p11, 1),
      (@p12, @p13, @p14, @p15, @p16, @p17, 2)) AS i ([AuthorId], [BlogId], [Content], [Discriminator], [PublishedOn], [Title], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([AuthorId], [BlogId], [Content], [Discriminator], [PublishedOn], [Title])
      VALUES (i.[AuthorId], i.[BlogId], i.[Content], i.[Discriminator], i.[PublishedOn], i.[Title])
      OUTPUT INSERTED.[Id], i._Position;

O comando gerado utiliza a instrução MERGE com a cláusula VALUES do SQL para inserir um conjunto de linhas em uma única transação no banco de dados, apesar de estranho essa é uma forma eficiente de inserir dados utilizando parâmetros.

Se ao invés de inserir novos itens estivermos alterando itens existentes como nesse exemplo:

    Blog? blog = await context.Blogs
                        .Include(b=>b.Posts)
                        .Where(b=>b.Id==1)
                        .FirstOrDefaultAsync();
    
    foreach (var post in blog?.Posts)
    {
        post.Content += "[Edited]";
    }
    
    context.SaveChanges();

Veríamos um comando SQL parametrizado similar a este aqui:

    Executed DbCommand (66ms) [Parameters=[@p1='21', @p0='New content 0[Edited]' (Nullable = false) (Size = 4000), @p3='22', @p2='New content 1[Edited]' (Nullable = false) (Size = 4000), @p5='23', @p4='New content 2[Edited]' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [Posts] SET [Content] = @p0
      OUTPUT 1
      WHERE [Id] = @p1;
      UPDATE [Posts] SET [Content] = @p2
      OUTPUT 1
      WHERE [Id] = @p3;
      UPDATE [Posts] SET [Content] = @p4
      OUTPUT 1
      WHERE [Id] = @p5; 

Note que neste lote cada alteração foi transformada em uma instrução UPDATE do SQL atualizando apenas as colunas modificadas.

O número de comandos executados em lote varia de acordo com o provider de banco de dados utilizado. A documentação do EF Core cita como exemplo o SQL Server que por padrão executa um máximo de 42 comandos em um único lote executando múltiplos lotes até terminar todas as alterações.

Este número foi determinado a partir de benchmarks, mas pode ser modificado via configuração do “DbContext”. É recomendado que caso se altere estes parâmetros medições sejam feitas para validar se o desempenho é o desejado.

    ...
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){
        optionsBuilder.UseSqlServer(connectionString,
            options => {
                options.MinBatchSize(1);
                options.MaxBatchSize(100);
            });
    }
    ...

Bulk updates

Obviamente enviar múltiplas atualizações em lotes é muito mais eficiente do que comandos individuais, porém ainda é possível ser mais eficiente em cenários onde várias linhas são atualizadas baseadas em algum critério.

Vamos imaginar um cenário onde desejamos excluir todos os posts publicados até um determinado ano, por exemplo 2018. Podemos escrever um código similar a este:

    DateTime baseDate = new DateTime(2019,1,1);
    var qry = context.Posts.Where(p=>p.PublishedOn < baseDate);
    foreach (var item in qry)
    {
        context.Posts.Remove(item);
    }
    context.SaveChanges();

Obviamente o código acima traz várias ineficiências (oops, você já escreveu algo assim, né?), de cara temos a questão dos “round trips”, um comando para selecionar os posts e, no melhor cenário, outro para excluir os registros.

Além disso, temos o custo de percorrer os resultados e carregar para a memória dados que não são necessários para a implementação da nossa regra de negócio e ainda iremos percorrer estes dados em um loop.

Considerando-se que o ideal para o nosso negócio é ter muitos blogs e muitos posts não é difícil imaginar que teremos problemas com isto (aposto que você lembrou de alguma história, certo?).

Muito ruim.

Na linguagem SQL é possível executar alterações em massa utilizando um único UPDATE/DELETE com uma cláusula WHERE.

O tracking do EF Core não consegue detectar este tipo de alteração, porém é possível executar estes comandos utilizando ExecuteSqlRaw e neste caso o nosso exemplo ficaria assim:

    context.Database.ExecuteSqlRaw("DELETE FROM Posts WHERE PublishedOn < '2019-01-01'");

Sempre vale lembrar que extremo cuidado é necessário em relação a validação das entradas de dados de forma a evitar possíveis injeções de SQL através de parâmetros concatenados.

Bulk updates com EF Core 7

O EF Core 7 introduz APIs para execução de bulk updates sem a necessidade de executar comandos SQL crus.

Os métodos ExecuteUpdate e ExecuteDelete (e suas versões assíncronas) são aplicados diretamente a uma query LINQ de forma a executar atualizações e exclusões baseadas em critérios na base de dados.

O mesmo comando para excluir os posts anteriores a 2019 no EF Core 7 pode ser executado da seguinte forma:

    DateTime baseDate = new DateTime(2019,1,1);
    await context.Posts
            .Where(p=>p.PublishedOn < baseDate)
            .ExecuteDeleteAsync();

O seguinte comando SQL será executado no banco de dados:

      Executed DbCommand (53ms) [Parameters=[@__baseDate_0='2019-01-01T00:00:00.0000000'], CommandType='Text', CommandTimeout='30']
      DELETE FROM [p]
      FROM [Posts] AS [p]
      WHERE [p].[PublishedOn] < @__baseDate_0

Conclusões

O ideal é, sempre que possível, procurar executar alterações a base de dados em lotes para reduzir o número de idas e vindas ao servidor de dados e no Entity Framework Core esse é o padrão.

Cuidado para não executar “SaveChanges” dentro de loops para permitir que o EF Core possa agregar comandos e otimizar a execução.

Sempre que houver a possibilidade de um bulk update este método deve ser utilizado e situações mais complexas envolvendo múltiplos comandos podem ser otimizadas usando comandos SQL ou executando-se stored procedures.

Referências

Artigos anteriores

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.