Trabalhar com Transações

Observação

Apenas a partir do EF6 - As funcionalidades, APIs, etc. discutidas nesta página foram introduzidas no Entity Framework 6. Se estiver a usar uma versão anterior, parte ou toda a informação não se aplica.

Este documento irá descrever o uso das transações no EF6, incluindo as melhorias que adicionámos desde o EF5 para facilitar o trabalho com transações.

O que o EF faz por defeito

Em todas as versões do Entity Framework, sempre que executar SaveChanges() para inserir, atualizar ou eliminar na base de dados, o framework irá envolver essa operação numa transação. Esta transação dura apenas o tempo suficiente para executar a operação e depois completa-se. Quando executa outra operação deste tipo, inicia-se uma nova transação.

A partir do EF6 Database.ExecuteSqlCommand(), por defeito, envolverá o comando numa transação, se uma ainda não estiver presente. Existem sobrecargas deste método que permitem substituir este comportamento, se desejar. Também no EF6, a execução de procedimentos armazenados incluídos no modelo através de APIs como ObjectContext.ExecuteFunction() faz o mesmo (exceto que o comportamento padrão não pode ser sobreposto neste momento).

Em qualquer dos casos, o nível de isolamento da transação é o nível de isolamento que o fornecedor de base de dados considera a sua configuração predefinida. Por padrão, por exemplo, no SQL Server, isto é READ COMMITTED.

O Entity Framework não envolve consultas numa transação.

Esta funcionalidade padrão é adequada para muitos utilizadores e, se assim for, não há necessidade de fazer nada diferente no EF6; Escreve o código como sempre fizeste.

No entanto, alguns utilizadores necessitam de maior controlo sobre as suas transações – isto é abordado nas secções seguintes.

Como funcionam as APIs

Antes do EF6, o Entity Framework insistia em abrir a própria ligação à base de dados (lançava uma exceção se lhe fosse passada uma ligação já aberta). Como uma transação só pode ser iniciada numa ligação aberta, isto significava que a única forma de um utilizador envolver várias operações numa única transação era usar um TransactionScope ou usar a propriedade ObjectContext.Connection e começar a chamar Open() e BeginTransaction() diretamente no objeto EntityConnection devolvido. Além disso, chamadas de API que contactavam a base de dados falhavam se tivesse iniciado uma transação na ligação subjacente à base de dados por conta própria.

Observação

A limitação de aceitar apenas ligações fechadas foi removida no Entity Framework 6. Para mais detalhes, consulte Gestão de Ligações.

A partir do EF6, o quadro agora fornece:

  1. Database.BeginTransaction(): Um método mais fácil para um utilizador iniciar e concluir as transações por si próprio dentro de um DbContext existente – permitindo que várias operações sejam combinadas na mesma transação e, assim, todas comprometidas ou todas revertidas como uma só. Também permite ao utilizador especificar mais facilmente o nível de isolamento da transação.
  2. Database.UseTransaction() : que permite ao DbContext usar uma transação iniciada fora do Entity Framework.

Combinar várias operações numa única transação dentro do mesmo contexto

O Database.BeginTransaction() tem dois overrides – um que aceita um IsolationLevel explícito e outro que não aceita argumentos e utiliza o IsolationLevel padrão do fornecedor da base de dados subjacente. Ambos as substituições retornam um objeto DbContextTransaction que fornece os métodos Commit() e Rollback(), que executam o commit e o rollback na transação subjacente ao armazenamento.

A DbContextTransaction destina-se a ser descartada depois de ter sido comprometida ou revertida. Uma forma fácil de conseguir isto é através da sintaxe using(...) {...}, que automaticamente chamará Dispose() quando o bloco using for concluído:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }
            }
        }
    }
}

Observação

Iniciar uma transação requer que a conexão ao armazenamento subjacente esteja aberta. Chamar Database.BeginTransaction() abrirá a ligação se ainda não estiver aberta. Se o DbContextTransaction abriu a ligação, fecha-a quando o Dispose() for chamado.

Passar uma transação existente para o contexto

Por vezes, gostaria de uma transação ainda mais ampla em âmbito e que inclua operações na mesma base de dados, mas completamente fora do EF. Para conseguir isto, deve abrir a ligação e iniciar a transação por si mesmo e depois dizer ao EF a) para usar a ligação à base de dados já aberta, e b) para usar a transação existente nessa ligação.

Para isso, deve definir e usar um construtor na sua classe de contexto que herda de um dos construtores DbContext que recebe i) um parâmetro de conexão existente e ii) o booleano contextOwnsConnection.

Observação

A flag contextOwnsConnection deve ser definida como false quando chamada neste cenário. Isto é importante porque informa o Entity Framework de que não deve fechar a ligação quando terminar (por exemplo, ver a linha 4 abaixo):

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

Além disso, deve iniciar a transação você próprio (incluindo o IsolationLevel se quiser evitar a definição padrão) e informar o Entity Framework de que já existe uma transação já iniciada na ligação (ver linha 33 abaixo).

Assim, fica livre para executar operações na base de dados, quer diretamente no próprio SqlConnection, quer no DbContext. Todas essas operações são executadas numa única transação. Assumes a responsabilidade de confirmar ou reverter a transação e de chamar Dispose() nela, bem como de fechar e descartar a ligação à base de dados. Por exemplo:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                       context.SaveChanges();
                    }

                    sqlTxn.Commit();
                }
            }
        }
    }
}

A liquidar a transação

Pode passar o null para o Database.UseTransaction() para limpar o conhecimento do Entity Framework sobre a transação atual. O Entity Framework não faz commit nem reverte a transação existente quando isto é feito, por isso use com cuidado e só se tiver a certeza de que é isso que pretende fazer.

Erros no UseTransaction

Verá uma exceção do Database.UseTransaction() se passar uma transação quando:

  • O Entity Framework já tem uma transação existente
  • O Entity Framework já opera dentro de um TransactionScope
  • O objeto de ligação na transação passada é nulo. Ou seja, a transação não está associada a uma ligação – normalmente isto é um sinal de que essa transação já foi concluída
  • O objeto de ligação na transação passada não corresponde à ligação do Entity Framework.

Utilização de transações com outras funcionalidades

Esta secção detalha como as transações acima interagem com:

  • Resiliência da conexão
  • Métodos assíncronos
  • Transações TransactionScope

Resiliência da conexão

A nova funcionalidade de Resiliência de Ligação não funciona com transações iniciadas pelo utilizador. Para mais detalhes, consulte Repetição de Estratégias de Execução.

Programação assíncrona

A abordagem descrita nas secções anteriores não necessita de mais opções ou definições para funcionar com os métodos de consulta e gravação assíncronos. Mas tenha em atenção que, dependendo do que faz dentro dos métodos assíncronos, isto pode resultar em transações de longa duração – o que pode causar interbloqueios ou bloqueios, o que é muito prejudicial para o desempenho geral da aplicação.

Transações TransactionScope

Antes do EF6, a forma recomendada de fornecer transações de maior alcance era usar um objeto TransactionScope:

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

O SqlConnection e o Entity Framework usariam ambos a transação ambiente TransactionScope e, por isso, seriam comprometidos em conjunto.

A partir do .NET 4.5.1, o TransactionScope foi atualizado para também funcionar com métodos assíncronos através da utilização da enumeração TransactionScopeAsyncFlowOption :

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

Ainda existem algumas limitações à abordagem TransactionScope:

  • Requer .NET 4.5.1 ou superior para funcionar com métodos assíncronos.
  • Não pode ser usado em cenários cloud a menos que tenha a certeza de que tem uma e apenas uma ligação (cenários cloud não suportam transações distribuídas).
  • Não pode ser combinado com a abordagem Database.UseTransaction() das secções anteriores.
  • Irá gerar exceções se emitir algum DDL e não tiver ativado transações distribuídas pelo serviço MSDTC.

Vantagens da abordagem TransactionScope:

  • Atualizará automaticamente uma transação local para uma transação distribuída se fizer mais do que uma ligação a uma determinada base de dados ou combinar uma ligação a uma base de dados com uma ligação a uma base de dados diferente dentro da mesma transação (nota: deve ter o serviço MSDTC configurado para permitir transações distribuídas para que isto funcione).
  • Facilidade de programação. Se preferir que a transação seja ambiente e tratada implicitamente em segundo plano, em vez de explicitamente sob o seu controlo, então a abordagem TransactionScope pode ser mais adequada para si.

Em resumo, com as novas APIs Database.BeginTransaction() e Database.UseTransaction() acima, a abordagem TransactionScope já não é necessária para a maioria dos utilizadores. Se continuar a usar o TransactionScope, esteja ciente das limitações acima. Recomendamos usar a abordagem descrita nas secções anteriores, sempre que possível.