Verwenden von Transaktionen

Mit Transaktionen können mehrere Datenbankvorgänge einzeln verarbeitet werden. Wenn die Transaktion committet wird, werden alle Vorgänge erfolgreich auf die Datenbank angewendet. Falls für die Transaktion ein Rollback ausgeführt wird, wird keiner der Vorgänge für die Datenbank übernommen.

Tipp

Das in diesem Artikel verwendete Beispiel finden Sie auf GitHub.

Standardtransaktionsverhalten

Wenn der Datenbankanbieter Transaktionen unterstützt, werden standardmäßig alle Änderungen in einem einzigen Aufruf von SaveChanges in einer Transaktion angewendet. Wenn bei einer der Änderungen ein Fehler auftritt, wird ein Rollback ausgeführt, und keine der Änderungen wird für die Datenbank übernommen. SaveChanges wird also entweder vollständig ausgeführt oder überhaupt nicht, wenn ein Fehler auftritt. Die Datenbank bleibt in diesem Fall unverändert.

Bei den meisten Anwendungen ist dieses Standardverhalten ausreichend. Sie sollten Transaktionen nur manuell steuern, wenn die Anforderungen Ihrer Anwendung dies notwendig machen.

Steuern von Transaktionen

Mit der DbContext.Database-API können Sie Transaktionen beginnen, committen und ein Rollback dafür ausführen. Im folgenden Beispiel werden zwei SaveChanges-Vorgänge und eine LINQ-Abfrage dargestellt, die in einer einzelnen Transaktion ausgeführt werden:

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
}

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 Verbindungssicherheit.

Sicherungspunkte

Wenn SaveChanges aufgerufen und im Kontext bereits eine Transaktion ausgeführt wird, erstellt EF automatisch einen Sicherungspunkt, bevor Daten gespeichert werden. Sicherungspunkte sind Punkte innerhalb einer Datenbanktransaktion, für die später ein Rollback ausgeführt werden kann, wenn ein Fehler auftritt, oder aus einem anderen Grund. Wenn bei SaveChanges ein Fehler auftritt, wird die Transaktion automatisch an den Sicherungspunkt zurückgesetzt, sodass die Transaktion sich in demselben Zustand befindet, als sei 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

Sicherungspunkte sind nicht kompatibel mit den Multiple Active Result Sets (MARS) von SQL Server. Sicherungspunkte 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, bleibt die Transaktion möglicherweise in einem unbekannten Zustand.

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

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
}

Kontextübergreifende Transaktion

Sie können Transaktionen auch für mehrere Kontextinstanzen freigeben. Diese Funktion ist nur bei Anbietern von relationalen Datenbanken verfügbar, da DbTransaction und DbConnection verwendet werden müssen, die charakteristisch für relationale Datenbanken sind.

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

Zulassen von extern bereitgestellten Verbindungen

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, eine externe Bereitstellung von DbConnection zuzulassen, ist, den Kontext nicht mehr mit der DbContext.OnConfiguring-Methode zu konfigurieren, sondern extern DbContextOptions zu erstellen und es an den Kontextkonstruktor zu übergeben.

Tipp

DbContextOptionsBuilder ist die API, die Sie in DbContext.OnConfiguring zum Konfigurieren des Kontexts verwendet haben. Jetzt werden Sie es extern verwenden und DbContextOptions erstellen.

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

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

Alternativ können Sie weiterhin DbContext.OnConfiguring verwenden und eine DbConnection akzeptieren, die gespeichert und dann in DbContext.OnConfiguring verwendet 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 nun mehrere Kontextinstanzen erstellen, die die gleiche Verbindung verwenden. Anschließend tragen Sie mit der DbContext.Database.UseTransaction(DbTransaction)-API beide Kontexte in derselben Transaktion ein.

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
}

Verwenden von externen DbTransactions (nur relationale Datenbanken)

Wenn Sie mit verschiedenen Datenzugriffstechnologien auf eine relationale Datenbank zugreifen, sollten Sie eine gemeinsame Transaktion für die Vorgänge einrichten, die von diesen verschiedenen Technologien ausgeführt werden.

Im folgenden Beispiel wird gezeigt, wie ein ADO.NET SqlClient-Vorgang und ein Entity Framework Core-Vorgang in derselben Transaktion ausgeführt werden.

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
}

Verwenden von 'System.Transactions'

Es ist möglich, Ambient-Transaktionen 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);
    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
    }
}

Sie können auch eine Eintragung in einer expliziten Transaktion vornehmen.

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

Hinweis

Wenn Sie asynchrone APIs verwenden, müssen Sie unbedingt 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. In EF Core müssen die Datenbankanbieter die Unterstützung für System.Transactions implementieren. Wenn ein Anbieter keine Unterstützung für System.Transactions implementiert, ist es möglich, dass Aufrufe dieser APIs vollständig ignoriert werden. SqlClient unterstützt dies.

    Wichtig

    Daher sollten Sie testen, ob die API ordnungsgemäß mit Ihrem Anbieter funktioniert, bevor Sie sie für die Verwaltung von Transaktionen einsetzen. Sollte die API nicht funktionieren, wenden Sie sich bitte an den Maintainer des Datenbankanbieters.

  2. Unterstützung für verteilte Transaktionen in System.Transactions wurde in .NET 7.0 nur 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.