Datensatz sperren in Entity Framework 6: Optimistisches vs. Pessimistisches Sperren
„Wie arbeite ich mit Datensatzsperren?“ ist eine der häufigsten Fragen beim Einstieg in die Arbeit mit dem Entity Framework. Hintergrund ist der Wunsch, die Änderungen, die ein Benutzer in der Datenbank hinterlassen hat, nicht ohne Weiteres durch andere Schreibzugriffe zu überschreiben – die sogenannte Concurrency.
Aus älteren Datenbanksystemen kennt man das pessimistische Sperren, was mit Technologien wie dem klassischen Recordset oder ähnlichen, Cursor-artigen Konzepten kein Problem war: Ein Datensatz wurde einfach zur Bearbeiten durch andere gesperrt, sobald ein Benutzer begann, diesen zu bearbeiten.
In mehrschichtigen Szenarien, wie sie heute in modernen Desktopanwendungen und Apps verwendet werden, ist diese Sperrung nur auf Datenbankserverseite möglich, erst Recht bei Webanwendungen, wo noch ein aufwändiges Session-Handling dazukommt. Entity Framework 6 arbeitet jedoch applikations- oder clientseitig, ein direkter Serverzugriff ist nicht vorgesehen. Daher unterstützt Entity Framework nur „Optimistisches Sperren“ – heißt: Alle Änderungen werden zugelassen, und erst beim Schreibzugriff wird geprüft, ob sich in der Zwischenzeit die zu bearbeitenden Daten geändert haben.
Entity Framework 6 unterstützt den Entwickler auf sehr einfache, aber effektive Weise: Datenbanktabellen, in denen konkurrierende Schreibzugriffe abgefangen werden müssen (gilt längst nicht für jede Tabelle in der DB!), bekommen zusätzlich eine Spalte, deren Wert beim Schreiben grundsätzlich verändert wird. In Microsofts SQL-Welt gibt es dafür den timestamp
-Datentyp, der hier nachträglich in die Customers-Tabelle der Nordwind-Datenbank eingebaut wird:
Beim Erstellen des Entity-Modells wird daraus eine TimeStamp-Property in der generierten Entitätsklasse:
Spannend für die Concurrency-Behandlung sind die beiden Properties Parallelitätsmodus (engl. „Concurrency Mode“) und StoreGeneratedPattern (zum Glück hat man darauf verzichtet, dies zu übersetzen) der generierten TimeStamp-Property:
Während „StoreGeneratedPattern“ bei Feldern vom Typ timestamp
bereits vom EF-Designer auf „Computed“ gesetzt wird (die anderen hier zulässigen Werte sind „Identity“ – siehe Auto-Inkrement-Primärschlüssel – und „None“), muss der „Parallelitätsmodus“ von Hand auf „Fixed“ gesetzt werden. Hierdurch wird beim Speichern des Datensatzes auf anderweitige Veränderungen geprüft und eine DbUpdateConcurrencyException geworfen, falls der Datensatz anderweitig geändert wurde, während er im speichernden Client geöffnet war.
Um das alles einfach zu testen, kann man sich hervorragend den Umstand zu Nutze machen, dass zwei verschiedene Objekt-Kontexte auch innerhalb einer Routine denselben Datensatz verändern können, ohne dass der jew. andere Kontext etwas davon mitbekommt. ctxA
und ctxCC
seien diese Kontexte, die beide versuchen, ein Customer
-Objekt (die Nordwind-Datenbank ist nicht tot!) zu speichern:
using (NWindEntities ctxAA = new NWindEntities()) { using (NWindEntities ctxCC = new NWindEntities()) { Customer cuCC = ctxCC.Customers.Where(cu => cu.CustomerID == cuAA.CustomerID).FirstOrDefault(); cuCC.ContactName = new string(cuAA.ContactName.Reverse().ToArray()); ctxCC.SaveChanges(); } ctxAA.SaveChanges(); }
cuAA
ist ein Customer
-Objekt, das bereits geändert wurde. Bevor ctxAA
unseren cuAA
speichert, schnappt sich ctxCC
unseren Customer
mit Hilfe einer weiteren Objektreferenz cuCC
, dreht den ContactName auf links und speichert diese Änderung direkt. Wenn ctxAA
speichern möchte, tritt besagte DbUpdateConcurrencyException auf, die verhindert, dass die Änderungen in der Datenbank überschrieben werden.
Und nun?
Zum einen natürlich ein ordentliches Exception-Handling: Die DbUpdateConcurrencyException wird sauber abgefangen und an die GUI weitergereicht, damit der Benutzer informiert wird. Je nach Anwendungsfall, wobei vielleicht auch Berechtigungskonzepte eine Rolle spielen können, gibt es verschiedene Möglichkeiten:
- Die Änderungen werden verworfen (Database wins).
- Bei einem sauberen Aufbau der Applikation ist hierzu häufig lediglich das Disposen des geänderten Objekts oder des
DbContext
nötig. Anderenfalls würde man im obigen Beispiel mitctx.Entry(cuAA).Reload();
dasCustomer
-Objekt auch zurücksetzen können.
-> Der klassische Weg aus Framework 5 und früher überObjectContext.Refresh(RefreshMode.StorageWins, entity)
steht im Framework 6 nur noch über ein mühsames Casting zur Verfügung, weil die im Framework 6 verwendeteDbContext
-Klasse nicht vonObjectContext
erbt, sondern lediglich dasIObjectContextAdapter
-Interface implementiert.
- Bei einem sauberen Aufbau der Applikation ist hierzu häufig lediglich das Disposen des geänderten Objekts oder des
- Die Änderungen werden dennoch geschrieben (Client wins).
- Damit beim Speichern keine erneute Exception auftritt, muss dem
DbContext
vorgegaukelt werden, die alten und die neuen Werte seien gleich:ctxAA.Entry(cuAA).OriginalValues.SetValues(ctxAA.Entry(cuAA).GetDatabaseValues());
. Ein erneutes Speichern wirft dann keine Exception mehr.
-> Der klassische Weg aus Framework 5 und früher überObjectContext.Refresh(RefreshMode.ClientWins, entity)
steht im Framework 6 nur noch über ein mühsames Casting zur Verfügung, weil die im Framework 6 verwendeteDbContext
-Klasse nicht vonObjectContext
erbt, sondern lediglich dasIObjectContextAdapter
-Interface implementiert.
- Damit beim Speichern keine erneute Exception auftritt, muss dem
- Die Änderungen werden mit den Originalwerten verglichen, der Benutzer kann entscheiden, welche er haben will.
- Erfordert deutlich mehr Coding-Aufwand, dürfte aber die höchste Benutzerfreundlichkeit bieten.