Korzystanie z transakcji

Transakcje umożliwiają przetwarzanie kilku operacji bazy danych w sposób niepodzielne. Jeśli transakcja zostanie zatwierdzona, wszystkie operacje zostaną pomyślnie zastosowane do bazy danych. Jeśli transakcja zostanie wycofana, żadna z operacji nie zostanie zastosowana do bazy danych.

Napiwek

Przykład z tego artykułu można zobaczyć w witrynie GitHub.

Domyślne zachowanie transakcji

Domyślnie jeśli dostawca bazy danych obsługuje transakcje, wszystkie zmiany w jednym wywołaniu SaveChanges są stosowane w transakcji. Jeśli którakolwiek ze zmian nie powiedzie się, transakcja zostanie wycofana i żadne zmiany nie zostaną zastosowane do bazy danych. Oznacza to, że SaveChanges gwarantuje to całkowite powodzenie lub pozostawienie bazy danych niezmodyfikowanej w przypadku wystąpienia błędu.

W przypadku większości aplikacji to zachowanie domyślne jest wystarczające. Transakcje należy kontrolować tylko ręcznie, jeśli wymagania aplikacji uznają to za konieczne.

Kontrolowanie transakcji

Interfejs API umożliwia DbContext.Database rozpoczynanie, zatwierdzanie i wycofywanie transakcji. W poniższym przykładzie przedstawiono dwie SaveChanges operacje i wykonywane zapytanie LINQ w jednej transakcji:

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
}

Podczas gdy wszyscy dostawcy relacyjnych baz danych obsługują transakcje, inni dostawcy mogą zgłaszać wywołania interfejsów API transakcji lub bez operacji.

Uwaga

Ręczne kontrolowanie transakcji w ten sposób jest niezgodne z niejawnie wywoływanymi strategiami ponawiania wykonywania. Aby uzyskać więcej informacji, zobacz odporność Połączenie ion.

Savepoints

Gdy SaveChanges jest wywoływana, a transakcja jest już w toku w kontekście, program EF automatycznie tworzy punkt zapisywania przed zapisaniem danych. Punkty zapisywania to punkty w ramach transakcji bazy danych, do których można później cofnąć, jeśli wystąpi błąd lub z jakiegokolwiek innego powodu. Jeśli SaveChanges wystąpi jakikolwiek błąd, automatycznie wycofa transakcję z powrotem do punktu zapisywania, pozostawiając transakcję w tym samym stanie, co nigdy nie uruchomiono. Pozwala to rozwiązać problemy i ponowić próbę zapisania, w szczególności w przypadku wystąpienia optymistycznych problemów ze współbieżnością .

Ostrzeżenie

Punkty zapisywania są niezgodne z wieloma aktywnymi zestawami wyników programu SQL Server (MARS). Punkty zapisywania nie będą tworzone przez program EF, gdy usługa MARS jest włączona w połączeniu, nawet jeśli usługa MARS nie jest aktywnie używana. Jeśli podczas zapisywania zmian wystąpi błąd, transakcja może pozostać w nieznanym stanie.

Istnieje również możliwość ręcznego zarządzania punktami zapisywania, tak jak w przypadku transakcji. Poniższy przykład tworzy punkt zapisu w ramach transakcji i wraca do niego po awarii:

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
}

Transakcja między kontekstami

Transakcję można również udostępnić w wielu wystąpieniach kontekstu. Ta funkcja jest dostępna tylko w przypadku korzystania z dostawcy relacyjnej bazy danych, ponieważ wymaga użycia DbTransaction elementów i DbConnection, które są specyficzne dla relacyjnych baz danych.

Aby udostępnić transakcję, konteksty muszą współużytkować element i DbConnectionDbTransaction.

Zezwalaj na zewnętrzne udostępnianie połączenia

Udostępnianie elementu DbConnection wymaga możliwości przekazania połączenia do kontekstu podczas jego konstruowania.

Najprostszym sposobem, aby umożliwić DbConnection udostępnianie zewnętrzne, jest zaprzestanie używania DbContext.OnConfiguring metody w celu skonfigurowania kontekstu i zewnętrznego utworzenia DbContextOptions i przekazania ich do konstruktora kontekstu.

Napiwek

DbContextOptionsBuilder to interfejs API używany w programie DbContext.OnConfiguring do konfigurowania kontekstu. Teraz użyjesz go zewnętrznie do utworzenia DbContextOptionselementu .

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

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

Alternatywą jest użycie metody DbContext.OnConfiguring, ale zaakceptuj zapisaną wartość , a następnie użytą DbConnection w elemecie 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);
    }
}

Udostępnianie połączenia i transakcji

Teraz możesz utworzyć wiele wystąpień kontekstu, które współużytkujące to samo połączenie. Następnie użyj interfejsu DbContext.Database.UseTransaction(DbTransaction) API, aby zarejestrować oba konteksty w tej samej transakcji.

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
}

Używanie zewnętrznych transakcji dbtransactions (tylko relacyjnych baz danych)

Jeśli używasz wielu technologii dostępu do danych w celu uzyskania dostępu do relacyjnej bazy danych, możesz udostępnić transakcję między operacjami wykonywanymi przez te różne technologie.

W poniższym przykładzie pokazano, jak wykonać operację ADO.NET SqlClient i operację Entity Framework Core w tej samej transakcji.

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
}

Korzystanie z elementu System.Transactions

Można użyć transakcji otoczenia, jeśli trzeba koordynować w większym zakresie.

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
    }
}

Istnieje również możliwość rejestracji w jawnej transakcji.

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
    }
}

Uwaga

Jeśli używasz asynchronicznych interfejsów API, upewnij się, że w konstruktorze TransactionScope określono wartość TransactionScopeAsyncFlowOption.Enabled, aby upewnić się, że otoczenia transakcji przepływa przez wywołania asynchroniczne.

Aby uzyskać więcej informacji na TransactionScope temat transakcji otoczenia i transakcji, zobacz tę dokumentację.

Ograniczenia transakcji System.Transactions

  1. Platforma EF Core korzysta z dostawców baz danych w celu zaimplementowania obsługi elementu System.Transactions. Jeśli dostawca nie implementuje obsługi elementu System.Transactions, możliwe, że wywołania tych interfejsów API zostaną całkowicie zignorowane. Program SqlClient obsługuje go.

    Ważne

    Zaleca się przetestowanie, czy interfejs API działa prawidłowo z dostawcą, zanim będzie on polegał na zarządzaniu transakcjami. Zachęcamy do skontaktowania się z opiekunem dostawcy bazy danych, jeśli nie.

  2. Obsługa transakcji rozproszonych w systemie System.Transactions została dodana tylko do platformy .NET 7.0 dla systemu Windows. Każda próba użycia transakcji rozproszonych na starszych wersjach platformy .NET lub na platformach innych niż Windows zakończy się niepowodzeniem.

  3. TransactionScope nie obsługuje zatwierdzania/wycofywania asynchronicznego; oznacza to, że usunięcie go synchronicznie blokuje wykonywanie wątku do momentu zakończenia operacji.