Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
EF6 и более поздние версии — функции, API и т. д., рассмотренные на этой странице, были представлены в Entity Framework 6. Если вы используете более раннюю версию, некоторые или все сведения не применяются.
В этом документе описывается использование транзакций в EF6, включая усовершенствования, которые мы добавили с момента EF5, чтобы облегчить работу с транзакциями.
Действия EF по умолчанию
Во всех версиях Entity Framework всякий раз, когда вы выполняете SaveChanges() для вставки, обновления или удаления в базе данных, платформа будет упаковывать эту операцию в транзакцию. Эта транзакция длится достаточно долго, чтобы выполнить операцию, а затем завершится. При выполнении другой такой операции запускается новая транзакция.
Начиная с EF6 Database.ExecuteSqlCommand() по умолчанию будет упаковывать команду в транзакцию, если она еще не присутствует. Существуют перегрузки этого метода, которые позволяют переопределять это поведение, если вы того пожелаете. Кроме того, в EF6 выполнение хранимых процедур, включенных в модель с помощью API, таких как ObjectContext.ExecuteFunction(), выполняет то же самое (за исключением того, что поведение по умолчанию не может быть переопределено в данный момент).
В любом случае уровень изоляции транзакции является любым уровнем изоляции, который поставщик базы данных считает его параметром по умолчанию. Например, в SQL Server это READ COMMITTED.
Entity Framework не упаковывает запросы в транзакцию.
Эта функция по умолчанию подходит для многих пользователей, и если поэтому нет необходимости делать что-либо другое в EF6; просто напишите код, как всегда.
Однако для некоторых пользователей требуется более широкий контроль над их транзакциями— это описано в следующих разделах.
Как работают API
До EF6 Entity Framework настаивал на открытии соединения к базе данных (EF выбрасывал исключение, если ему передавалось соединение, которое уже было открыто). Так как транзакция может быть запущена только в открытом подключении, это означало, что единственный способ, которым пользователь мог упаковать несколько операций в одну транзакцию, был либо использовать TransactionScope , либо использовать свойство ObjectContext.Connection и начать вызывать Open() и BeginTransaction() непосредственно в возвращаемом объекте EntityConnection . Кроме того, вызовы API, которые связались с базой данных, завершились сбоем, если вы запустили транзакцию в базовом подключении к базе данных самостоятельно.
Замечание
Ограничение только принятия закрытых подключений было удалено в Entity Framework 6. Дополнительные сведения см. в разделе "Управление подключениями".
Начиная с EF6 платформа теперь предоставляет следующие возможности:
- Database.BeginTransaction() — более простой метод для пользователя начать и завершить транзакции в существующем DbContext, который позволяет объединить несколько операций в одной транзакции и, следовательно, либо все фиксируются, либо все откатываются одним набором. Он также позволяет пользователю проще указать уровень изоляции для транзакции.
- Database.UseTransaction() — позволяет DbContext использовать транзакцию, которая была запущена за пределами Entity Framework.
Объединение нескольких операций в одну транзакцию в одном контексте
Database.BeginTransaction() имеет два переопределения — один из которых принимает явный идентификатор IsolationLevel и тот, который не принимает аргументы и использует изоляцию по умолчанию из базового поставщика базы данных. Оба переопределения возвращают объект DbContextTransaction , предоставляющий методы Commit() и Rollback(), которые выполняют фиксацию и откат для базовой транзакции хранилища.
DbContextTransaction предназначен для завершения после того, как он был зафиксирован или отменён. Одним из простых способов этого является синтаксис using(...) {...} , который автоматически вызывает Dispose() при завершении блока использования:
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();
}
}
}
}
}
Замечание
Для запуска транзакции требуется, чтобы базовое подключение хранилища было открыто. Поэтому вызов Database.BeginTransaction() откроет подключение, если оно еще не открыто. Если DbContextTransaction открыло соединение, оно закроет его при вызове Dispose().
Передача существующей транзакции в контекст
Иногда требуется транзакция, которая обширнее по охвату и вмещает операции с той же базой данных, но полностью вне EF. Для этого необходимо открыть подключение и запустить транзакцию самостоятельно, после чего сообщить EF: a) использовать уже открытое подключение к базе данных и б) использовать существующую транзакцию в этом соединении.
Для этого необходимо определить и использовать конструктор в классе контекста, который наследует от одного из конструкторов DbContext, которые принимают i) существующий параметр подключения и ii) логическое значение contextOwnsConnection.
Замечание
Флаг contextOwnsConnection должен иметь значение false при вызове в этом сценарии. Это важно, так как оно сообщает Entity Framework, что оно не должно закрывать подключение при его завершении (например, см. строку 4 ниже):
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
}
}
Кроме того, необходимо самостоятельно запустить транзакцию (включая IsolationLevel, если вы хотите избежать параметра по умолчанию) и сообщите Entity Framework, что в подключении уже запущена существующая транзакция (см. строку 33 ниже).
Затем вы можете выполнять операции базы данных непосредственно в самом sqlConnection или в DbContext. Все такие операции выполняются в рамках одной транзакции. Вы несете ответственность за фиксацию или откат транзакции и вызов Dispose(), а также за закрытие и удаление подключения к базе данных. Рассмотрим пример.
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();
}
}
}
}
}
Очистка транзакции
Вы можете передать значение NULL в Database.UseTransaction() для очистки знаний Entity Framework о текущей транзакции. Entity Framework не будет ни фиксировать, ни откатывать существующую транзакцию при выполнении этого, поэтому используйте это с осторожностью и только если уверены, что действительно хотите так поступить.
Ошибки в UseTransaction
При использовании Database.UseTransaction() вы увидите исключение, если передаете транзакцию, когда:
- Entity Framework уже имеет существующую транзакцию
- Entity Framework уже работает в TransactionScope
- Объект подключения в переданной транзакции имеет значение NULL. То есть транзакция не связана с подключением — обычно это признак того, что транзакция уже завершена.
- Объект подключения в транзакции, переданный, не соответствует соединению Entity Framework.
Использование транзакций с другими функциями
В этом разделе описано, как взаимодействуют указанные выше транзакции:
- Устойчивость подключения
- Асинхронные методы
- Транзакции TransactionScope
Устойчивость подключения
Новая функция устойчивости подключений не работает с транзакциями, инициированными пользователем. Дополнительные сведения см. в разделе "Повторные попытки выполнения стратегий".
Асинхронное программирование
Подход, описанный в предыдущих разделах, не требует дополнительных параметров или параметров для работы с асинхронным запросом и сохранением методов. Но помните, что в зависимости от того, что вы делаете в асинхронных методах, это может привести к длительным транзакциям, которые могут спровоцировать взаимоблокировки или блокировки, негативно влияющие на производительность всего приложения.
Транзакции TransactionScope
До EF6 рекомендуемый способ предоставления транзакций с большей областью — использовать объект 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();
}
}
}
}
SqlConnection и Entity Framework используют транзакцию TransactionScope, используемую в окружении, и, следовательно, будут зафиксированы вместе.
Начиная с .NET 4.5.1, TransactionScope также было обновлено для работы с асинхронными методами с помощью перечисления 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();
}
}
}
}
Существуют некоторые ограничения для подхода TransactionScope:
- Для работы с асинхронными методами требуется .NET 4.5.1 или более поздней версии.
- Его нельзя использовать в облачных сценариях, если у вас нет одного и только одного подключения (облачные сценарии не поддерживают распределенные транзакции).
- Его нельзя объединить с подходом Database.UseTransaction() предыдущих разделов.
- Это вызовет исключения, если вы выполните DDL-команды и не включили поддержку распределенных транзакций через службу MSDTC.
Преимущества подхода TransactionScope:
- Он автоматически обновит локальную транзакцию до распределенной транзакции, если вы выполняете несколько подключений к определенной базе данных или объединяете подключение к одной базе данных с подключением к другой базе данных в рамках одной транзакции (примечание. Необходимо настроить службу MSDTC, чтобы разрешить распределенные транзакции для работы).
- Простота написания кода. Если вы предпочитаете, чтобы транзакция была фоновой и неявно обрабатывалась в фоновом режиме, а не явно управлялась вами, то подход TransactionScope может подходить вам лучше.
В итоге с новыми API Database.BeginTransaction() и Database.UseTransaction() выше подход TransactionScope больше не нужен для большинства пользователей. Если вы продолжаете использовать TransactionScope, помните об указанных выше ограничениях. Вместо этого рекомендуется использовать подход, описанный в предыдущих разделах.