DEV Community

Rafael
Rafael

Posted on

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.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            "Data Source=localhost,1433;Initial Catalog=Timesheet;User Id=sa;Password=Password1")
        .LogTo(message => Debug.WriteLine(message));
}
Enter fullscreen mode Exit fullscreen mode

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 se 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.

var employee = new Employee
{
    Name = "John Doe",
    Entries = new List<TimeEntry>
    {
        new TimeEntry
        {
            Start = TimeSpan.FromHours(8),
            End = TimeSpan.FromHours(12)
        }
    }
};
context.Add(employee);

context.SaveChanges();
Enter fullscreen mode Exit fullscreen mode

É 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.

dbug: CoreEventId.SaveChangesStarting[10004] (Microsoft.EntityFrameworkCore.Update) 
      SaveChanges starting for 'TestContext'.
dbug: CoreEventId.DetectChangesStarting[10800] (Microsoft.EntityFrameworkCore.ChangeTracking) 
      DetectChanges starting for 'TestContext'.
dbug: CoreEventId.DetectChangesCompleted[10801] (Microsoft.EntityFrameworkCore.ChangeTracking) 
      DetectChanges completed for 'TestContext'.
dbug: RelationalEventId.ConnectionOpening[20000] (Microsoft.EntityFrameworkCore.Database.Connection) 
      Opening connection to database 'Timesheet' on server 'localhost,1433'.
dbug: RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection) 
      Opened connection to database 'Timesheet' on server 'localhost,1433'.
Enter fullscreen mode Exit fullscreen mode

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.

dbug: RelationalEventId.TransactionStarting[20209] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Beginning transaction with isolation level 'Unspecified'.
dbug: RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Began transaction with isolation level 'ReadCommitted'.
Enter fullscreen mode Exit fullscreen mode

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.

dbug: RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0='John Doe' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Employees] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Employees]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

dbug: RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p1='3' (Nullable = true), @p2='12:00:00', @p3='08:00:00'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [TimeEntries] ([EmployeeId], [End], [Start])
      VALUES (@p1, @p2, @p3);
      SELECT [Id]
      FROM [TimeEntries]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
Enter fullscreen mode Exit fullscreen mode

Por último, é feito o commit da transação.

dbug: RelationalEventId.TransactionCommitting[20210] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committing transaction.
dbug: RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committed transaction.
Enter fullscreen mode Exit fullscreen mode

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.

var employee = new Employee
{
      Name = "John Doe",
      Entries = new List<TimeEntry>
      {
            new TimeEntry
            {
                  Start = TimeSpan.FromHours(8),
                  End = TimeSpan.FromHours(12)
            }
      }
};
context.Add(employee);

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

context.SaveChanges();

transaction.Commit();
Enter fullscreen mode Exit fullscreen mode

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.

dbug: RelationalEventId.TransactionStarting[20209] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Beginning transaction with isolation level 'Unspecified'.
dbug: RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Began transaction with isolation level 'ReadCommitted'.
Enter fullscreen mode Exit fullscreen mode

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.

dbug: CoreEventId.SaveChangesStarting[10004] (Microsoft.EntityFrameworkCore.Update) 
      SaveChanges starting for 'TestContext'.

dbug: RelationalEventId.CreatingTransactionSavepoint[20212] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Creating transaction savepoint.
dbug: RelationalEventId.CreatedTransactionSavepoint[20213] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Created transaction savepoint.
Enter fullscreen mode Exit fullscreen mode

Em seguida se realiza a execução dos comandos da mesma forma do primero teste.

dbug: RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0='John Doe' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Employees] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Employees]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

dbug: RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p1='5' (Nullable = true), @p2='12:00:00', @p3='08:00:00'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [TimeEntries] ([EmployeeId], [End], [Start])
      VALUES (@p1, @p2, @p3);
      SELECT [Id]
      FROM [TimeEntries]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
Enter fullscreen mode Exit fullscreen mode

Por fim, ao executar o método transaction.Commit(), as alterações são confirmadas.

dbug: RelationalEventId.TransactionCommitting[20210] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committing transaction.
dbug: RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committed transaction.
Enter fullscreen mode Exit fullscreen mode

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.

diferença entre transações aninhadas e savepoints

Savepoints no EF Core

O EF Core também permite a criação manual de savepoints usando o método CreateSavepoint.

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

context.Add(new Employee { Name = "John Doe" });
context.SaveChanges();

transaction.CreateSavepoint("A");

context.Add(new Employee { Name = "Jane Doe" });
context.SaveChanges();

transaction.Commit();
Enter fullscreen mode Exit fullscreen mode

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

  1. Simple Logging - https://docs.microsoft.com/pt-br/ef/core/logging-events-diagnostics/simple-logging
  2. Usando transações - https://docs.microsoft.com/pt-br/ef/core/saving/transactions
  3. 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
  4. Nesting transactions and SAVE TRANSACTION command - https://dba-presents.com/index.php/databases/sql-server/43-nesting-transactions-and-save-transaction-command
  5. SAVE TRANSACTION - https://docs.microsoft.com/pt-br/sql/t-sql/language-elements/save-transaction-transact-sql?view=sql-server-ver15
  6. https://github.com/dotnet/efcore/blob/release/5.0/src/EFCore.Relational/Update/Internal/BatchExecutor.cs#L89
  7. https://github.com/dotnet/efcore/blob/release/2.1/src/EFCore.Relational/Update/Internal/BatchExecutor.cs#L70

Top comments (0)