Softwarearchitektur verstehen und entscheiden

Aaron KreisÜber mich
Anwendungsarchitektur

Unit of Work

Änderungen sammeln und gemeinsam speichern

Die Idee und ihr Nutzen

Ein Sachbearbeiter einer Versicherung schließt die Prüfung von Schaden 4711 ab. Die Schadenanwendung der Versicherung muss dafür zwei Dinge speichern: den neuen Status „geprüft“ und eine Notiz mit der Begründung „Deckung bestätigt“. Beides gehört zusammen. Ohne Notiz wüsste bei einer späteren Kontrolle niemand, warum der Schaden als geprüft gilt. Ohne neuen Status bliebe der Fall offen und käme erneut auf die Arbeitsliste. Ein Programmteil der Schadenanwendung sammelt deshalb alle Änderungen des Vorgangs und speichert sie am Ende gemeinsam in einer einzigen Datenbanktransaktion. Eine Datenbanktransaktion ist eine Folge von Änderungen, die die Datenbank entweder vollständig oder gar nicht übernimmt. Scheitert eine der beiden Änderungen, bleibt deshalb keine von beiden gespeichert. Dieser Programmteil heißt Unit of Work, auf Deutsch Arbeitseinheit. Der Name meint: Alle Änderungen eines Vorgangs, hier Status und Notiz, bilden eine Einheit, die nur als Ganzes gespeichert wird.

Bausteine: Status „geprüft“; Notiz „Deckung bestätigt“; Unit of Work: sammelt alle Änderungen eines Vorgangs; Datenbank: übernimmt alles oder nichts. Verbindungen: Status „geprüft“ → Unit of Work; Notiz „Deckung bestätigt“ → Unit of Work; Unit of Work → Datenbank (eine Transaktion). Die Unit of Work sammelt beide Änderungen und speichert sie gemeinsam; scheitert eine, bleibt keine gespeichert.

Wie funktioniert es?

Der Sachbearbeiter setzt in der Schadenanwendung den Status von Schaden 4711 auf „geprüft“. Die Unit of Work vermerkt diese Änderung, schreibt aber noch nichts in die Datenbank. Dann legt der Sachbearbeiter die Notiz „Deckung bestätigt“ an, und die Unit of Work merkt auch die Notiz nur vor. Erst als der Sachbearbeiter auf „Prüfung abschließen“ klickt, beginnt die Unit of Work eine Datenbanktransaktion und schreibt zuerst den Status und dann die Notiz hinein. Beide Schreibvorgänge gelingen. Danach schickt die Unit of Work der Datenbank den Befehl COMMIT, auf Deutsch festschreiben. Erst jetzt sind Status und Notiz zusammen gespeichert, und erst jetzt sehen auch andere Sachbearbeiter und Programme die beiden Änderungen.

Die Schadenanwendung legt für jeden Vorgang eine eigene Unit of Work an. Die Unit of Work führt im Arbeitsspeicher eine Liste der geplanten Änderungen. Darin steht, welche Datensätze geändert und welche neu hinzugefügt werden sollen, bei Schaden 4711 also der geänderte Status und die neue Notiz. Die Programmteile, die den Status ändern und die Notiz anlegen, schreiben nicht selbst in die Datenbank. Diese Programmteile beginnen deshalb keine Transaktion und schicken kein COMMIT. Dass Status und Notiz vollständig oder gar nicht gespeichert werden, stellt die Datenbank mit der Transaktion sicher. Die Unit of Work schreibt nur beide Änderungen in dieselbe Transaktion. Änderungen in einer anderen Datenbank und Aufrufe an einen fremden Dienst gehören nicht zur Transaktion und werden bei einem Fehler nicht zurückgenommen. Ein solcher fremder Dienst ist zum Beispiel ein Zahlungsanbieter. Ob eine Änderung fachlich erlaubt ist, prüft die Unit of Work nicht. Ob Schaden 4711 überhaupt als geprüft markiert werden darf, muss die Schadenanwendung vor dem Speichern prüfen.

Scheitert einer der beiden Schreibvorgänge, schickt die Unit of Work statt COMMIT den Befehl ROLLBACK, auf Deutsch zurückrollen. Die Datenbank verwirft dann alle Änderungen dieser Transaktion. Angenommen, die Unit of Work hat den Status schon in die offene Transaktion geschrieben. Dann stürzt der Server der Schadenanwendung ab, bevor die Notiz geschrieben ist. Weil noch kein COMMIT erfolgt ist, verwirft die Datenbank die offene Transaktion. Schaden 4711 ist danach weiter „offen“ und ohne Notiz, genau wie vorher. Der Sachbearbeiter kann die Prüfung erneut abschließen.

Unit of Work: Status und Notiz gemeinsam speichern oder gemeinsam verwerfen

Beteiligte: Sachbearbeiter: prüft Schaden 4711; Unit of Work: sammelt Änderungen, schreibt sie gemeinsam; Datenbank: Schaden 4711 und Notizen. Ablauf: 1. Der Sachbearbeiter setzt den Status von Schaden 4711 auf „geprüft“. Die Unit of Work vermerkt die Änderung, schreibt aber noch nichts in die Datenbank. 2. Der Sachbearbeiter legt die Notiz „Deckung bestätigt“ an. Die Unit of Work merkt auch die Notiz nur vor. 3. Der Sachbearbeiter klickt auf „Prüfung abschließen“. 4. Nach dem Klick beginnt die Unit of Work eine Datenbanktransaktion. 5. Die Unit of Work schreibt den Status „geprüft“ in die Transaktion. Der Schreibvorgang gelingt. 6. Die Unit of Work schreibt die Notiz „Deckung bestätigt“ in die Transaktion. Auch dieser Schreibvorgang gelingt. 7. Die Unit of Work schickt der Datenbank den Befehl COMMIT. Erst jetzt sind Status und Notiz zusammen gespeichert, und erst jetzt sehen auch andere Sachbearbeiter und Programme die beiden Änderungen. 8. Anderer Ausgang: Die Unit of Work hat den Status schon in die offene Transaktion geschrieben. 9. Der Server der Schadenanwendung stürzt ab, bevor die Notiz geschrieben ist. Die Unit of Work hat noch kein COMMIT geschickt. 10. Die Datenbank verwirft die offene Transaktion. Schaden 4711 ist weiter „offen“ und ohne Notiz. Der Sachbearbeiter kann die Prüfung erneut abschließen.

Wann ist es sinnvoll und wann nicht?

Eine Unit of Work lohnt sich

  • Ein Vorgang erzeugt mehrere Änderungen in derselben Datenbank, die nur gemeinsam einen Sinn ergeben. Ohne Unit of Work schreibt die Schadenanwendung jede Änderung einzeln, sobald sie entsteht. Stürzt die Schadenanwendung zwischen dem Schreiben von Status und Notiz ab oder scheitert der zweite Schreibvorgang, ist nur eine der beiden Änderungen gespeichert.
  • Die Änderungen entstehen nacheinander in verschiedenen Programmteilen und sollen trotzdem zusammen gespeichert werden. Neben Status und Notiz kann ein weiterer Programmteil zum Beispiel die Reserve anpassen. Die Reserve ist der Betrag, den die Versicherung für den Schaden zurücklegt. Ohne Unit of Work müssten sich die Programmteile für Status, Notiz und Reserve absprechen, welcher von ihnen die Transaktion beginnt und festschreibt. Mit Unit of Work übergibt jeder dieser Programmteile seine Änderung an die Unit of Work, und nur die Unit of Work schreibt fest.
  • Andere Sachbearbeiter wollen dieselben Datensätze ändern, eine Kollegin zum Beispiel die Reserve von Schaden 4711. Die Kollegin muss warten, solange die Transaktion läuft. Soll ohne Unit of Work trotzdem alles in einer Transaktion landen, bleibt die Transaktion vom Ändern des Status bis zum Klick auf „Prüfung abschließen“ offen. Weil die Unit of Work alle Änderungen erst am Ende schreibt, bleibt die Transaktion kurz.
  • Die Schadenanwendung nutzt ohnehin eine Programmbibliothek für Datenbankzugriffe. Eine Programmbibliothek ist fertiger Code, den ein Programm einbindet. Viele solche Bibliotheken enthalten eine Unit of Work bereits. Das Entwicklungsteam nutzt dann die eingebaute Unit of Work und muss das Sammeln und gemeinsame Schreiben der Änderungen nicht selbst programmieren.

Eine Unit of Work ist nicht das Richtige

  • Ein Vorgang besteht aus einer einzigen Änderung. Dann schreibt der Programmteil, der die Änderung erzeugt, sie direkt in die Datenbank. Einen einzelnen Schreibvorgang übernimmt die Datenbank ohnehin vollständig oder gar nicht, und die Liste geplanter Änderungen im Arbeitsspeicher entfällt.
  • Ein Vorgang erzeugt alle seine Änderungen in einem einzigen Programmteil. Dann beginnt dieser Programmteil die Datenbanktransaktion selbst und schreibt sie selbst fest. Eine eigene Unit of Work, die die Änderungen erst sammelt, entfällt.
  • Die Änderungen sollen über viele Minuten oder mehrere Bildschirmseiten gesammelt werden. Eine so lange offene Unit of Work belegt Arbeitsspeicher, und die gesammelten Daten veralten in der Zwischenzeit. Dann speichert die Schadenanwendung die Eingaben jeder Bildschirmseite sofort als Entwurf. Erst beim Abschließen sammelt eine Unit of Work die endgültigen Änderungen und speichert sie gemeinsam. So bleibt keine Unit of Work über Minuten offen.
  • Die Änderungen verteilen sich auf mehrere Dienste mit eigenen Datenbanken, etwa wenn die Reserve in einem eigenen Reservedienst mit eigener Datenbank liegt. Eine Datenbanktransaktion gilt nur innerhalb einer Datenbank, und die Unit of Work kann Status, Notiz und Reserve deshalb nicht gemeinsam festschreiben. Dann stimmt ein eigenes Programm, der Koordinator, die Änderungen über die getrennten Datenbanken ab; das heißt Two Phase Commit. Unterstützen die Datenbanken das nicht, baut das Entwicklungsteam den Vorgang als Saga: Die Schadenanwendung speichert Status und Notiz sofort, der Reservedienst speichert die Reserve. Scheitert das Speichern der Reserve, macht ein Ausgleichsschritt Status und Notiz wieder rückgängig.
  • Eine Änderung muss auch einen fremden Dienst erreichen, etwa einen Zahlungsanbieter. Einen direkten Aufruf beim Zahlungsanbieter nimmt ein ROLLBACK nicht zurück. Dann speichert die Unit of Work eine Nachricht an den fremden Dienst in derselben Datenbanktransaktion wie Status und Notiz; das heißt Outbox Pattern. Ein eigener Programmteil versendet die Nachricht danach aus der Datenbank, statt dass die Schadenanwendung den fremden Dienst direkt aufruft. Scheitert die Transaktion, ist auch die Nachricht nicht gespeichert, und der fremde Dienst erhält keinen Auftrag zu einem Schaden, dessen Status unverändert ist.

Nachteile und typische Fehler

Wer eine Bibliothek mit eingebauter Unit of Work verwendet, muss wissen, wann diese Bibliothek tatsächlich in die Datenbank schreibt. Bei der Bibliothek Entity Framework geschieht das zum Beispiel erst beim Aufruf SaveChanges. Sammelt die Unit of Work Änderungen eine Weile, steigt die Wahrscheinlichkeit, dass jemand anderes dieselben Daten inzwischen geändert hat. Für diesen Fall braucht die Schadenanwendung zusätzlich ein Verfahren wie Optimistic Locking: Jeder Datensatz trägt eine Versionsnummer, an der die Schadenanwendung beim Speichern einen veralteten Stand erkennt.

Typische Fehler und woran man sie erkennt:

  • Halb gespeicherter Vorgang: Ein Programmteil schreibt an der Unit of Work vorbei direkt in die Datenbank, etwa den Status sofort beim Ändern. Stürzt der Server danach ab, ist nur dieser Status gespeichert. Erkennbar ist das an Schäden im Status „geprüft“ ohne Begründung.
  • Fremder Aufruf in der Transaktion: Die Schadenanwendung löst eine Zahlung beim Zahlungsanbieter aus, bevor die Unit of Work Status und Notiz gespeichert hat. Scheitert danach das Speichern, nimmt das ROLLBACK die Zahlung nicht zurück. Erkennbar ist das an Zahlungen zu Schäden, deren Status in der Datenbank unverändert ist.
  • Zu lange offene Unit of Work: Die Unit of Work sammelt Änderungen über viele Minuten, und die Schadenanwendung prüft beim Speichern keine Versionsnummer. Eine Kollegin ändert inzwischen die Reserve von Schaden 4711 und speichert. Die Unit of Work hält noch den alten Stand des Datensatzes mit der alten Reserve und schreibt diesen Stand beim Abschließen zurück. Die Änderung der Kollegin ist damit überschrieben. Erkennbar ist das an Änderungen von Kollegen, die nach dem Speichern verschwunden sind.
  • Vergessenes Abschließen: Die Schadenanwendung sammelt die Änderungen, löst das Schreiben am Ende aber nie aus. Nichts wird gespeichert, ohne dass eine Fehlermeldung erscheint. Erkennbar ist das an Sachbearbeitern, die ihre Eingaben nach dem Neuladen nicht mehr finden.
  • Fehlende fachliche Prüfung: Die Unit of Work speichert Status und Notiz gemeinsam, obwohl der Schaden nach den fachlichen Regeln noch nicht als geprüft markiert werden darf. Erkennbar ist das an geprüften Schäden, denen vorgeschriebene Unterlagen fehlen.

Weiterführend

Verwandte Ansätze

  • Repository: stellt den Programmteilen mit den fachlichen Regeln Datensätze bereit, ohne Tabellen und Spalten preiszugeben. Die Unit of Work sammelt die Änderungen an diesen Datensätzen und speichert sie gemeinsam.

Quellen

  • Martin Fowler: Unit of Work: Eintrag im Katalog zum Buch Patterns of Enterprise Application Architecture. Die Unit of Work führt eine Liste aller Objekte, die ein fachlicher Vorgang verändert, und schreibt die Änderungen am Ende gemeinsam in die Datenbank.
  • Microsoft: Design the infrastructure persistence layer: Abschnitt aus dem .NET-Architekturleitfaden, der die Unit of Work als eine Transaktion mit mehreren Änderungen beschreibt und zeigt, dass die Bibliothek Entity Framework sie mit dem Aufruf SaveChanges umsetzt.