Udostępnij za pośrednictwem


Korzystanie z transakcji

Transakcje umożliwiają przetwarzanie kilku operacji bazy danych w sposób atomowy. 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.

Wskazówka

Przykład tego artykułu możesz wyświetlić na 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 zapewnia całkowity sukces lub pozostawia bazę danych niezmodyfikowaną, jeśli wystąpi błąd.

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 DbContext.Database umożliwia rozpoczynanie, zatwierdzanie i wycofywanie transakcji. W poniższym przykładzie przedstawiono dwie operacje SaveChanges i zapytanie LINQ wykonywane w jednej transakcji:

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

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

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

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

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

Podczas gdy wszyscy dostawcy relacyjnych baz danych obsługują transakcje, inni dostawcy mogą zgłaszać lub no-op, gdy są wywoływane interfejsy API transakcji.

Uwaga

Ręczna kontrola transakcji w ten sposób nie jest zgodna z niejawnie wywoływanymi strategiami ponawiania wykonania. Aby uzyskać więcej informacji, zobacz Odporność połączeń.

Punkty zapisywania

Gdy SaveChanges jest wywołane, a transakcja jest już w toku w kontekście, EF automatycznie tworzy punkt zapisu przed zapisaniem danych. Punkty zapisywania to punkty w ramach transakcji bazy danych, do których można wrócić później, jeśli wystąpi błąd lub z dowolnego innego powodu. Jeśli SaveChanges napotka jakikolwiek błąd, automatycznie wycofa transakcję z powrotem do punktu kontrolnego, pozostawiając transakcję w tym samym stanie, jakby nigdy się nie rozpoczęła. Dzięki temu można rozwiązać problemy i ponowić próbę zapisania, w szczególności w przypadku wystąpienia optymistycznej współbieżności problemów.

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 w przypadku niepowodzenia.

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

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

    await transaction.CreateSavepointAsync("BeforeMoreBlogs");

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

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

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

Transakcja między kontekstami

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

Aby udostępnić transakcję, konteksty muszą udostępniać zarówno DbConnection, jak i DbTransaction.

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

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

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

Wskazówka

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

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

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

Alternatywą jest dalej używać DbContext.OnConfiguring, ale akceptować DbConnection, które są zapisywane, a następnie używane w 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łączeń i transakcji

Teraz możesz utworzyć wiele wystąpień kontekstu, które współużytkują to samo połączenie. Następnie użyj interfejsu API DbContext.Database.UseTransaction(DbTransaction), 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);
await using var transaction = await context1.Database.BeginTransactionAsync();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    await context1.SaveChangesAsync();

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

        var blogs = await context2.Blogs
            .OrderBy(b => b.Url)
            .ToListAsync();

        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        await context2.SaveChangesAsync();
    }

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

Używanie zewnętrznych DbTransactions (tylko relacyjne bazy 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);
await connection.OpenAsync();

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

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

Korzystanie z System.Transactions

Można użyć transakcji środowiskowych, jeśli trzeba koordynować w szerszym zakresie.

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

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

        // 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" });
            await context.SaveChangesAsync();
        }

        // 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ść dołączenia do 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))
        {
            await context.Database.OpenConnectionAsync();
            context.Database.EnlistTransaction(transaction);

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

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

        // 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, należy określić TransactionScopeAsyncFlowOption.Enabled w konstruktorze TransactionScope, aby upewnić się, że otoczenia transakcji przepływa w wywołaniach asynchronicznych.

Aby uzyskać więcej informacji na temat transakcji TransactionScope i transakcji środowiskowych, 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 funkcji System.Transactions, możliwe, że wywołania tych API zostaną całkowicie zignorowane. SqlClient go obsługuje.

    Ważne

    Zaleca się przetestowanie, czy interfejs API działa prawidłowo z dostawcą, zanim będziecie na nim polegać do zarządzania transakcjami. Zachęcamy do skontaktowania się z opiekunem bazy danych dostawcy, jeśli tak nie jest.

  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.