Transações são um conceito essencial no gerenciamento de bancos de dados, pois garantem a consistência e a integridade dos dados durante a execução de operações. No contexto de um aplicativo que interage com um banco de dados, as transações desempenham um papel crucial para garantir que, quando múltiplas operações são realizadas, elas ocorram como uma única unidade atômica. Neste artigo vamos explorar como implementar transações no Entity Framework Core.
O que são transações?
Uma transação é um conjunto de operações que são tratadas como uma única unidade de trabalho. As transações possuem quatro propriedades essenciais, conhecidas pela sigla ACID:
- Atomicidade: ou seja, ou todas as operações dentro da transação são completadas com sucesso, ou nenhuma delas é. Em outras palavras, se uma falha ocorrer durante a execução da transação, todas as alterações realizadas até aquele ponto devem ser revertidas.
- Consistência: ao final da transação, o banco de dados deve estar em um estado consistente, ou seja, deve respeitar todas as regras de integridade e as restrições impostas.
- Isolamento: as alterações realizadas dentro de uma transação não devem ser visíveis para outras transações até que a transação atual seja completada (commit).
- Durabilidade: após o commit de uma transação, as alterações realizadas são permanentes, mesmo que ocorra uma falha de sistema.
Quando se trabalha com um banco de dados, é comum realizar múltiplas operações (como inserir, atualizar ou excluir dados) em uma única execução. Sem transações, se uma dessas operações falhar, o banco de dados pode ficar em um estado inconsistente. É aqui que as transações se tornam essenciais.
Transações explícitas com EF Core
O EF Core oferece suporte a transações explícitas, onde você pode criar e gerenciar a transação de maneira manual. Isso é útil quando você precisa de maior controle sobre o ciclo de vida da transação, como em operações complexas que envolvem várias etapas ou interações com diferentes fontes de dados.
Imaginemos uma situação em que você precisa salvar informações de um cliente e de um pedido em tabelas diferentes. Assegurando que ambos os registros sejam inseridos corretamente, podemos usar transações explícitas.
using (var context = new AppDbContext())
{
using var transaction = context.Database.BeginTransaction();
try
{
// Operação 1: Inserir um cliente
var cliente = new Cliente { Nome = "João Silva" };
context.Clientes.Add(cliente);
context.SaveChanges();
// Operação 2: Inserir um pedido relacionado
var pedido = new Pedido { ClienteId = cliente.Id, Total = 100.50m };
context.Pedidos.Add(pedido);
context.SaveChanges();
// Confirmar a transação
transaction.Commit();
}
catch (Exception ex)
{
// Reverter as alterações em caso de erro
transaction.Rollback();
Console.WriteLine($"Erro ao processar transação: {ex.Message}");
throw;
}
}
O início de uma transação no Entity Framework Core é feito através de “context.Database.BeginTransaction()”, que cria uma nova transação. Durante a execução, as alterações realizadas no banco de dados são registradas como o “SaveChanges()”, mas não são efetivadas até que o método “transaction.Commit()” seja chamado, confirmando todas as mudanças feitas até aquele momento. Caso ocorra algum erro durante o processo, é possível reverter todas as alterações executadas na transação utilizando o método “transaction.Rollback()”, garantindo que o banco de dados permaneça em um estado consistente.
Este exemplo mostra como garantir que ambas as operações (inserção de cliente e pedido) sejam tratadas como uma unidade. Se qualquer uma das operações falhar, as alterações são revertidas e o banco de dados permanece inalterado.
Transações implícitas no EF Core
Por padrão, o EF Core utiliza transações implícitas em chamadas para “SaveChanges()”. Isso significa que, a cada vez que você salva alterações no banco de dados, o EF Core automaticamente inicia, executa e finaliza uma transação. Isso é útil para operações simples, mas pode ser insuficiente quando você deseja realizar múltiplas operações em um único processo.
try
{
context.Clientes.Add(new Cliente { Nome = "Maria Silva" });
context.Pedidos.Add(new Pedido { ClienteId = 1, Total = 200.00m });
context.SaveChanges();
}
catch (Exception ex)
{
Console.WriteLine($"Erro ao salvar: {ex.Message}");
}
Neste exemplo, o EF Core cria automaticamente uma transação para realizar as operações de inserção do cliente e do pedido. Se ocorrer um erro, a transação é revertida.
Combinando EF Core com outros frameworks
Em alguns cenários, você pode querer combinar o EF Core com outros frameworks como o Dapper, que também oferece um acesso direto e performático ao banco de dados. O EF Core permite que você compartilhe a mesma transação entre essas diferentes abordagens.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Dapper;
using (var context = new AppDbContext())
{
using var transaction = context.Database.BeginTransaction();
var connection = context.Database.GetDbConnection();
try
{
// Operação com EF Core
context.Clientes.Add(new Cliente { Nome = "Pedro" });
context.SaveChanges();
// Operação com Dapper
var sql = "INSERT INTO Pedidos (ClienteId, Total) VALUES (@ClienteId, @Total)";
var parameters = new { ClienteId = 1, Total = 150.00m };
connection.Execute(sql, parameters, transaction: transaction.GetDbTransaction());
// Confirmar transação
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
Console.WriteLine($"Erro: {ex.Message}");
}
}
Usamos “GetDbConnection” para obter a conexão do banco de dados gerenciada pelo EF Core.
“GetDbTransaction” permite que a transação seja compartilhada entre o EF Core e o Dapper.
Isso é útil quando você precisa executar operações rápidas e de baixo nível com o Dapper enquanto mantém o controle completo da transação com o EF Core.
Nível de isolamento da transação
O nível de isolamento é um mecanismo que controla como uma transação interage com outras transações em execução simultânea, determinando a visibilidade dos dados alterados e o comportamento de bloqueios durante as operações. Sua utilidade está em balancear a consistência dos dados com a performance, evitando problemas como leituras incorretas ou conflitos entre transações concorrentes.
No Entity Framework Core, você pode especificar o nível de isolamento ao iniciar uma transação explícita da seguinte forma:
using (var transaction = context.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
{
try
{
//Sua transação.
}
catch
{
transaction.Rollback();
throw;
}
}
ReadCommitted (Padrão)
O nível apresentado acima é o “ReadCommitted”, que garante que uma transação só possa ler dados que já foram confirmados (comitados) por outras transações, evitando leituras “sujas” (dados que podem ser revertidos por outra transação).
Imagine um sistema de estoque onde uma transação verifica o saldo de um produto antes de reduzir a quantidade. Com “ReadCommitted”, você evita que a leitura capture alterações não confirmadas de outra transação, garantindo que o saldo seja consistente.
Serializable
Este é o nível mais restritivo, onde todas as transações são tratadas como se fossem executadas sequencialmente, evitando problemas como leituras fantasmas (leituras que incluem dados inseridos por outra transação durante a execução).
Em um sistema bancário, onde duas transações podem tentar transferir dinheiro da mesma conta, o “Serializable” impede que ambas leiam o mesmo saldo inicial e causem inconsistências.
using (var transaction = context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
{
//Sua transação.
}
ReadUncommitted
Este nível permite leituras de dados ainda não confirmados por outras transações, oferecendo maior performance, mas com risco de leituras “sujas”.
Em relatórios temporários, onde dados podem mudar frequentemente e a precisão absoluta não é prioritária, o “ReadUncommitted” pode ser usado para melhorar a performance.
using (var transaction = context.Database.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted))
{
//Sua transação.
}
Importância da Escolha
A escolha do nível de isolamento deve considerar o equilíbrio entre consistência e desempenho:
- Alta consistência necessária? Use “Serializable” .
- Performance é crucial? Use “ReadUncommitted”, mas com cuidado.
- Cenários intermediários? O padrão “ReadCommitted” geralmente é suficiente.
Cada nível de isolamento é uma ferramenta para gerenciar como transações simultâneas acessam os dados, protegendo a integridade e mantendo o desempenho dentro das necessidades do sistema.
Lidando com erros em transações
Em cenários mais complexos, o tratamento adequado de erros é essencial. A abordagem ideal é sempre garantir que, em caso de falha, a transação seja revertida para garantir a consistência dos dados.
Estratégias de tratamento de erros
Validação Antecipada: antes de iniciar a transação, valide os dados de entrada para garantir que estão no formato correto.
Logs: registre erros de forma detalhada para facilitar o diagnóstico.
Transações Curtas: execute apenas as operações necessárias dentro da transação. Isso reduz o tempo de bloqueio do banco de dados e evita deadlocks (quando dois ou mais processos ficam bloqueados, esperando uns pelos outros, sem possibilidade de progresso).
Boas práticas para gerenciar transações
Minimize a duração da transação: não mantenha transações abertas por longos períodos. Isso evita bloqueios prolongados e melhora a performance.
Evite operações desnecessárias: realize apenas operações que precisam de transações. Operações simples, como inserções de um único registro, não necessitam de transação explícita.
Escolha o nível de isolamento adequado: o EF Core oferece suporte para definir o nível de isolamento da transação, como citado acima. A escolha correta pode melhorar a performance e evitar problemas de bloqueio.
Acelere a sua carreira conosco!
Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui
Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui
Conclusão
As transações no Entity Framework Core são essenciais para manter a consistência e a integridade dos dados em operações críticas. Com suporte a transações explícitas e implícitas, o EF Core oferece flexibilidade para lidar com cenários variados, incluindo integrações com outros frameworks, como o Dapper. Para garantir um sistema eficiente e confiável, é fundamental adotar boas práticas. Agora é sua vez! Teste os exemplos, explore os recursos do EF Core e aplique esses conceitos em seus projetos para criar soluções robustas e eficientes.