EF Core -  Usando transações


  Hoje vamos recordar os conceitos sobre transações no EF Core.

Vamos iniciar apresentando qual o comportamento padrão das transações no EF Core.

Transações - Comportamento padrão

O comportamento padrão das transações no EF Core é o seguinte:

  1. Todas as operações de leitura e gravação em uma única chamada ao método SaveChanges() são executadas em uma única transação. Se alguma operação falhar, toda a transação será revertida.
     
  2. O EF Core suporta transações explícitas, que podem ser gerenciadas usando a classe DbContextTransaction. As transações explícitas permitem que os desenvolvedores controlem manualmente o escopo das transações e possam realizar várias operações em uma única transação.
     
  3. Quando o EF Core é usado com um provedor de banco de dados que suporta transações distribuídas, como o SQL Server, o EF Core pode ser usado para gerenciar transações distribuídas que envolvem vários bancos de dados ou recursos.
     
  4. O EF Core também suporta transações de leitura não bloqueantes (non-locking read transactions) para bancos de dados que suportam esse recurso, como o SQL Server. As transações de leitura não bloqueantes permitem que várias transações de leitura possam ser executadas simultaneamente, sem bloquear outras transações de leitura ou gravação.

Desta forma, esse recurso dos bancos de dados SQL nos poupa de muitas dores de cabeça. Não precisamos pensar se os bancos de dados permanecem em um estado inconsistente, porque as transações do banco de dados podem fazer o trabalho para nós.

Consideremos o seguinte código:

using var context = new ShoppingContext();

context.Items.Add(new Item
{
   ProdutoId = produtoId,
   Quantidade = quantidade
});

var estoque = context.Estoque.FirstOrDefault(s => s.ProdutoId == produtoId);

estoque.Quantidade -= quantidade;

context.SaveChanges();
 

Como estamos adicionando um Item e, no mesmo escopo, reduzindo a quantidade de estoque, a chamada a SaveChanges aplicará as duas alterações dentro de uma transação. Podemos garantir que o banco de dados permanecerá em um estado consistente.

Criando transações com o EF Core

E se você quiser ter mais controle sobre as transações ao trabalhar com o EF Core ?

Você pode criar manualmente uma transação acessando o facade Database disponível em uma instância de DbContext e chamando BeginTransaction.

Aqui está um exemplo em que temos várias chamadas para SaveChanges.

No cenário padrão, ambas as chamadas seriam executadas em suas próprias transações. Isso deixa a possibilidade da segunda chamada para SaveChanges falhar e deixar o banco de dados em um estado inconsistente.

using var context = new ShoppingContext();

using var transaction = context.Database.BeginTransaction();

try
{
   context.Items.Add(
new Item
   {
       ProdutoId = produtoId,
       Quantidade = quantidade
    });
    context.SaveChanges();

    var estoque = context.Estoque.FirstOrDefault(s => s.ProdutoId == produtoId);

    estoque.Quantidade -= quantidade;

    context.SaveChanges();

     // Ao comitar as alterações elas serão aplicadas ao banco de dados
     // A transação fará um auto-rollback quando ela for liberada
     // se qualquer comando falhar

     transaction.Commit();
}

catch
(Exception)
{
   transaction.Rollback();
}

Invocamos BeginTransaction para iniciar manualmente uma nova transação de banco de dados. Isso criará uma nova transação e a retornará, para que possamos confirmar a transação quando quisermos concluir a operação.

Você também deseja adicionar um bloco try-catch ao seu código, para que possa reverter a transação se houver alguma exceção.

A seguir temos o exemplo de código mostrando o uso de uma transação explícita no EF Core:

using (var context = new MeuContexto())
{
    using (var transaction = context.Database.BeginTransaction())
    {
        try
        {
            // Realize as operações que deseja realizar dentro da transação
            context.Pessoas.Add(new Pessoa { Nome = "João" });
            context.Pessoas.Add(new Pessoa { Nome = "Maria" });
            context.SaveChanges();

            // Se chegou até aqui sem erros, comite a transação
            transaction.Commit();
        }
        catch (Exception ex)
        {
            // Se ocorrer um erro, desfaça a transação
            transaction.Rollback();
            Console.WriteLine($"Erro: {ex.Message}");
        }
    }
}

Neste exemplo, estamos criando uma nova instância do contexto de banco de dados do EF Core (MeuContexto), e em seguida iniciando uma nova transação explicitamente utilizando o método BeginTransaction() do objeto Database do contexto.

Dentro do bloco try da transação, estamos realizando algumas operações no banco de dados (adicionando duas pessoas ao banco de dados), e em seguida salvando as mudanças usando o método SaveChanges().

Se todas as operações dentro do bloco try forem bem sucedidas, a transação é comitada usando o método Commit() do objeto DbContextTransaction.

No entanto, se ocorrer um erro durante a execução das operações, a transação é desfeita usando o método Rollback() do objeto DbContextTransaction. Além disso, o erro é capturado e uma mensagem de erro é exibida na tela.

Usando transações existentes com o EF Core

Criar uma transação usando o EF Core DbContext não é a única opção. Você pode criar uma instância SqlTransaction e passá-la para o EF Core, para que as alterações aplicadas com o EF Core possam ser confirmadas dentro da mesma transação.

Assim, para criar uma instância de SqlTransaction e passá-la para o EF Core, você pode usar o método BeginTransaction() da classe SqlConnection e, em seguida, passar o objeto SqlTransaction resultante para o método UseTransaction() do objeto DbContext.

Continuando com o exemplo anterior a seguir temos um exemplo para este cenário:

using (var connection = new SqlConnection("connectionString"))
{
    await connection.OpenAsync();
    using (var transaction = connection.BeginTransaction())
    {
        try
        {
            using (var context = new MeuContexto())
            {
                context.Database.UseTransaction(transaction);
                // Realize as operações que deseja realizar dentro da transação
                context.Pessoas.Add(new Pessoa { Nome = "João" });
                context.Pessoas.Add(new Pessoa { Nome = "Maria" });
                await context.SaveChangesAsync();
            }
            // Se chegou até aqui sem erros, comite a transação
            transaction.Commit();
        }
        catch (Exception ex)
        {
            // Se ocorrer um erro, desfaça a transação
            transaction.Rollback();
            Console.WriteLine($"Erro: {ex.Message}");
        }
    }
}

Neste exemplo, estamos criando uma nova conexão com o banco de dados e abrindo-a usando o método OpenAsync(), e a seguir estamos iniciando uma nova transação usando o método BeginTransaction() da conexão.

Dentro do bloco try da transação, estamos criando uma nova instância do contexto de banco de dados do EF Core (MeuContexto) e, em seguida, chamando o método UseTransaction() do objeto Database do contexto, passando o objeto SqlTransaction criado anteriormente.

Em seguida, realizamos algumas operações no banco de dados usando o contexto do EF Core, adicionando duas pessoas ao banco de dados e salvando as alterações usando o método SaveChangesAsync().

Se todas as operações dentro do bloco try forem bem sucedidas, a transação é comitada usando o método Commit() do objeto SqlTransaction.

No entanto, se ocorrer um erro durante a execução das operações, a transação é desfeita usando o método Rollback() do objeto SqlTransaction. Além disso, o erro é capturado e uma mensagem de erro é exibida na tela.

Desta forma, vimos que o EF Core tem um excelente suporte para transações sendo muito fácil de usar.

Você tem três opções disponíveis:

1- Confie no comportamento de transação padrão;
2- Criar uma nova transação;
3- Usar uma transação existente;

Na maioria das vezes, você deseja confiar no comportamento padrão e não precisa pensar nisso.

E estamos conversados...

Referências:


José Carlos Macoratti