Trabalhando com Transações
Observação
EF6 em diante apenas: os recursos, as APIs etc. discutidos nessa página foram introduzidos no Entity Framework 6. Se você estiver usando uma versão anterior, algumas ou todas as informações não se aplicarão.
Este documento descreverá o uso de transações no EF6 incluindo os aprimoramentos que adicionamos a partir do EF5 para facilitar o trabalho com transações.
O que o EF faz por padrão
Em todas as versões do Entity Framework, sempre que você executar SaveChanges() para inserir, atualizar ou excluir no banco de dados, o Framework encapsulará essa operação em uma transação. Essa transação dura apenas o tempo suficiente para executar a operação e, em seguida, concluir. Quando você executar outra operação desse tipo, uma nova transação iniciará.
A partir do EF6, o Database.ExecuteSqlCommand() por padrão encapsulará o comando em uma transação, se uma ainda não estiver presente. Se você quiser, há sobrecargas desse método que permitem substituir esse comportamento. Também na execução de procedimentos armazenados do EF6 incluídos no modelo por meio de APIs como, por exemplo, ObjectContext.ExecuteFunction() faz o mesmo (exceto que o comportamento padrão não poderá ser substituído no momento).
Em ambos os casos, o nível de isolamento da transação será qualquer nível de isolamento que o provedor de banco de dados considerar sua configuração padrão. Por exemplo, no SQL Server, será READ COMMITTED por padrão.
O Entity Framework não encapsula consultas em uma transação.
Essa funcionalidade padrão é adequada para muitos usuários e, nesse caso, não é necessário realizar outras ações no EF6, basta gravar o código normalmente.
No entanto, alguns usuários exigem um maior controle sobre as transações, e esse tópico será abordado nas seções a seguir.
Como as APIs funcionam
Antes do EF6, o próprio Entity Framework continuava abrindo a conexão de banco de dados (isto é, gerava uma exceção se uma conexão já aberta fosse aprovada). Como só é possível iniciar uma transação em uma conexão aberta, significava que a única maneira de um usuário encapsular várias operações em uma transação era usar TransactionScope ou usar a propriedadeObjectContext.Connection e iniciar chamando Open() e BeginTransaction() diretamente no objeto EntityConnection retornado. Além disso, as chamadas à API que contatavam o banco de dados falhariam se você iniciasse uma transação na conexão de banco de dados subjacente por conta própria.
Observação
A limitação de aceitar somente as conexões fechadas foi removida no Entity Framework 6. Para obter mais detalhes, consulte Gerenciamento de conexão.
A partir do EF6, o Framework passa a fornecer:
- Database.BeginTransaction(): um método mais fácil para o próprio usuário iniciar e concluir as transações em um DbContext existente, permitindo que várias operações sejam combinadas na mesma transação e, portanto, todas confirmadas ou revertidas como uma. Também permite que o usuário especifique de maneira mais fácil o nível de isolamento para a transação.
- Database.UseTransaction(): permite que o DbContext use uma transação que foi iniciada fora do Entity Framework.
Combinando várias operações em uma transação de um mesmo contexto
Database.BeginTransaction() tem duas substituições: uma que aceita um IsolationLevel explícito e outra que não aceita argumentos e usa o IsolationLevel padrão do provedor de banco de dados subjacente. Ambas as substituições retornam um objeto DbContextTransaction que fornece métodos Commit() e Rollback() que executam confirmação e reversão na transação do armazenamento subjacente.
O DbContextTransaction deverá ser descartado depois de confirmado ou revertido. Uma maneira fácil de fazer isso é a sintaxe using(…) {…} que chamará Dispose() automaticamente quando o bloco de uso concluir:
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 do armazenamento subjacente esteja aberta. Portanto, chamar Database.BeginTransaction() abrirá a conexão se ainda não estiver aberta. Se DbContextTransaction abriu a conexão, fechará quando Dispose() for chamado.
Aprovando uma transação existente para o contexto
Às vezes, o ideal é optar por uma transação com escopo ainda mais amplo e que inclua operações no mesmo banco de dados, mas completamente fora do EF. Para conseguir isso, abra a conexão e inicie a transação você mesmo e, em seguida, informe ao EF a) para usar a conexão de banco de dados já aberta e b) para usar a transação existente nessa conexão.
Para fazer isso, defina e use um construtor na classe de contexto que herda de um dos construtores DbContext que usa i) um parâmetro de conexão existente e ii) o booliano contextOwnsConnection.
Observação
O sinalizador contextOwnsConnection deverá ser definido como falso quando chamado nesse cenário. Isso é importante porque informa ao Entity Framework que ele não deverá fechar a conexão quando terminar (por exemplo, consulte a linha 4 abaixo):
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
}
}
Além disso, você mesmo deverá iniciar a transação (incluindo o IsolationLevel, se quiser evitar a configuração padrão) e permitir que o Entity Framework reconheça que há uma transação existente já iniciada na conexão (consulte a linha 33 abaixo).
Em seguida, você poderá executar operações de banco de dados diretamente no próprio SqlConnection ou no DbContext. Todas essas operações serão executadas em uma transação. Você assume a responsabilidade de confirmar ou reverter a transação e de chamar Dispose() nela, bem como encerrar e descartar a conexão de banco 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();
}
}
}
}
}
Limpar a transação
É possível aprovar nulo para Database.UseTransaction() e limpar o conhecimento do Entity Framework sobre a transação atual. O Entity Framework não confirmará nem reverterá a transação existente quando você fizer isso, portanto, use com cuidado e somente se você tiver certeza que deseja fazer isso.
Erros em UseTransaction
Você verá uma exceção de Database.UseTransaction() se aprovar uma transação quando:
- O Entity Framework já tiver uma transação existente
- O Entity Framework já estiver operando em uma TransactionScope
- O objeto de conexão na transação aprovada for nulo. Ou seja, a transação não está associada a uma conexão, normalmente isso é um sinal de que essa transação já foi concluída
- O objeto de conexão na transação aprovada não corresponde à conexão do Entity Framework.
Usando transações com outros recursos
Esta seção detalha como as transações acima interagem com:
- Resiliência de conexão
- Métodos assíncronos
- Transações TransactionScope
Resiliência de conexão
O novo recurso Resiliência de Conexão não funciona com transações iniciadas pelo usuário. Para obter mais detalhes, consulte Tentar novamente as estratégias de execução.
Programação assíncrona
A abordagem descrita nas seções anteriores não precisa de opções ou configurações adicionais para funcionar com os métodos de consulta e salvamento assíncronos. Mas lembre-se de que, dependendo do que você fizer nos métodos assíncronos poderá resultar em transações de execução longa, que poderão causar deadlocks ou bloqueios, e prejudicar o desempenho do aplicativo global.
Transações TransactionScope
Antes do EF6, a maneira recomendada para fornecer transações de escopo maior 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 usavam a transação TransactionScope ambiente e, desse modo, eram confirmados juntos.
A partir do .NET 4.5.1, a TransactionScope foi atualizada para funcionar também com métodos assíncronos por meio do uso 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 há algumas limitações na abordagem TransactionScope:
- Exige .NET 4.5.1 ou superior para funcionar com métodos assíncronos.
- Não pode ser usada em cenários de nuvem, a menos que você tenha certeza de ter uma, e apenas uma, conexão (cenários de nuvem não dão suporte a transações distribuídas).
- Não pode ser combinada com a abordagem Database.UseTransaction() das seções anteriores.
- Irá gerar exceções se você emitir qualquer DDL e não tiver habilitado as transações distribuídas por meio do serviço MSDTC.
Vantagens da abordagem TransactionScope:
- Uma transação local será atualizada automaticamente para uma transação distribuída se você fizer mais de uma conexão com um determinado banco de dados, ou, combinar uma conexão para um banco de dados com uma conexão para um banco de dados diferente na mesma transação (observação: para que isso funcione, é necessário ter o serviço MSDTC configurado para permitir as transações distribuídas).
- Facilidade de codificação. Se preferir que a transação seja ambiente e tratada implicitamente em segundo plano, em vez de explicitamente sob seu controle, a abordagem TransactionScope poderá ser mais adequada para você.
Em resumo, com as novas APIs Database.BeginTransaction() e Database.UseTransaction() acima, a abordagem TransactionScope não será mais necessária para a maioria dos usuários. Se você continuar usando TransactionScope, lembre-se das limitações acima. É recomendável usar a abordagem descrita nas seções anteriores, sempre que possível.