Partilhar via


Usando transações

As transações permitem que várias operações de banco de dados sejam processadas de forma atômica. Se a transação for confirmada, todas as operações serão aplicadas com êxito ao banco de dados. Se a transação for revertida, nenhuma das operações será aplicada ao banco de dados.

Sugestão

Você pode visualizar a amostra do deste artigo no GitHub.

Comportamento de transação padrão

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 alguma das alterações falhar, a transação será revertida e nenhuma das alterações será aplicada ao banco de dados. Isso significa que é garantido que SaveChanges terá sucesso completo ou deixará o banco de dados sem modificações se ocorrer um erro.

Para a maioria dos aplicativos, esse comportamento padrão é suficiente. Você só deve controlar manualmente as transações se os requisitos do seu aplicativo considerarem necessário.

Controlo de transações

Você pode usar a API DbContext.Database para iniciar, confirmar e reverter transações. O exemplo a seguir mostra duas operações SaveChanges e uma consulta LINQ sendo executada em uma única transação:

using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();

try
{
    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    await context.SaveChangesAsync();

    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    await context.SaveChangesAsync();

    var blogs = await context.Blogs
        .OrderBy(b => b.Url)
        .ToListAsync();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
catch (Exception)
{
    // TODO: Handle failure
}

Embora todos os provedores de banco de dados relacional ofereçam suporte a transações, outros tipos de provedores podem lançar ou no-op quando as APIs de transação são chamadas.

Observação

Controlar manualmente as transações desta forma é incompatível com estratégias de execução de tentativa de repetição invocadas implicitamente. Consulte Resiliência de Conexão para obter mais informações.

Guardar pontos

Quando SaveChanges é invocado e uma transação já está em andamento no contexto, o EF cria automaticamente um ponto de salvaguarda antes de salvar quaisquer dados. Savepoints são pontos dentro de uma transação de banco de dados que podem ser retomados posteriormente, se ocorrer um erro ou por qualquer outro motivo. Se SaveChanges encontrar algum erro, ele automaticamente rola a transação de volta para o ponto de salvamento, deixando a transação no mesmo estado como se nunca tivesse sido iniciada. Isso permite que você possivelmente corrija problemas e tente salvar novamente, em particular quando simultaneidade otimista problemas ocorrem.

Advertência

Os Savepoints são incompatíveis com o MARS (Multiple Ative Result Sets) do SQL Server. Os Savepoints não serão criados pelo EF quando o MARS estiver ativado na conexão, mesmo que o MARS não esteja ativamente em uso. Se ocorrer um erro durante SaveChanges, a transação pode ser deixada em um estado desconhecido.

Também é possível gerenciar manualmente os savepoints, assim como acontece com as transações. O exemplo a seguir cria um savepoint dentro de uma transação e retrocede para ele em caso de falha.

using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();

try
{
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
    await context.SaveChangesAsync();

    await transaction.CreateSavepointAsync("BeforeMoreBlogs");

    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    await transaction.RollbackToSavepointAsync("BeforeMoreBlogs");

    // TODO: Handle failure, possibly retry inserting blogs
}

Transação entre contextos

Você também pode compartilhar uma transação em várias instâncias de contexto. Essa funcionalidade só está disponível ao usar um provedor de banco de dados relacional porque requer o uso de DbTransaction e DbConnection, que são específicos para bancos de dados relacionais.

Para partilhar uma transação, os contextos devem partilhar tanto um DbConnection como um DbTransaction.

Permitir que a conexão seja fornecida externamente

Compartilhar um DbConnection requer a capacidade de passar uma conexão para um contexto ao construí-lo.

A maneira mais fácil de permitir que DbConnection sejam fornecidos externamente é parar de usar o método DbContext.OnConfiguring para configurar o contexto e criar DbContextOptions externamente e passá-los para o construtor de contexto.

Sugestão

DbContextOptionsBuilder é a API que você usou no DbContext.OnConfiguring para configurar o contexto, agora você vai usá-la externamente para criar DbContextOptions.

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

Uma alternativa é continuar usando DbContext.OnConfiguring, mas aceitar um DbConnection que é salvo e depois usado em DbContext.OnConfiguring.

public class BloggingContext : DbContext
{
    private DbConnection _connection;

    public BloggingContext(DbConnection connection)
    {
      _connection = connection;
    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connection);
    }
}

Compartilhar conexão e transação

Agora você pode criar várias instâncias de contexto que compartilham a mesma conexão. Em seguida, use a API DbContext.Database.UseTransaction(DbTransaction) para inscrever ambos os contextos na mesma transação.

using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(connection)
    .Options;

using var context1 = new BloggingContext(options);
await using var transaction = await context1.Database.BeginTransactionAsync();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    await context1.SaveChangesAsync();

    using (var context2 = new BloggingContext(options))
    {
        await context2.Database.UseTransactionAsync(transaction.GetDbTransaction());

        var blogs = await context2.Blogs
            .OrderBy(b => b.Url)
            .ToListAsync();

        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        await context2.SaveChangesAsync();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
catch (Exception)
{
    // TODO: Handle failure
}

Usando DbTransactions externos (somente bancos de dados relacionais)

Se você estiver usando várias tecnologias de acesso a dados para acessar um banco de dados relacional, convém compartilhar uma transação entre operações executadas por essas tecnologias diferentes.

O exemplo a seguir mostra como executar uma operação ADO.NET SqlClient e uma operação Entity Framework Core na mesma transação.

using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
try
{
    // Run raw ADO.NET command in the transaction
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "DELETE FROM dbo.Blogs";
    command.ExecuteNonQuery();

    // Run an EF Core command in the transaction
    var options = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlServer(connection)
        .Options;

    using (var context = new BloggingContext(options))
    {
        await context.Database.UseTransactionAsync(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context.SaveChangesAsync();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
catch (Exception)
{
    // TODO: Handle failure
}

Usando a System.Transactions

É possível usar transações ambientais se você precisar coordenar um escopo maior.

using (var scope = new TransactionScope(
           TransactionScopeOption.Required,
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM dbo.Blogs";
        await command.ExecuteNonQueryAsync();

        // Run an EF Core command in the transaction
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            await context.SaveChangesAsync();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        scope.Complete();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

Também é possível inscrever-se numa transação explícita.

using (var transaction = new CommittableTransaction(
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    var connection = new SqlConnection(connectionString);

    try
    {
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            await context.Database.OpenConnectionAsync();
            context.Database.EnlistTransaction(transaction);

            // Run raw ADO.NET command in the transaction
            var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM dbo.Blogs";
            await command.ExecuteNonQueryAsync();

            // Run an EF Core command in the transaction
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            await context.SaveChangesAsync();
            await context.Database.CloseConnectionAsync();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        transaction.Commit();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

Observação

Se você estiver usando APIs assíncronas, certifique-se de especificar TransactionScopeAsyncFlowOption.Enabled no construtor TransactionScope para garantir que a transação ambiente flua entre chamadas assíncronas.

Para obter mais informações sobre transações TransactionScope e ambientais, consulte esta documentação.

Limitações de System.Transactions

  1. O EF Core depende de provedores de banco de dados para implementar suporte para System.Transactions. Se um provedor não implementar suporte para System.Transactions, é possível que as chamadas para essas APIs sejam completamente ignoradas. SqlClient suporta isso.

    Importante

    É recomendável testar se a API se comporta corretamente com seu provedor antes de confiar nela para gerenciar transações. Você é incentivado a entrar em contato com o mantenedor do provedor de banco de dados se ele não funcionar.

  2. O suporte a transações distribuídas em System.Transactions foi adicionado ao .NET 7.0 apenas para Windows. Qualquer tentativa de usar transações distribuídas em versões mais antigas do .NET ou em plataformas que não sejam do Windows falhará.

  3. O TransactionScope não suporta confirmação/reversão assíncrona; Isso significa que descartá-lo de forma síncrona bloqueia o thread de execução até que a operação seja concluída.