Использование транзакций

Транзакции позволяют обрабатывать несколько операций с базой данных атомарным способом. Если транзакция зафиксирована, все операции успешно применяются к базе данных. Если транзакция отменяется, ни одна из операций не применяется к базе данных.

Совет

Вы можете скачать используемый в этой статье пример из репозитория GitHub.

Поведение транзакции по умолчанию

По умолчанию, если поставщик базы данных поддерживает транзакции, все изменения в одном вызове SaveChanges применяются в транзакции. Если какое-либо из изменений завершается ошибкой, транзакция откатывается и ни одно из изменений не применяется к базе данных. Это означает, что операция SaveChanges гарантированно либо будет выполнена, либо оставит базу данных без изменений, если возникла ошибка.

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

Управление транзакциями

Вы можете использовать API DbContext.Database для начала, фиксации и отката транзакций. В следующем примере показаны две операции SaveChanges и запрос LINQ, выполняемый в одной транзакции.

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

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

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

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

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

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

Примечание.

Управление транзакциями вручную таким способом несовместимо с неявно вызываемыми стратегиями выполнения повторных попыток. Дополнительные сведения см. в статье Устойчивость подключения.

Точки сохранения

Если при вызове SaveChanges транзакция уже выполняется в этом контексте, EF автоматически создает точку сохранения перед сохранением любых данных. Точки сохранения — это позиции в пределах транзакции базы данных, до которых можно выполнить откат в случае возникновения ошибок. Если SaveChanges встречает любую ошибку, выполняется автоматический откат транзакции до точки сохранения, то есть транзакция возвращается в такое состояние, как если бы она не запускалась. Это позволяет исправить возникшие проблемы и повторно сохранить данные, что особенно важно для проблем оптимистической блокировки.

Предупреждение

Точки сохранения несовместимы с несколькими активными результирующих наборами SQL Server (MARS). Точки сохранения не будут созданы EF при включении MARS в подключении, даже если MARS не используется. Если во время SaveChanges возникает ошибка, транзакция может оставаться в неизвестном состоянии.

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

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

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

    transaction.CreateSavepoint("BeforeMoreBlogs");

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

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

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

Транзакция в нескольких контекстах

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

Для совместного использования транзакции в контекстах должны применяться одинаковые DbConnection и DbTransaction.

Разрешение подключения извне

Для совместного использования подключения DbConnection требуется возможность передать подключение в контекст при его создании.

Самый простой способ разрешить предоставление DbConnection извне — это прекратить использование метода DbContext.OnConfiguring, чтобы настроить контекст и создать параметры DbContextOptions извне и передать их конструктору контекста.

Совет

DbContextOptionsBuilder — это API, который вы использовали в DbContext.OnConfiguring для настройки контекста. Теперь вы будете использовать его извне для создания параметров DbContextOptions.

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

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

Как вариант, можно использовать DbContext.OnConfiguring, но принимать подключение DbConnection, которое сохраняется, а затем используется в 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);
    }
}

Совместное использование подключения и транзакции

Теперь вы можете создать несколько экземпляров контекста, которые используют одно и то же подключение. Затем используйте API DbContext.Database.UseTransaction(DbTransaction), чтобы включить оба контекста в одну транзакцию.

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

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

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

        var blogs = context2.Blogs
            .OrderBy(b => b.Url)
            .ToList();
            
        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        context2.SaveChanges();
    }

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

Использование внешних транзакций базы данных (только для реляционных баз данных)

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

В следующем примере показано, как выполнить операцию ADO.NET SqlClient и операцию Entity Framework Core в одной транзакции.

using var connection = new SqlConnection(connectionString);
connection.Open();

using var transaction = connection.BeginTransaction();
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))
    {
        context.Database.UseTransaction(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();
    }

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

Использование System.Transactions

Можно использовать внешние транзакции, если вам нужно координировать действия в более широком диапазоне.

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

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        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))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
        }

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

Также можно использовать явную транзакцию.

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))
        {
            context.Database.OpenConnection();
            context.Database.EnlistTransaction(transaction);

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

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

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

Примечание.

Если вы используете асинхронные API, обязательно укажите TransactionScopeAsyncFlowOption.Enabled в конструкторе TransactionScope , чтобы обеспечить потоки внешних транзакций через асинхронные вызовы.

Дополнительные сведения о TransactionScope транзакциях и внешних транзакциях см. в этой документации.

Ограничения System.Transactions

  1. Реализация поддержки System.Transactions в EF Core зависит от поставщиков баз данных. Если поставщик не реализует поддержку System.Transactions, вызовы этих API могут полностью игнорироваться. SqlClient поддерживает этот вариант.

    Важно!

    Рекомендуется проверить, что API правильно работает с поставщиком, прежде чем использовать его для управления транзакциями. Если он работает неправильно, рекомендуется связаться с представителем поставщика базы данных.

  2. Поддержка распределенных транзакций в System.Transactions добавлена только в .NET 7.0 для Windows. Любая попытка использовать распределенные транзакции на более старых версиях .NET или на платформах, отличных от Windows, завершится ошибкой.

  3. TransactionScope не поддерживает асинхронную фиксацию или откат; это означает, что удаление его синхронно блокирует исполняемый поток до завершения операции.