Arbeiten mit Transaktionen
Hinweis
Nur EF6 und höher: Die Features, APIs usw., die auf dieser Seite erläutert werden, wurden in Entity Framework 6 eingeführt. Wenn Sie eine frühere Version verwenden, gelten manche Informationen nicht.
In diesem Artikel wird die Verwendung von Transaktionen in EF6 beschrieben, einschließlich der Verbesserungen, die wir seit EF5 hinzugefügt haben, um die Arbeit mit Transaktionen zu vereinfachen.
Die Standardfunktionsweise von EF
In allen Versionen von Entity Framework (EF) wird dieser Vorgang beim Ausführen der SaveChanges()-Methode zum Einfügen, Aktualisieren oder Löschen in der Datenbank durch das Framework in einer Transaktion umschlossen. Diese Transaktion dauert nur so lange, wie der Vorgang zum Ausführen und anschließenden Abschließen benötigt. Wenn Sie einen weiteren Vorgang dieser Art ausführen, wird eine neue Transaktion gestartet.
Ab EF6 wird der Befehl Database.ExecuteSqlCommand() standardmäßig in einer Transaktion umschlossen, wenn noch keine vorhanden war. Diese Methode verfügt über Überladungen, mit denen Sie dieses Verhalten bei Bedarf außer Kraft setzen können. Auch die Ausführung von im Modell enthaltenen gespeicherten Vorgängen in EF6 über APIs wie ObjectContext.ExecuteFunction() führt zum selben Ergebnis (außer dass das Standardverhalten derzeit nicht außer Kraft gesetzt werden kann).
In jedem Fall ist die Isolationsebene der Transaktion unabhängig von der Isolationsebene, die der Datenbankanbieter als Standardeinstellung betrachtet. In SQL Server lautet diese zum Beispiel standardmäßig „READ COMMITED“.
Entity Framework umschließt keine Abfragen in einer Transaktion.
Diese Standardfunktionalität eignet sich für viele Benutzer*innen. In diesem Fall ist kein abweichendes Vorgehen in EF6 erforderlich: schreiben Sie einfach den Code wie immer.
Einige Benutzer*innen benötigen jedoch mehr Kontrolle über ihre Transaktionen. Darauf wird in den folgenden Abschnitten eingegangen.
Funktionsweise der APIs
Vor EF6 war es erforderlich, dass die Datenbankverbindung durch Entity Framework selbst geöffnet wird (dabei wurde eine Ausnahme ausgelöst, wenn eine bereits geöffnete Verbindung übergeben wurde). Da eine Transaktion nur für eine geöffnete Verbindung gestartet werden kann, konnten Benutzer*innen mehrere Vorgänge in eine Transaktion nur über die Verwendung einer TransactionScope-Klasse oder der ObjectContext.Connection-Eigenschaft umschließen und open() sowie BeginTransaction() direkt für das zurückgegebene EntityConnection-Objekt aufrufen. Darüber hinaus schlugen API-Aufrufe, die die Datenbank kontaktiert haben, fehl, wenn Sie eine Transaktion für die zugrunde liegende Datenbankverbindung eigenständig gestartet hatten.
Hinweis
Die Einschränkung, dass nur geschlossene Verbindungen akzeptiert werden, wurde in Entity Framework 6 entfernt. Weitere Informationen finden Sie unter Verbindungsverwaltung.
Ab EF6 bietet das Framework jetzt Folgendes:
- Database.BeginTransaction(): Hierbei handelt es sich um eine einfachere Methode, mit der Benutzer*innen Transaktionen selbst innerhalb einer vorhandenen DbContext-Klasse starten und abschließen können. Dadurch können mehrere Vorgänge innerhalb derselben Transaktion kombiniert und somit alle Vorgänge als eine Transaktion zugesichert oder zurückgesetzt werden. Außerdem können Benutzer*innen dadurch die Isolationsebene für diese Transaktion einfacher angeben.
- Database.UseTransaction() : Mit dieser Methode kann die DbContext-Klasse eine Transaktion verwenden, die außerhalb von Entity Framework gestartet wurde.
Kombinieren mehrerer Vorgänge in einer Transaktion innerhalb desselben Kontexts
Die Database.BeginTransaction()-Methode verfügt über zwei Außerkraftsetzungen: eine, die ein explizites IsolationLevel verwendet, und eine, die keine Argumente akzeptiert und die Standardisolationsebene des zugrunde liegenden Datenbankanbieters verwendet. Beide Außerkraftsetzungen geben ein DbContextTransaction-Objekt zurück, das Commit()- und Rollback()-Methoden bereitstellt, die Commit- und Rollbackvorgänge in der zugrunde liegenden Speichertransaktion ausführen.
Das Objekt DbContextTransaction soll gelöscht werden, sobald es committet oder ein Rollback ausgeführt wurde. Eine einfache Möglichkeit hierfür ist die Syntax using(...) {...}, die automatisch die Dispose()-Methode aufruft, wenn der Block der using-Anweisung abgeschlossen ist:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void StartOwnTransactionWithinContext()
{
using (var context = new BloggingContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
context.Database.ExecuteSqlCommand(
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'"
);
var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
dbContextTransaction.Commit();
}
}
}
}
}
Hinweis
Um mit einer Transaktion beginnen zu können, muss die zugrunde liegende Speicherverbindung offen sein. Das bedeutet, dass die Verbindung durch das Aufrufen der Database.BeginTransaction()-Methode geöffnet wird, wenn die Verbindung noch nicht offen ist. Falls die DbContextTransaction-Klasse die Verbindung geöffnet hat, wird sie geschlossen, wenn die Dispose()-Methode aufgerufen wird.
Übergeben einer vorhandenen Transaktion an den Kontext
Manchmal streben Sie eine Transaktion an, die noch umfangreicher ist und Vorgänge in derselben Datenbank umfasst, die sich jedoch vollständig außerhalb von EF befinden. Hierzu müssen Sie die Verbindung öffnen und die Transaktion selbst starten. Daraufhin müssen Sie EF zuerst anweisen, die bereits geöffnete Datenbankverbindung zu verwenden und dann die vorhandene Transaktion für diese Verbindung zu verwenden.
Dazu müssen Sie einen Konstruktor für Ihre Kontextklasse definieren und verwenden, der von einem der DbContext-Konstruktoren erbt. Diese verwenden einen vorhandenen Verbindungsparameter und den booleschen Wert „contextOwnsConnection“.
Hinweis
Das Flag „contextOwnsConnection“ muss auf „false“ festgelegt werden, wenn es in diesem Szenario aufgerufen wird. Dieser Wert ist wichtig, da er Entity Framework darüber informiert, dass die Verbindung nicht geschlossen werden sollte, wenn EF den Vorgang abgeschlossen hat (siehe beispielsweise Zeile 4 unten):
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
}
}
Darüber hinaus müssen Sie die Transaktion selbst starten (einschließlich der Isolationsebene, wenn Sie die Standardeinstellung vermeiden möchten) und Entity Framework mitteilen, dass eine Transaktion bereits mit der Verbindung gestartet wurde (siehe Zeile 33 unten).
Anschließend können Sie Datenbankvorgänge direkt über die SqlConnection-Klasse selbst oder die DbContext-Klasse ausführen. Alle derartigen Vorgänge werden innerhalb einer Transaktion ausgeführt. Die Verantwortung für das Ausführen eines Commits oder Rollbacks der Transaktion und für den Aufruf der Dispose()-Methode dafür sowie für das Schließen und Löschen der Datenbankverbindung liegt bei Ihnen. Beispiel:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void UsingExternalTransaction()
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
{
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.Transaction = sqlTxn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
sqlCommand.ExecuteNonQuery();
using (var context =
new BloggingContext(conn, contextOwnsConnection: false))
{
context.Database.UseTransaction(sqlTxn);
var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
}
sqlTxn.Commit();
}
}
}
}
}
Löschen der Transaktion
Sie können einen NULL-Wert an die Database.UseTransaction()-Methode übergeben, um das Wissen von Entity Framework über die aktuelle Transaktion zu löschen. In diesem Fall führt Entity Framework keinen Commit oder Rollback der vorhandenen Transaktion durch. Dieses Vorgehen sollte daher bedacht und nur dann verwendet werden, wenn die Transaktion tatsächlich gelöscht werden soll.
Fehler in der UseTransaction-Methode
Wenn Sie eine Transaktion übergeben, wird eine Ausnahme von der Database.UseTransaction()-Methode in den folgenden Fällen angezeigt:
- Entity Framework verfügt bereits über eine vorhandene Transaktion.
- Entity Framework wird bereits in einer TransactionScope-Klasse ausgeführt.
- Das Verbindungsobjekt in der übergebenen Transaktion entspricht dem Wert NULL. Die Transaktion ist also keiner Verbindung zugeordnet. In der Regel bedeutet das, dass die Transaktion bereits abgeschlossen wurde.
- Das Verbindungsobjekt in der übergebenen Transaktion stimmt nicht mit der Verbindung von Entity Framework überein.
Verwenden von Transaktionen mit weiteren Features
In diesem Abschnitt wird erläutert, wie die oben genannten Transaktionen mit dem Folgenden interagieren:
- Verbindungsstabilität
- Asynchrone Methoden
- TransactionScope-Transaktionen
Verbindungsstabilität
Das neue Feature der Verbindungsresilienz funktioniert nicht mit von Benutzer*innen initiierten Transaktionen. Weitere Informationen finden Sie unter Wiederholen von Ausführungsstrategien.
Asynchrone Programmierung
Der in den vorherigen Abschnitten beschriebene Ansatz benötigt keine weiteren Optionen oder Einstellungen für das Arbeiten mit der asynchronen Abfrage und das Speichern von Methoden. Beachten Sie jedoch, dass dies in Abhängigkeit davon, was Sie innerhalb der asynchronen Methoden tun, zu langen Transaktionen führen kann. Das kann wiederum zu Deadlocks oder Blockierungen führen, die sich negativ auf die Leistung der gesamten Anwendung auswirken.
TransactionScope-Transaktionen
Vor EF6 wurde empfohlen, ein TransactionScope-Objekt zu verwenden, um Transaktionen in größerem Umfang bereitzustellen:
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void UsingTransactionScope()
{
using (var scope = new TransactionScope(TransactionScopeOption.Required))
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
sqlCommand.ExecuteNonQuery();
using (var context =
new BloggingContext(conn, contextOwnsConnection: false))
{
var query = context.Posts.Where(p => p.Blog.Rating > 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
}
}
scope.Complete();
}
}
}
}
Die SqlConnection-Klasse und Entity Framework verwendeten die umgebende TransactionScope-Transaktion und wurden daher gemeinsam committet.
Ab .NET 4.5.1 wurde die TransactionScope-Klasse aktualisiert und funktioniert seitdem auch mit asynchronen Methoden über die Verwendung der TransactionScopeAsyncFlowOption-Enumeration:
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
public static void AsyncTransactionScope()
{
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
using (var conn = new SqlConnection("..."))
{
await conn.OpenAsync();
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
await sqlCommand.ExecuteNonQueryAsync();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
var query = context.Posts.Where(p => p.Blog.Rating > 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
await context.SaveChangesAsync();
}
}
scope.Complete();
}
}
}
}
Es gibt noch einige Einschränkungen für den TransactionScope-Ansatz:
- Er benötigt NET 4.5.1 oder höher, um mit asynchronen Methoden zu funktionieren.
- Er kann nicht in Cloudszenarios verwendet werden, es sei denn, Sie sind sicher, dass Sie über eine einzige Verbindung verfügen (Cloudszenarios unterstützen keine verteilten Transaktionen).
- Er kann nicht mit dem Database.UseTransaction()-Ansatz der vorherigen Abschnitte kombiniert werden.
- Er löst Ausnahmen aus, wenn Sie eine beliebige DDL ausgeben und keine verteilten Transaktionen über den MS DTC-Dienst aktiviert haben.
Vorteile des TransactionScope-Ansatzes:
- Bei diesem Ansatz wird automatisch ein Upgrade einer lokale Transaktion auf eine verteilte Transaktion durchgeführt, wenn Sie mehr als eine Verbindung zu einer bestimmten Datenbank herstellen oder eine Verbindung zu einer Datenbank mit einer Verbindung zu einer anderen Datenbank innerhalb derselben Transaktion kombinieren (Hinweis: Hierfür muss der MS DTC-Dienst so konfiguriert sein, dass er verteilte Transaktionen zulässt).
- Es ist einfach, mit diesem Ansatz Code zu verfassen. Wenn Sie umgebende Transaktionen vorziehen, die implizit im Hintergrund und nicht explizit unter Ihrer Kontrolle ausgeführt werden, ist der TransactionScope-Ansatz möglicherweise besser für Sie geeignet.
Zusammenfassend ist der TransactionScope-Ansatz für die meisten Benutzer*innen durch die APIs „Database.BeginTransaction()“ und „Database.UseTransaction()“ nicht mehr erforderlich. Wenn Sie den TransactionScope-Ansatz weiterhin verwenden, beachten Sie die oben genannten Einschränkungen. Es wird empfohlen, stattdessen den Ansatz zu verwenden, der in den vorherigen Abschnitten beschrieben wird.