Freigeben über


Verwenden von Transaktionen

Transaktionen ermöglichen eine atomare Verarbeitung mehrerer Datenbankvorgänge. Wenn die Transaktion zugesichert wird, werden alle Vorgänge erfolgreich auf die Datenbank angewendet. Wenn die Transaktion zurückgesetzt wird, werden keine Vorgänge auf die Datenbank angewendet.

Tipp

Sie können das Beispiel dieses Artikels auf GitHub anzeigen.

Standardtransaktionsverhalten

Wenn der Datenbankanbieter Transaktionen unterstützt, werden standardmäßig alle Änderungen eines einzelnen Aufrufs an SaveChanges in einer Transaktion angewendet. Wenn eine der Änderungen fehlschlägt, wird die Transaktion zurückgesetzt, und keine der Änderungen wird auf die Datenbank angewendet. Dies bedeutet, dass SaveChanges entweder vollständig erfolgreich ist oder die Datenbank unverändert bleibt, wenn ein Fehler auftritt.

Für die meisten Anwendungen reicht dieses Standardverhalten aus. Sie sollten Transaktionen nur manuell steuern, wenn ihre Anwendungsanforderungen dies als notwendig erahnen.

Kontrollieren von Transaktionen

Sie können die DbContext.Database-API zum Starten, Commit und Rollback von Transaktionen verwenden. Das folgende Beispiel zeigt zwei SaveChanges Vorgänge und eine LINQ-Abfrage, die in einer einzelnen Transaktion ausgeführt wird:

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
}

Während alle Anbieter relationaler Datenbanken Transaktionen unterstützen, können andere Anbieter möglicherweise beim Aufrufen von Transaktions-APIs Fehler auslösen oder dies nicht ausführen.

Hinweis

Das manuelle Steuern von Transaktionen auf diese Weise ist nicht mit den implizit aufgerufenen Ausführungsstrategie für die Wiederholung kompatibel. Weitere Informationen finden Sie unter Verbindungsresilienz.

Sicherungspunkte

Wenn SaveChanges aufgerufen wird und eine Transaktion bereits im Kontext ausgeführt wird, erstellt EF automatisch einen Speicherpunkt vor dem Speichern von Daten. Savepoints sind Punkte innerhalb einer Datenbanktransaktion, auf die später zurückgegangen werden kann, wenn ein Fehler auftritt oder aus einem anderen Grund. Wenn ein Fehler bei SaveChanges auftritt, wird die Transaktion automatisch zum Speicherpunkt zurückgesetzt, sodass sie sich in demselben Zustand befindet, als wäre sie nie gestartet worden. Dies ermöglicht Ihnen, mögliche Probleme zu beheben und den Speichervorgang zu wiederholen, insbesondere wenn Probleme mit der optimistischen Nebenläufigkeit auftreten.

Warnung

Savepoints sind nicht mit den multiplen Active Result Sets (MARS) von SQL Server kompatibel. Savepoints werden nicht von EF erstellt, wenn MARS für die Verbindung aktiviert ist, auch wenn MARS nicht aktiv verwendet wird. Wenn während SaveChanges ein Fehler auftritt, wird die Transaktion möglicherweise in einem unbekannten Zustand verbleiben.

Es ist auch möglich, Speicherpunkte manuell zu verwalten, genau wie bei Transaktionen. Im folgenden Beispiel wird ein Sicherungspunkt innerhalb einer Transaktion erstellt und bei einem Fehler ein Rollback dahin ausgeführt:

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
}

Kontextübergreifende Transaktion

Sie können eine Transaktion auch für mehrere Kontextinstanzen freigeben. Diese Funktionalität ist nur bei Verwendung eines relationalen Datenbankanbieters verfügbar, da sie die Verwendung von DbTransaction und DbConnectionerfordert, die speziell für relationale Datenbanken sind.

Um eine Transaktion freigeben zu können, müssen die Kontexte DbConnection und DbTransaction verwenden.

Zulassen, dass die Verbindung extern bereitgestellt wird

Um DbConnection freigeben zu können, muss eine Verbindung an einen Kontext übergeben werden können, während dieser erstellt wird.

Die einfachste Möglichkeit, DbConnection extern bereitgestellt zu werden, besteht darin, die Verwendung der DbContext.OnConfiguring-Methode zu beenden, um den Kontext zu konfigurieren und extern DbContextOptions zu erstellen und an den Kontextkonstruktor zu übergeben.

Tipp

DbContextOptionsBuilder ist die API, die Sie in DbContext.OnConfiguring zum Konfigurieren des Kontexts verwendet haben. Jetzt werden Sie sie extern verwenden, um DbContextOptionszu erstellen.

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

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

Eine Alternative besteht darin, DbContext.OnConfiguringweiterhin zu verwenden, jedoch eine DbConnection zu akzeptieren, die gespeichert und dann in DbContext.OnConfiguringverwendet wird.

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

Freigeben der Verbindung und der Transaktion

Sie können jetzt mehrere Kontextinstanzen erstellen, die dieselbe Verbindung gemeinsam nutzen. Verwenden Sie dann die DbContext.Database.UseTransaction(DbTransaction)-API, um beide Kontexte in derselben Transaktion auflisten.

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
}

Verwenden externer DbTransactions (nur relationale Datenbanken)

Wenn Sie mehrere Datenzugriffstechnologien für den Zugriff auf eine relationale Datenbank verwenden, können Sie eine Transaktion zwischen Vorgängen freigeben, die von diesen verschiedenen Technologien ausgeführt werden.

Das folgende Beispiel zeigt, wie Sie einen ADO.NET SqlClient-Vorgang und einen Entity Framework Core-Vorgang in derselben Transaktion ausführen.

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
}

Verwenden von 'System.Transactions'

Es ist möglich, Umgebungstransaktionen zu verwenden, wenn Sie einen größeren Bereich koordinieren müssen.

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

Es ist auch möglich, sich an einer expliziten Transaktion zu beteiligen.

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

Hinweis

Wenn Sie asynchrone APIs verwenden, müssen Sie TransactionScopeAsyncFlowOption.Enabled im TransactionScope-Konstruktor angeben, um sicherzustellen, dass die Umgebungstransaktion über asynchrone Aufrufe hinweg fließt.

Weitere Informationen zu TransactionScope und Umgebungstransaktionen finden Sie in dieser Dokumentation.

Einschränkungen von System.Transactions

  1. EF Core basiert auf Datenbankanbietern, um Unterstützung für System.Transactions zu implementieren. Wenn ein Anbieter keine Unterstützung für System.Transactions implementiert, wird der Aufruf dieser APIs möglicherweise vollständig ignoriert. SqlClient unterstützt es.

    Wichtig

    Es wird empfohlen, zu testen, ob sich die API ordnungsgemäß mit Ihrem Anbieter verhält, bevor Sie sie für die Verwaltung von Transaktionen verwenden. Sie werden aufgefordert, sich an den Betreuer des Datenbankanbieters zu wenden, wenn dies nicht der Fall ist.

  2. Unterstützung für verteilte Transaktionen in System.Transactions wurde nur zu .NET 7.0 für Windows hinzugefügt. Jeder Versuch, verteilte Transaktionen auf älteren .NET-Versionen oder auf Nicht-Windows-Plattformen zu verwenden, schlägt fehl.

  3. TransactionScope unterstützt kein asynchrones Commit/Rollback, d. h. die synchrone Disposition blockiert den ausführenden Thread, bis der Vorgang abgeschlossen ist.