EF Core -  Múltiplos Requests p/ um DataBase em uma Transação


   Neste artigo vamos estudar como tratar múltiplos requests para um banco de dados em uma única transação usando o EF Core.

Ao trabalhar com Banco de Dados, seja por meio de APIs ou de outras formas, quando usamos o Entity Framework Core você sabe o que ocorre nos bastidores ?

Por exemplo, se estivermos realizando várias operações de banco de dados em uma única solicitação, isso criará uma conexão de banco de dados e confirmará as alterações separadamente,e com isso teremos a ocorrência de muitas viagens de ida e volta ao banco de dados para essa solicitação. Acrescente agora que isso pode corromper os dados se algo der errado.

Isso não parece ser um bom cenário, mas qual poderia ser uma abordagem mais robusta ?

Imagine abrir uma transação e fazer toda a operação do banco de dados dentro dela e, se todas elas forem concluídas, basta confirmar a transação.

Por meio dessa abordagem, a integridade dos dados pode ser alcançada e também, se alguma operação falhar, toda a transação pode ser revertida.

Parece uma boa ideia, certo ? Então, vamos ver como podemos conseguir isso.

Para ilustrar  vamos criar uma aplicação console referenciando o EF Core criar e configurar a classe de contexto e habilitar a exibição dos comandos SQL gerados pelo EF Core na execução dos comandos.

No projeto Console chamado EFCore_MultipleDBs vamos criar uma pasta Models onde vamos criar as seguintes classes das entidades:

1- Funcionario

public class Funcionario
{
    [Key]
    public int FuncionarioId { get; set; }
    public string? Nome { get; set; }
    Departamento? Departamento { get; set; }
    public int DepartamentoId { get; set; }
}

2- Departamento

public class Departamento
{
    [Key]
    public int DepartamentoId { get; set; }
    public string? NomeDepartamento { get; set; }
    public ICollection<Funcionario>? Funcionarios { get; set; }
}

Definimos as entidades Funcionario e Departamento e o relacionamento um-para-muitos entre as entidades usando as convenções do EF Core.

A seguir vamos criar a classe AppDbContext :

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.LogTo(Console.WriteLine);
        optionsBuilder.UseSqlServer("Data Source=.\\SQLEXPRESS;Initial Catalog=TesteDB2;Integrated Security=True");
    }
    public AppDbContext() : base()
    { }
    public DbSet<Funcionario>? Funcionarios { get; set; }
    public DbSet<Departamento>? Departamentos { get; set; }
}

Agora podemos aplicar o Migrations e gerar o banco e as tabelas usando os comandos:

Vamos definir no arquivo Program o seguinte código :

Console.WriteLine("\nPressione algo para iniciar...");
Console.ReadKey();
using (var context = new AppDbContext())
{
    context.Departamentos.Add(new Departamento() { NomeDepartamento = "Contabilidade" });
    context.SaveChanges();
    context.Funcionarios.Add(new Funcionario() { Nome = "David", DepartamentoId = 1 });
    context.SaveChanges();
}
Console.WriteLine("Processamento Concluido...");
Console.ReadKey();

Neste código vamos analisar oque ocorre quando emitimos o comando SaveChanges mais de uma vez na mesma transação.

Teremos o Log de Console gerado que podemos analisar (não vou publicar pois é muito extenso).

Se dermos uma olhada no trecho de código acima e nos logs de console correspondentes, podemos ver que SaveChanges() é chamado 2 vezes e em ambas as vezes o EF Core abriu conexões de banco de dados separadas e confirmou as alterações.

Isso significa que o EF Core está criando duas transações separadas para essas duas operações de inserção. Esta é uma abordagem dispendiosa e queremos evitá-la.

Agora, para obter vários SaveChanges() em uma única transação, podemos usar um recurso chamado BeginTransaction().

A instrução DbContext.Database.BeginTransaction(); cria uma nova transação para o banco de dados subjacente e nos permite confirmar ou reverter as alterações feitas no banco de dados usando várias chamadas de método SaveChanges.

Assim,, vamos reescrever o método escrito anteriormente usando BeginTransaction() :

Console.WriteLine("\nPressione algo para iniciar...");
Console.ReadKey();
using (var context = new AppDbContext())
{
    using (IDbContextTransaction transaction = context.Database.BeginTransaction())
    {
        try
        {
            context.Departamentos.Add(new Departamento() { NomeDepartamento = "RH" });
            context.SaveChanges();
            context.Funcionarios.Add(new Funcionario() { Nome = "Maria", DepartamentoId = 2 });
            context.SaveChanges();
            transaction.Commit();
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            Console.WriteLine(ex.Message);
        }
    }
}
Console.ReadKey();

No trecho de código acima, estamos criando uma transação por BeginTransaction(), fazendo todas as operações de banco de dados dentro dela e finalmente confirmando por Commit().

Se olharmos para o log do console, podemos ver que a conexão do banco de dados é aberta apenas uma vez e confirmada apenas uma vez, e as duas operações de inserção são feitas dentro dessa transação.

Voilà, então é isso que queríamos alcançar, lidando com várias solicitações de banco de dados em uma única transação.

Mas isso é tudo? 

Na verdade, não. 

E se houver várias transações e qualquer uma delas falhar?

De forma ideal, toda a transação deve ser revertida ou revertida.

Agora, a parte interessante é que também podemos conseguir isso na mesma abordagem.

Vamos adicionar uma exceção forçada no código acima.

Console.WriteLine("\nPressione algo para iniciar...");
Console.ReadKey();
using (var context = new AppDbContext())
{
    using (IDbContextTransaction transaction = context.Database.BeginTransaction())
    {
        try
        {
            context.Departamentos.Add(new Departamento() { NomeDepartamento = "Compras" });
            context.SaveChanges();
            context.Funcionarios.Add(new Funcionario() { Nome = "Paulo", DepartamentoId = 3 });
            context.SaveChanges();
               // lança uma exectiopn para testar o roll back da transação
               throw new Exception();
            transaction.Commit();
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            Console.WriteLine(ex.Message);
        }
    }
}
Console.ReadKey();

Na seção try do trecho acima, estamos forçando uma exceção e na seção catch temos o método Rollback() desfaz a operação de forma segura.

Nota: Você pode especificar diferentes níveis de isolamento no método DbContext.Database.BeginTransaction().

Então, é assim que podemos lidar com várias solicitações de banco de dados em uma única transação usando o EF Core de maneira eficiente.

E estamos conversados...

Pegue o projeto aqui :  EFCore_MultipleDBs.zip

"Porque nele(Jesus) foram criadas todas as coisas que há nos céus e na terra, visíveis e invisíveis, sejam tronos, sejam dominações, sejam principados, sejam potestades. Tudo foi criado por ele e para ele."
Colossenses 1:16

Referências:


José Carlos Macoratti