40.2 Konkurrierende Zugriffe mit dem Entity Framework
40.2.1 Das Standardverhalten des Entity Frameworks

Das Entity Framework unterstützt nur das optimistische Sperren, nicht aber das pessimistische. Das Entity Framework speichert Objektänderungen in der Datenbank, ohne die Parallelität zu überprüfen. Sehen wir uns zuerst das Standardverhalten an, wenn mehrere Benutzer gleichzeitig dieselbe Datenzeile aktualisieren. Dazu dient das folgende Listing.
using (NorthwindEntities context = new NorthwindEntities())
{
var query = context.Products.First(p => p.ProductID == 1);
query.ProductName = "Kuchen";
// Konflikt simulieren
Console.WriteLine("2. User simulieren ...");
Console.ReadLine();
// Änderungen speichern
context.SaveChanges();
Console.WriteLine("DB aktualisiert.");
}
Listing 40.1 Das Standardverhalten bei konkurrierenden Zugriffen
Wir besorgen uns die erste Datenzeile aus der Tabelle Products und ändern die Eigenschaft ProductName. Das Listing erlaubt es, einen zweiten Benutzer zu simulieren. Dazu können Sie beispielsweise in Visual Studio das Fenster Server-Explorer öffnen und, falls nicht schon vorhanden, eine Verbindung zur Datenbank Northwind herstellen. Öffnen Sie dann die Tabelle Products, und editieren Sie die erste Datenzeile. Dabei spielt es keine Rolle, ob Sie die Spalte ProductName editieren oder eine andere. Am Ende wird, nach Fortsetzung des Konsolenprogramms, die Änderung des Produktbezeichners aus der Anwendung heraus in »Kuchen« erfolgreich verlaufen. Es liegt kein Konflikt vor.
Sehen wir uns an, welches SQL-Statement vom Entity Framework gegen die Datenbank abgesetzt wird.
exec sp_executesql N'update [dbo].[Products]
set [ProductName] = @0
where ([ProductID] = @1)
',N'@0 nvarchar(40),@1 int',@0=N'Kuchen',@1=1
Die alles entscheidende WHERE-Klausel enthält nur die Angabe der Primärschlüsselspalte. Solange die Datenzeile in der Datenbank nicht gelöscht worden ist, wird die Aktualisierung zu einem erfolgreichen Abschluss führen.
40.2.2 Das Aktualisierungsverhalten mit »Fixed« beeinflussen

Nehmen wir an, die beiden Benutzer A und B würden gleichzeitig dieselbe Datenzeile editieren. Dabei müssen Sie aber sicherstellen, dass eine Änderung von Benutzer A im Feld ProductName nicht blindlings von Benutzer B überschrieben wird. Um das zu gewährleisten, muss die Eigenschaft ProductName in die WHERE-Klausel mit aufgenommen werden. Diese Forderung lässt sich sehr einfach umsetzen, wenn man die Eigenschaft ConcurrencyMode (in der deutschen Version von Visual Studio leider in Parallelitätsmodus übersetzt) der Entitätseigenschaft ProductName im Eigenschaftsfenster des EDM-Designers auf Fixed einstellt (siehe Abbildung 40.1). Bei der Verwendung dieses Attributs wird die Datenbank vom Entity Framework vor dem Speichern von Änderungen auf Änderungen hin geprüft.
Abbildung 40.1 Setzen des Parallelitätsmodus einer Eigenschaft
Diese Eigenschaftsänderung bewirkt auch eine Anpassung der Beschreibung der Eigenschaft ProductName im konzeptionellen Modell:
<Property Name="ProductName" Type="String" Nullable="false" MaxLength="40"
Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
Führen Sie das Listing 40.1 mit dieser Änderung noch einmal aus und simulieren den konkurrierenden Zugriff auf ProductName, kommt es zu einer Ausnahme vom Typ OptimisticConcurrencyException. Interessanter ist für uns aber in diesem Moment zunächst das SQL-Statement, das gegen die Datenbank abgesetzt wird:
exec sp_executesql N'update [dbo].[Products]
set [ProductName] = @0
where (([ProductID] = @1) and ([ProductName] = @2))
',N'@0 nvarchar(40),@1 int,@2 nvarchar(40)',@0=N'Kuchen',@1=1,@2=N'Chai'
Es ist zu erkennen, dass die Einstellung Fixed der Eigenschaft ProductName dafür gesorgt hat, dass die Spalte in die WHERE-Klausel aufgenommen wird. Das gilt nicht nur für eine Aktualisierung mit UPDATE, sondern auch dann, wenn eine Datenzeile mit DELETE gelöscht werden soll.
Möchten wir, dass jede Änderung eines anderen Benutzers zu der Ausnahme führt, müssen wir alle Eigenschaften der Entität entsprechend auf Fixed einstellen. In solchen Fällen ist es besser, sich spätestens jetzt Gedanken über eine Timestamp-Spalte in der Tabelle zu machen.
40.2.3 Auf die Ausnahme »OptimisticConcurrencyException« reagieren

Um auf die Ausnahme OptimisticConcurrencyException des Listings 40.1 zu reagieren, benötigen wir einen entsprechenden try-catch-Block. Zumindest die ausnahmeauslösende Methode SaveChanges muss hier innerhalb des try-Blocks codiert werden.
using (NorthwindEntities context = new NorthwindEntities())
{
var query = context.Products.First(p => p.ProductID == 1);
query.ProductName = "Kuchen";
// Konflikt simulieren
Console.WriteLine("2. User simulieren ...");
Console.ReadLine();
// Änderungen speichern
try
{
context.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
return;
}
Console.WriteLine("DB aktualisiert.");
}
Listing 40.2 Behandlung der Ausnahme »OptimisticConcurrencyException«
Damit behandeln wir zwar die aufgetretene Ausnahme, aber eine Lösung des Konflikts haben wir noch nicht erreicht. Wie könnte die Lösung überhaupt aussehen?
Grundsätzlich stehen Ihnen im Entity Framework zwei allgemeine Lösungsansätze zur Verfügung:
- Entweder der Benutzer setzt seine Änderungen gegenüber den Änderungen in der Datenbank durch, die ein anderer Benutzer gemacht hat. Dieser Ansatz wird als ClientWins bezeichnet.
- Die Änderungen des Benutzers werden verworfen. Dieser Ansatz heißt StoreWins.
Wir wollen uns nun diese beiden Konzepte genauer ansehen.
40.2.4 Das »ClientWins«-Szenario

Das ClientWins-Szenario arbeitet nach dem folgenden Prinzip: Tritt ein Parallelitätskonflikt auf, werden im ersten Schritt die neuen aktuellen Werte der betreffenden Datenzeile bei der Datenbank abgefragt und zu den neuen Originalwerten der Entität im Objektkontext. Die Current-Werte, die auch die Änderungen durch den Benutzer beinhalten, bleiben unverändert. Zudem werden alle Eigenschaften als Modified gekennzeichnet.
Zur Aktualisierung der Werte der konfliktverursachenden Entität veröffentlicht der Objektkontext die Methode Refresh, die zwei Parameter definiert. Dem ersten Parameter wird entweder die Option RefreshMode.ClientWins oder RefreshMode.StoreWins übergeben. Damit wird festgelegt, wie die Werte der Entität weiter behandelt werden. Der zweite Parameter erwartet die Referenz auf die konfliktverursachende Entität.
Im folgenden Listing wird das ClientWins-Szenario genutzt, um bei einem auftretenden Konflikt die Änderungen des Benutzers gegenüber den zuvor erfolgten Änderungen durchzusetzen.
using (NorthwindEntities context = new NorthwindEntities())
{
var query = (context.Products).FirstOrDefault(p => p.ProductID == 1);
query.ProductName = "Kuchen";
// Konflikt simulieren
Console.WriteLine("2. User simulieren ...");
Console.ReadLine();
try
{
// Änderungen speichern
context.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
context.Refresh(RefreshMode.ClientWins, ex.StateEntries[0].Entity);
context.SaveChanges();
}
Console.WriteLine("DB aktualisiert.");
}
Listing 40.3 Konfliktlösung mit dem »ClientWins«-Ansatz
Im catch-Zweig wird mit der ersten Anweisung zunächst die konfliktverursachende Entität ermittelt. Die entsprechende Information wird durch die Eigenschaft StateEntries des Exception-Objekts bereitgestellt, bei der es sich um eine schreibgeschützte Collection von ObjectStateEntry-Objekten handelt. Danach wird auf den Objektkontext dessen Methode Refresh aufgerufen unter Bekanntgabe der Option RefreshMode.ClientWins.
Die Refresh-Methode sorgt dafür, dass die in dem Moment aktuellen Werte der konfliktverursachenden Datenzeile aus der Datenbank abgerufen werden und an OriginalValues des ObjectStateEntry-Objekts der konfliktverursachenden Entität eingetragen werden. Nun kann erneut die Methode SaveChanges aufgerufen werden. Da nun in der WHERE-Klausel die in dem Moment tatsächlich vorliegenden Werte zur Identifizierung des Datensatzes herangezogen werden, wird die Aktualisierung nun gelingen. Der Client hat sich gegenüber den zuvor erfolgten Änderungen durchgesetzt.
Sie können sich das ansehen, wenn Sie den catch-Zweig des Listings 40.3 wie nachfolgend gezeigt ergänzen:
catch (OptimisticConcurrencyException ex)
{
context.Refresh(RefreshMode.ClientWins, ex.StateEntries[0].Entity);
// Ausgabe der Current-Werte des ObjectStateEntry-Objekts
Console.WriteLine("Current\n" + new string('-', 50));
DbDataRecord actual = errorEntry.CurrentValues;
for (int i = 0; i < actual.FieldCount - 1; i++)
Console.WriteLine("{0,-35}{1}", actual.GetName(i), actual.GetValue(i));
// Ausgabe der Current-Werte des ObjectStateEntry-Objekts
DbDataRecord orig = errorEntry.OriginalValues;
Console.WriteLine("\nOriginal\n" + new string('-', 50));
for (int i = 0; i < orig.FieldCount - 1; i++)
Console.WriteLine("{0,-35}{1}", orig.GetName(i), orig.GetValue(i));
// Daten speichern
context.SaveChanges();
}
Listing 40.4 Änderung des catch-Zweiges aus Listing 40.3
Hinweis
Vielleicht stellen Sie sich die Frage nach dem Unterschied zu dem Szenario, in dem der Konflikt komplett ignoriert wird (die WHERE-Klausel enthält zur Identifizierung der zu ändernden Datenzeile in der Datenbank nur den Primärschlüssel). Die Antwort ist in den Spalten zu finden, die an die Datenbank übermittelt werden. Wird der Parallelitätskonflikt ignoriert, werden im SQL-Aktualisierungsstatement mit SET nur die Felder (Eigenschaften) angegeben, die tatsächlich durch den Benutzer verändert worden sind. Beim Updaten nach dem Aufruf
der Refresh-Methode mit der Option RefreshMode.ClientWins hingegen werden jedoch alle Felder angegeben. Damit werden natürlich alle Änderungen, die ein Benutzer zuvor an einer Datenzeile vorgenommen hat, überschrieben.
Wiederholter Aufruf der Methode »SaveChanges«
Wird im catch-Zweig SaveChanges erneut aufgerufen, besteht die Gefahr, dass erneut eine Ausnahme ausgelöst wird. Auch darauf muss reagiert werden, damit die Anwendung nicht unplanmäßig durch einen nicht behandelten Fehler beendet wird. Verfolgt man den Ablauf weiter, müsste man sehr viele ineinander verschachtelte try-catch-Zweige programmieren.
Das ist natürlich eine schlechte Lösung, ohne dass am Ende die Gewähr besteht, dass eine beliebige Anzahl aufeinander folgender Ausnahmen behandelt werden kann. Hier gibt es einen besseren Lösungsansatz, wenn man die von der Klasse ObjectContext geerbte Methode SaveChanges überschreibt. Bezogen auf unser Entity Data Model, in dem die Klasse NorthwindEntities den Objektkontext beschreibt, müsste eine partielle Klasse bereitgestellt werden, innerhalb deren die Methode SaveChanges rekursiv aufgerufen wird.
Das folgende Codefragment zeigt das Prinzip des rekursiven Aufrufs von SaveChanges.
public partial class NorthwindEntities {
public override int SaveChanges(SaveOptions options) {
try {
return base.SaveChanges(options);
}
catch (OptimisticConcurrencyException ex) {
Refresh(RefreshMode.ClientWins, ex.StateEntries[0].Entity);
return SaveChanges(options);
}
catch (UpdateException ex) {
throw ex;
}
}
}
40.2.5 Das »StoreWins«-Szenario
Im zweiten denkbaren Konfliktbehandlungsszenario werden die Benutzerdaten im Objektkontext durch die aktuellen Daten aus der Datenbank ersetzt. Dabei gehen natürlich auch sämtliche Änderungen des Benutzers verloren. So wenig verlockend dieses Szenario im ersten Moment auch klingt, es kann bei einigen Anwendungen durchaus die beste Lösung darstellen. Zwar muss der Benutzer alle seine Daten neu eingeben, aber das kann natürlich auch von der Anwendung übernommen werden – falls die Benutzeränderungen vorher gesichert worden sind.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.