Работа с транзакциями

Примечание.

Только в 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 настаивал на открытии самого подключения к базе данных (он вызвал исключение, если он был передан подключение, которое уже открыто). Так как транзакция может быть запущена только в открытом подключении, это означало, что единственный способ, которым пользователь мог упаковать несколько операций в одну транзакцию, — использовать TransactionScope или использовать ObjectContext.ПодключениеСвойство ion и начало вызова Open() и BeginTransaction() непосредственно в возвращаемом объекте Entity Подключение ion. Кроме того, вызовы API, которые связались с базой данных, завершились сбоем, если вы запустили транзакцию в базовом подключении к базе данных самостоятельно.

Примечание.

Ограничение только принятия закрытых подключений было удалено в Entity Framework 6. Дополнительные сведения см. в разделе Подключение management.

Начиная с EF6 платформа теперь предоставляет следующие возможности:

  1. Database.BeginTransaction() — более простой метод запуска и завершения транзакций пользователем в существующем DbContext— позволяет объединить несколько операций в одной транзакции и, следовательно, все зафиксированные или все откаты как один. Он также позволяет пользователю проще указать уровень изоляции для транзакции.
  2. 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) контекстOwns Подключение ion boolean.

Примечание.

Флаг contextOwns Подключение ion должен иметь значение false при вызове в этом сценарии. Это важно, так как оно сообщает Entity Framework, что оно не должно закрывать подключение при его завершении (например, см. строку 4 ниже):

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

Кроме того, необходимо самостоятельно запустить транзакцию (включая IsolationLevel, если вы хотите избежать параметра по умолчанию) и сообщите Entity Framework, что в подключении уже запущена существующая транзакция (см. строку 33 ниже).

Затем вы можете выполнять операции базы данных непосредственно в самом sql Подключение ion или в 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

Устойчивость подключения

Новая функция устойчивости Подключение ion не работает с транзакциями, инициированными пользователем. Дополнительные сведения см. в разделе "Повторные попытки выполнения стратегий".

Асинхронное программирование

Подход, описанный в предыдущих разделах, не требует дополнительных параметров или параметров для работы с асинхронным запросом и сохранением методов. Но помните, что в зависимости от того, что вы делаете в асинхронных методах, это может привести к длительным транзакциям, что, в свою очередь, может привести к взаимоблокировкам или блокировке, что плохо для производительности общего приложения.

Транзакции 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();
            }
        }
    }
}

Sql Подключение ion и Entity Framework используют транзакцию ambient 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, помните об указанных выше ограничениях. Вместо этого рекомендуется использовать подход, описанный в предыдущих разделах.