SaveChanges e controle transacional no EF Core
O método SaveChanges
é responsável por salvar todas as alterações no banco de dados quando se está trabalhando com o Entity Framework Core. Ao executar esse método, por padrão, estamos executando as operações dentro de uma transação. A documentação diz:
Por padrão, se o provedor de banco de dados oferecer suporte a transações, todas as alterações em uma única chamada para SaveChanges serão aplicadas em uma transação. Se qualquer uma das alterações falhar, a transação é revertida e nenhuma das alterações será aplicada ao banco de dados.
Será tratado aqui o funcionamento do método SaveChanges
analisando os logs do EF Core e as consultas geradas. O foco da análise será o controle da transação. A versão da biblioteca nos testes é a 5 e o banco de dados utilizado na análise é o SQL Server da Microsoft. Os resultados encontrados podem ser diferentes em outras versões e/ou utilizando outro bancos de dados. O código criado para os testes pode ser visto aqui.
Configurando o DbContext
O contexto do banco de dados deve ser configurado para obter as informações necessárias para o entendimento do EF Core.
O método LogTo
, usado na configuração, é a nova maneira de acessar os logs do EF Core. A funcionalidade Simple Logging foi apresentada na versão 5 do ORM e permite direcionar as entradas de log para o tipo de saída desejada. No exemplo, os dados são direcionados para janela de Debug do Visual Studio usando o método Debug.Writeline
. Nas versões antigas é possível obter os logs usando o método UseLoggerFactory
.
Adicionando dados com o SaveChanges
Para verificar o funcionamento do SaveChanges
foi escrito um teste simples adicionando dois objetos no banco de dados. No banco serão armazenadas duas linhas, uma em cada tabela.
É possível ver no log gerado que os primeiros passos do EF Core na execução do método SaveChanges
é a verificação das mudanças das entidades do contexto. Logo em seguida, a conexão com o banco é aberta.
As entradas de log seguintes são relacionadas à transação. No primeiro momento, a transação é iniciada com o nível de isolamento não especificado. A próxima mensagem mostra que a transação foi inicializada com o nível ReadCommitted
, que é o padrão do SQL Server.
Em seguida são executados dois comandos INSERT
, um para cada entidade. Como o Id
das entidades são gerados pelo banco de dados, pode-se ver que, para cada INSERT
, também foi realizado um SELECT
para que o EF Core obtenha o Id da entidade inserida. Alguns logs de criação dos comandos e detecção de alterações de chave estrangeira foram omitidos.
Por último, é feito o commit
da transação.
Controlando a transação
Para a maioria dos casos, o uso do SaveChanges
é o suficiente para garantir a consistência dos dados. Porém, quando necessário, é possível controlar as transações manualmente.
O segundo teste utiliza o mesmo exemplo do primeiro. Dessa vez o método SaveChanges
está sendo executado dentro de uma transação iniciada com método BeginTransaction
.
As primeiras entradas do log são de abertura da transação. Como o primeiro teste, a transação é iniciada com o nível de isolamento não especificado e criada com o nível ReadCommited
.
Ao executar o método SaveChanges
, o EF Core cria um savepoint da transação ao invés de abrir uma transação aninhada. Os logs de detecção de alterações nas entidades foram omitidos.
Em seguida se realiza a execução dos comandos da mesma forma do primeiro teste.
Por fim, ao executar o método transaction.Commit()
, as alterações são confirmadas.
Diferença entre transações aninhadas e savepoints no SQL Server
No SQL Server, o uso de transações aninhadas tem um comportamento que pode confundir os desenvolvedores. O ROLLBACK
reverte todas as transações abertas da sessão. Não importa se existam transações aninhadas, o ROLLBACK
vai desfazer todas as operações realizadas desde o primeiro BEGIN TRANSACTION
.
Já o SAVE TRANSACTION
coloca uma tag no ponto desejado da transação de forma a ser possível executar o ROLLBACK
e reverter as mudanças até esse ponto. É possível criar vários savepoints em uma mesma transação. É importante lembrar que o SAVE TRANSACTION
não abre uma nova transação.
Savepoints no EF Core
O EF Core também permite a criação manual de savepoints usando o método CreateSavepoint
.
Versões anteriores
A funcionalidade de savepoints foi incluída no EF Core 5. Nas versões 3.1 e 2.1 o EF Core, além de não permitir a criação de savepoints de forma manual, também não executa um SAVE TRANSACTION
durante a execução do método SaveChanges
. Ou seja, caso já exista uma transação aberta, como no segundo teste, o SaveChanges
apenas executa os comandos de alteração da base de dados.
Conclusão
O método SaveChanges
facilita a vida do desenvolvedor ao executar os comandos de alteração no banco de dados dentro de uma transação. Para os outros casos, em que se faz a abertura de uma transação manualmente, o SaveChanges
apenas cria um savepoint dentro da transação.
Referências
- Simple Logging — https://docs.microsoft.com/pt-br/ef/core/logging-events-diagnostics/simple-logging
- Usando transações — https://docs.microsoft.com/pt-br/ef/core/saving/transactions
- Guia de Controle de Versão de Linha e Bloqueio de Transações — https://docs.microsoft.com/pt-br/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver15
- Nesting transactions and SAVE TRANSACTION command — https://dba-presents.com/index.php/databases/sql-server/43-nesting-transactions-and-save-transaction-command
- SAVE TRANSACTION — https://docs.microsoft.com/pt-br/sql/t-sql/language-elements/save-transaction-transact-sql?view=sql-server-ver15
- https://github.com/dotnet/efcore/blob/release/5.0/src/EFCore.Relational/Update/Internal/BatchExecutor.cs#L89
- https://github.com/dotnet/efcore/blob/release/2.1/src/EFCore.Relational/Update/Internal/BatchExecutor.cs#L70