Compartilhar via


Entity Framework Core 1.0: Transações em lote (Batching)

Introdução

Dentre as diversas mudanças ocorridas na versão Core 1.0 do Entity Framework em relação a suas últimas versões (6.x), destaca-se uma nova implementação que possibilita o agrupamento de transações com o banco de dados em forma de lotes, denominado como "Batching". 

Nas versões anteriores do ORM, as transações efetuadas na aplicação eram executadas de forma separada, resultando em diversas chamadas ao banco de dados, mesmo que fossem operações semelhantes (Inserções ou alterações na mesma tabela). Muitos desenvolvedores até escreviam rotinas para implementar tal funcionalidade e foi a partir de recorrentes feedbacks que esse recurso entrou na lista de novos recursos da versão Core 1.0 do EF.

Essa nova implementação agora é nativa e provê um ganho na redução de requisições feitas ao banco de dados no momento que a transação é salva.
Outras vantagens consideráveis do uso desse recurso no EF Core são:

  • Redução dos problemas provenientes da latência quando usamos bancos de dados alocados na nuvem, como o SQL Azure;
  • Alteração mínima do código.

Exemplo

Afim de demonstrar o uso do recurso de batching no EF Core, o projeto criado a seguir é do tipo Console Application (porém o EF Core tem suporte para diversos outros tipos de projeto, como ASP.NET, ASP.NET Core, WinForms, WPF  UWP e em breve para Xamarin) e a base de dados utilizada foi o SQL Server rodando localmente na máquina, sendo criada segundo o modelo Code First.

O primeiro passo é baixar o pacote do EF Core através do NuGet Package Console, executando o seguinte comando:
Install-Package Microsoft.EntityFrameworkCore.SqlServer

A classe criada para efeito de testes chama-se Livro e pode ser observada abaixo:

public class  Livro
{
    public int  LivroId { get; set; }
    public string  Titulo { get; set; }
    public string  Autor { get; set; }
    public int  AnoPublicacao { get; set; }
}

Neste passo iremos implementar o contexto, onde criamos um DbSet para a Classe Livro e sobrescrevemos o método OnConfiguring para setar algumas opções específicas. É aqui onde é definido o provider a ser utilizado pelo EF (neste caso o SQL Server) e a sua connection string, assim como a definição da quantidade máxima de operações executados por lote (neste caso serão 2 operações).

public class  LivrosContext : DbContext
{
    public DbSet<Livro> Livros { get; set; }
 
    protected override  void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFCore.Demo;Trusted_Connection=True;",
             options => options.MaxBatchSize(2));
    }
}

O código abaixo utiliza o contexto criado para criar novos livros, adicioná-los ao DbSet e por fim salvar as alterações.

using (var db = new LivrosContext())
{
    db.Database.EnsureCreated();
 
    if (db.Livros.Any())
      db.Database.ExecuteSqlCommand("DELETE FROM dbo.Livros");
 
    db.Livros.Add(new Livro { Titulo = "Domain-Driven Design: Tackling Complexity in the Heart of Software", Autor = "Eric Evans", AnoPublicacao = 2003 });
    db.Livros.Add(new Livro { Titulo = "Agile Principles, Patterns, and Practices in C#", Autor = "Robert C. Martin", AnoPublicacao = 2006 });
    db.Livros.Add(new Livro { Titulo = "Clean Code: A Handbook of Agile Software Craftsmanship", Autor = "Robert C. Martin", AnoPublicacao = 2008 });
    db.Livros.Add(new Livro { Titulo = "Implementing Domain-Driven Design", Autor = "Vaughn Vernon", AnoPublicacao = 2013 });
    db.Livros.Add(new Livro { Titulo = "Patterns, Principles, and Practices of Domain-Driven Design", Autor = "Scott Millet", AnoPublicacao = 2015 });
    db.Livros.Add(new Livro { Titulo = "Refactoring: Improving the Design of Existing Code ", Autor = "Martin Fowler", AnoPublicacao = 2012 });
 
    db.SaveChanges();
}

Executando e Analisando os testes

Executando o código criado, não temos nenhuma comprovação que o mesmo foi executado em lotes. Para analisar as transações enviadas para o banco de dados, será utilizado o Microsoft SQL Server Profiler que é uma ferramenta anexa ao SQL Server Management Studio (Baixe-o aqui)

Abra o SQL Server Management Studio, vá até o menu Tools > SQL Server Profiler

Defina o servidor e credenciais para acessar, conforme imagem abaixo.

Na janela de "Trace Properties", Clique no botão Run. Neste momento o profiler estará listando em tempo real todas as atividades ocorridas na sua base de dados.
Execute a aplicação novamente e será possível verificar a quantidade e o conteúdo das transações realizadas.
Abaixo é possível ver o profiler dos testes executados.

Sem uso de transações em lote

Neste primeiro teste, não foi utilizado o recurso de batching, portanto foram realizadas 6 transações para inserir os 6 livros na base de dados, conforme mostra as linhas destacadas em azul na imagem abaixo.

Cada uma das 6 requisições gera uma instrução T-SQL semelhante a que é demonstrada abaixo.

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Livros] ([AnoPublicacao], [Autor], [Titulo])
VALUES (@p0, @p1, @p2);
SELECT [LivroId]
FROM [Livros]
WHERE @@ROWCOUNT = 1 AND [LivroId] = scope_identity();
',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000)',@p0=2003,@p1=N'Eric Evans',@p2=N'Domain-Driven Design: Tackling Complexity in the Heart of Software'

Com uso de transações em lote

Neste segundo teste, foi utilizado o recurso de batching (configuradas para executar 2 operações por lote), portanto foram realizadas 3 transações para inserir os 6 livros na base de dados, conforme mostra as linhas destacadas em azul na imagem abaixo.

Cada uma das 3 requisições (lote, agrupando duas instruções, conforme o que foi configurado) gera uma instrução T-SQL semelhante a que é demonstrada abaixo.

exec sp_executesql N'SET NOCOUNT ON;
DECLARE @toInsert1 TABLE ([AnoPublicacao] int, [Autor] nvarchar(max), [Titulo] nvarchar(max), [_Position] [int]);
INSERT INTO @toInsert1
VALUES (@p0, @p1, @p2, 0),
(@p3, @p4, @p5, 1);
 
DECLARE @inserted1 TABLE ([LivroId] int, [AnoPublicacao] int, [Autor] nvarchar(max), [Titulo] nvarchar(max), [_Position] [int]);
MERGE [Livros] USING @toInsert1 AS i ON 1=0
WHEN NOT MATCHED THEN
INSERT ([AnoPublicacao], [Autor], [Titulo])
VALUES (i.[AnoPublicacao], i.[Autor], i.[Titulo])
OUTPUT INSERTED.[LivroId], INSERTED.[AnoPublicacao], INSERTED.[Autor], INSERTED.[Titulo], i._Position
INTO @inserted1;
 
SELECT [LivroId] FROM @inserted1
ORDER BY _Position;
',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 int,@p4 nvarchar(4000),@p5 nvarchar(4000)',@p0=2003,@p1=N'Eric Evans',@p2=N'Domain-Driven Design: Tackling Complexity in the Heart of Software',@p3=2006,@p4=N'Robert C. Martin',@p5=N'Agile Principles, Patterns, and Practices in C#'

Conclusão

A utilização de recursos que reduzam o alto tráfego de requisições existente nas aplicações para manipulação de dados é de grande valia, uma vez que o ganho de performance sempre é uma das metas dos desenvolvedores e faz toda a diferença na experiência final do usuário. Através do novo recurso de batching existente no Entity Framework Core 1.0, é possível evidenciar essa diferença, tendo grande impacto principalmente quando o servidor de aplicação não é o mesmo ou está geograficamente separado do servidor de banco de dados.

Referências

Documentação do EF Core
https://docs.efproject.net