Mai 2016

Band 31, Nummer 5

Innovation – Erstellen eines CRUD-Verlaufssystems

Von Dino Esposito | Mai 2016

Dino EspositoRelationale Datenbanken gibt es seit den 1970-er Jahren, und einige Generationen von Entwicklern haben ihre berufliche Laufbahn begonnen und beendet, ohne einen alternativen Ansatz zur Datenspeicherung kennenzulernen oder überhaupt wohlwollend in Betracht zu ziehen. In jüngster Zeit haben große soziale Netzwerke den überzeugenden Beweis geliefert, dass relationale Datenbanken nicht alle möglichen Geschäftsszenarien abdecken können. Wenn Sie es mit einer (überaus) großen Menge schemaloser Daten zu tun haben sollten, erweisen sich relationale Datenbanken mitunter eher als Engpass denn als Leitung.

Können Sie sich vorstellen, wie groß der Aufwand ist, die Anzahl der „Gefällt mir“-Kommentare von Freunden zu einem Beitrag in einer allumfassenden relationalen Datenbank mit ein paar Milliarden Datensätzen unverzüglich zu bestimmen? Ganz zu schweigen davon, dass das Einschränken der Definition eines Beitrags auf ein starres Schema zumindest anspruchsvoll ist. Um sich vor allem das geschäftliche Überleben zu sichern, verlagerte sich bei sozialen Netzwerken der Fokus bei der Speicherung ab einem gewissen Punkt zu einer Mischung aus relationalen und nicht relationalen Datenspeichern, wodurch das Geschäftsfeld polyglotter Daten offiziell eröffnet wurde.

Die wesentliche Erkenntnis, die aus der Softwarearchitektur sozialer Netzwerke gewonnen wurde, ist, dass manchmal die bloße Speicherung Ihrer Daten (im geschäftlichen Sinne) nicht den idealen Ansatz darstellt. Anstatt also bloß alle Daten zu speichern, die Sie haben, sollten vorzugsweise Details zu einem stattfindenden Ereignis und die Daten gespeichert werden, die am jeweiligen Ereignis beteiligt sind.

In diesem Artikel beschäftige ich mich zuerst mit den Geschäftsgrundlagen des Event-Sourcings (d. h. dem Verwenden protokollierter Ereignisse als primäre Datenquelle von Anwendungen) und erörtere anschließend, wie sich vorhandene CRUD-Kenntnisse vor dem Hintergrund von Ereignissen auf den neuesten Stand bringen lassen. Um eine Sache von vornherein klar zu stellen: die Frage ist nicht, ob Sie Event-Sourcing brauchen. Die Frage ist, wann Sie es brauchen und wie Sie es programmieren.

Entwicklung in Richtung dynamischer Datenmodelle

Das Verwenden polyglotter Daten ist derzeit ein angesagtes Thema: relationale Datenbanken für strukturierte Daten, NoSQL-Datenspeicher für teilstrukturierte Daten, Schlüssel/Wert-Wörterbücher für Voreinstellungen und Protokolle, Graphdatenbanken für Beziehungen und Korrelationen. Die Einführung verschiedener Speichermodelle, die nebeneinander ausgeführt werden, ist ein Schritt in die richtige Richtung. Doch für mich scheint dies eher ein wirksames Gegenmittel für ein sichtbares Symptom als die Heilung der eigentlichen Krankheit zu sein.

Das relationale Modell verdankt seine jahrzehntelange Effektivität einer ausgewogenen Anzahl von Vorteilen, die es beim Lesen und Schreiben von Daten bietet. Ein relationales Modell lässt sich einfach abfragen und aktualisieren, wenngleich unter einigen (sehr) extremem Bedingungen Einschränkungen offenkundig werden. Bei Tabellen mit mehreren Millionen Datensätzen und Hunderten von Spalten lässt die Leistung spürbar nach. Darüber hinaus ist das Schema von Daten festgelegt, und es sind Kenntnisse der Datenbankstruktur erforderlich, um Ad-hoc- und Schnellabfragen zu erstellen. Deshalb stellt die Welt, in der Sie derzeit programmieren (ein umfassendes Modell wie das bereits erwähnte große relationale Modell) eine wesentliche Einschränkung dar, die sich zunächst negativ auf Ihre Ausdrucksmöglichkeiten und später auf Ihre Programmierungsmöglichkeiten auswirkt. Letztendlich ist ein Modell bloß ein Modell und nicht das, was Sie in der realen Welt direkt wahrnehmen. In der realen Welt nehmen Sie keinerlei Modelle wahr. Stattdessen nutzen Sie ein Modell zum Kapseln nachvollziehbarer und wiederholbarer Verhalten. In der realen Welt beobachten Sie schlussendlich Ereignisse, erwarten jedoch das Speichern ereignisbezogener Informationen in einem eingeschränkten (relationalen) Modell. Wenn sich dies als schwierig erweist, untersuchen Sie alternative Speichermodelle, die lediglich einige Schema- und Indizierungseinschränkungen lockern.

Ein ereignisbasiertes Speichermodell

Jahrzehntelang war es hilfreich und wirkungsvoll, bloß den aktuellen Status von Entitäten zu speichern. Wenn Sie einen bestimmten Status speichern, überschreiben Sie den vorhandenen Status, sodass vorherige Informationen verloren gehen. Dieses Verhalten verdient per se weder Lob noch Tadel. Jahrelang hat sich dieser Ansatz als effektiv erwiesen und deshalb weite Verbreitung erlangt. Nur der jeweilige Geschäftsbereich und die Kunden können wirklich sagen, ob der Verzicht auf vorherige Status annehmbar ist. Den Fakten nach war dies über viele Jahre und in den meisten Unternehmen der Fall. Doch der Trend ändert sich, da immer mehr Geschäftsanwendungen das Nachverfolgen des vollständigen Verlaufs von Geschäftsentitäten verlangen. Was jahrelang als CRUD bezeichnet (einfache Create-, Read-, Update- und Delete-Vorgänge) und aufsetzend auf einfachen relationalen Tabellen modelliert wurde, entwickelt sich derzeit zu etwas, das als CRUD-Verlaufssystem bezeichnet wird. Ein CRUD-Verlaufssystem ist einfach eine CRUD-Codebasis, bei deren Implementierung es gelingt, die gesamte Liste der Änderungen ausfindig zu machen.

Die reale Welt ist voll von branchenspezifischen Systemen, die Ereignisse auf gewisse Weise nachverfolgen, während sie im jeweiligen Geschäftsbereich stattfinden. Solche Klassen von Anwendungen gab es jahrzehntelang, von denen einige sogar noch in COBOL oder Visual Basic 6 geschrieben wurden. Zweifelsohne verfolgt z. B. eine Buchhaltungsanwendung alle Änderungen nach, die ggf. an Rechnungen erfolgen, wie beispielsweise eine Änderung des Datums oder der Adresse, das Ausstellen einer Gutschrift u. ä. In einigen Geschäftsszenarien ist die Nachverfolgung von Ereignissen seit den Anfangstagen von Software ein gefragtes Feature, das häufig dem erweiterten Feld der Überwachungsfunktionalität zugerechnet wird.

Aus diesem Grund ist das Überwachen von Geschäftsereignissen im Softwarebereich kein neues Konzept. Jahrzehntelang lösten Entwicklungsteams dasselbe Problem wieder und wieder und haben dabei bekannte Techniken nach besten Kräften über- und wiederaufgearbeitet. Mittlerweile gibt es für die gute alte Praxis der Überwachung von Geschäftsereignissen den einnehmenderen Namen „Event-Sourcing“.

Programmieren Ihres Pfads zu Geschäftsereignissen

Angenommen, Sie haben eine konzeptionell einfache Anwendung, die Benutzern das Buchen einer gemeinsam genutzten Ressource, z. B. eines Besprechungsraums, ermöglicht. Wenn ein Benutzer den Status einer bestimmten Buchung überprüft, wird ihm ggf. nicht bloß der aktuelle Status der Buchung, sondern die gesamte Liste der Aktualisierungen seit Erstellung angezeigt. Abbildung 1 zeigt eine mögliche zeitachsenbasierte Benutzeroberfläche für die Ansicht.

Zeitachsenbasierte Ansicht des Gesamtverlaufs einer Buchung
Abbildung 1: Zeitachsenbasierte Ansicht des Gesamtverlaufs einer Buchung

Wie würden Sie ein Buchungsdatenmodell entwerfen, das sich wie ein CRUD-Verlaufssystem anstatt wie ein einfaches statusbasiertes CRUD-System verhält? Das Hinzufügen weiterer Spalten zur Tabellendefinition reicht nicht aus. Der Hauptunterschied zwischen einem CRUD-System und einem CRUD-Verlaufssystem besteht darin, dass Sie beim letztgenannten Szenario mehrere Kopien derselben Entität speichern möchten, und zwar pro Geschäftsereignis, das zu einem bestimmten Zeitpunkt durchlaufen wurde. Abbildung 2 zeigt eine mögliche neue Struktur für die Buchungstabelle der relationalen Datenbank.

Ein mögliches relationales Datenmodell für eine CRUD-Verlaufsanwendung
Abbildung 2: Ein mögliches relationales Datenmodell für eine CRUD-Verlaufsanwendung

Die in Abbildung 2 gezeigte Tabelle weist die erwartete Gruppe von Spalten auf, die den Status der Geschäftsentität sowie einige andere vollständig abbildet. Als absolutes Minimum gilt eine Primärschlüsselspalte zum eindeutigen Bestimmen einer Zeile in der Tabelle. Als Nächstes benötigen Sie eine Zeitstempelspalte, die entweder den Zeitpunkt des Vorgangs in der Datenbank oder lediglich einen beliebigen Zeitstempel angibt, der geschäftlich sinnvoll ist. Allgemeiner dient die Spalte zum Zuordnen eines sicheren Datums zum Status der Entität. Schließlich wünschen Sie eine Spalte, die das protokollierte Ereignis beschreibt.

Dies ist noch immer eine relationale Tabelle, die weiterhin die Liste der Buchungen verwaltet, die von der Anwendung benötigt wird. Es wurde keine neue Technologie hinzugefügt, doch konzeptionell stellt die in Abbildung 2 schematisierte Tabelle einen Quantensprung im Vergleich mit dem klassischem CRUD-Modell dar. Das Hinzufügen von Datensätzen zur neuen Tabelle ist einfach. Sie füllen lediglich Datensätze aus und fügen sie an, sobald Sie die Benachrichtigung erhalten, dass etwas im System geschehen ist, das nachverfolgt werden muss. Soweit zum „C“ (für Create, dt. Erstellen) in CRUD, doch was ist mit anderen Vorgängen?

Aktualisierungs- und Löschvorgänge in einer CRUD-Verlaufsanwendung

Nachdem die klassische relationale Tabelle in eine verlaufs-, d. h. ereignisbasierte Tabelle umgewandelt wurde, ändern sich Rolle und Relevanz von Aktualisierungs- und Löschvorgängen erheblich. Als Erstes verschwinden Aktualisierungen. Sämtliche Änderungen am logischen Status der Entität werden nun als neuer angefügter Datensatz implementiert, mit dessen Hilfe der neue Status nachverfolgt wird.

Löschvorgänge sind ein heiklerer Punkt, weshalb das letzte Wort zu ihrer Programmierung in der Geschäftslogik der Domäne zu finden ist. In einer idealen ereignisbasierten Welt gibt es keine Löschvorgänge. Daten werden nur noch hinzugefügt, weshalb ein Löschvorgang einfach die Hinzufügung eines neuen Ereignisses ist, das Sie informiert, dass die Entität logisch nicht mehr vorhanden ist. Doch das physische Entfernen von Daten aus einer Tabelle ist nicht gesetzlich verboten und kann weiter erfolgen. Beachten Sie jedoch, dass bei einem ereignisbasierten Szenario die zu entfernende Entität nicht aus einem einzelnen Datensatz, sondern aus einer Sammlung von Datensätzen besteht (siehe Abbildung 2). Wenn Sie sich entschließen, eine Entität zu löschen, müssen Sie alle Ereignisse (und Datensätze) entfernen, die damit verbunden sind.

Lesen des Status einer Entität

Der größte Vorteil beim Protokollieren von Geschäftsereignissen in Ihrer Anwendung ist, dass Sie nichts mehr verpassen. Sie können potenziell den Status des Systems zu jedem gegebenen Zeitpunkt nachverfolgen, die exakte Reihenfolge von Aktionen ausmachen, die zu einem gegebenen Status geführt haben, und diese Ereignisse vollständig oder teilweise rückgängig machen. Dadurch legen Sie die Grundlagen für selbst erstellte Business Intelligence- und Was-wäre-wenn-Szenarien bei Geschäftsanalysen. Um es genauer zu sagen, erhalten Sie diese Features nicht standardmäßig mit Ihrer Anwendung. Doch Sie verfügen bereits über sämtliche Daten, die Sie brauchen, um solche Erweiterungen für die vorhandene Anwendung zu entwickeln.

Der schwierigste Teil eines CRUD-Verlaufssystems bildet das Lesen von Daten. Sie verfolgen derzeit alle relevanten Geschäftsereignisse im Beispielbuchungssystem nach, doch es gibt keine Stelle, an der Sie ohne Probleme die vollständige Liste der bestehenden Buchungen abrufen können. Es gibt keine schnelle und einfache Möglichkeit, um beispielsweise herauszufinden, wie viele Buchungen es für nächste Woche gibt. Hier kommen Projektionen ins Spiel. Abbildung 3 fasst die Gesamtarchitektur eines Systems zusammen, das sich von einem einfachen CRUD- zu einem CRUD-Verlaufssystem entwickelt.

Architektur eines CRUD-Verlaufssystems
Abbildung 3: Architektur eines CRUD-Verlaufssystems

Ein ereignisbasiertes System ist unweigerlich auf die Implementierung einer sauberen Trennung von Befehls- und Abfragestapel ausgelegt. Auf der Darstellungsschicht löst der Benutzer eine Aufgabe aus, die die Anwendungs- und Domänenschicht durchläuft und dabei alle Geschäftslogikkomponenten einbezieht. Ein Befehl ist der Auslöser einer Geschäftsaufgabe, die den aktuellen Status des Systems ändern. Dies bedeutet, das für etwas ein Commit ausgeführt werden muss, das den bestehenden Status logisch ändert. Wie bereits erwähnt, bedeutet bei einem ereignisbasierten System (auch wenn es sich um ein einfaches CRUD-System handelt) das Ändern des Status, dass ein Datensatz hinzugefügt wird, der angibt, dass der Benutzer eine bestimmte Buchung erstellt oder aktualisiert hat. Der Block in Abbildung 3 mit der Bezeichnung „Event Repository“ stellt eine beliebige Codeschicht dar, die für die persistente Speicherung des Ereignisses zuständig ist. Mit Blick auf konkrete Technologien kann der „Event Repository“-Block eine Entity Framework-basierte Repositoryklasse sowie ein Wrapper um eine Dokumentdatenbank (Azure DocumentDB, RavenDB oder MongoDB) sein. Noch interessanter ist, dass es sich um eine Wrapperklasse handeln kann, die die API eines Ereignisspeichers wie EventStore oder NEventStore verwendet.

Bei einer ereignisbasierten Architektur wird der Status einer bestimmten Entität auf Anforderung algorithmisch berechnet. Dieser Vorgang wird als „Ereigniswiedergabe“ bezeichnet und umfasst das Abfragen aller Ereignisse im Zusammenhang mit der angegebenen Entität und deren Anwendung auf eine ganz neue Instanz der Entitätsklasse. Am Ende der Schleife ist die Entitätsinstanz auf dem neuesten Stand, da sie den Status einer neuen Instanz hat, die alle aufgezeichneten Ereignisse durchlaufen hat.

Allgemeiner gesagt, wird durch Verarbeiten des Protokolls der Ereignisse eine Projektion von Daten erstellt und ein dynamisches Datenmodell aus einer Datenmenge auf niedrigerer Ebene extrahiert. Dies wird in Abbildung 3 als Lesemodell bezeichnet. Basierend auf demselben Protokoll von Ereignissen können Sie alle Datenmodelle erstellen, die den verschiedenen Front-Ends dienen. Analog zu SQL entspricht das Erstellen einer Projektion aus Daten aus protokollierten Ereignissen dem Erstellen einer Sicht anhand einer relationalen Tabelle.

Das Wiedergeben von Ereignissen zum Bestimmen des aktuellen Status einer Entität für Abfragezwecke ist allgemein eine geeignete Option. Sie verliert jedoch immer weiter an Effektivität, wenn die Anzahl der Ereignisse oder Häufigkeit von Anforderungen mit der Zeit zunimmt. Sie wollen nicht jedes mehrere Tausend Datensätze durchlaufen, bloß um den aktuellen Kontostand eines Bankkontos zu ermitteln. Ebenso wollen Sie auch nicht Hunderte von Ereignissen durchlaufen, um die Liste ausstehender Buchungen anzuzeigen. Um diese Probleme zu umschiffen, hat das Lesemodell häufig die Form einer klassischen relationalen Tabelle, die programmgesteuert mit der Tabelle protokollierter Ereignisse synchron gehalten wird.

Zusammenfassung

Die meisten Anwendungen können weiter grob als CRUD-Apps kategorisiert werden. Auf dieselbe Weise kann Facebook irgendwie als CRUD-System bezeichnet werden kann, das vielleicht ein bisschen größer als der Durchschnitt ist. Doch Spaß beiseite. Für die meisten Benutzer ist der letzte als funktionierend bekannt Zustand immer noch ausreichend, doch die Zahl der Kunden, für die diese Ansicht unzureichend ist, nimmt zu. Der nächste kann ausgerechnet Ihr bester Kunde sein. In diesem Artikel wurde die Oberfläche eines CRUD-Verlaufssystems nur angekratzt. Nächsten Monat präsentiere ich ein konkretes Beispiel. Bleiben Sie am Ball!


Dino Espositoist Autor von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2014) und „Modern Web Applications with ASP.NET“ (Microsoft Press, 2016). Esposito ist Technical Evangelist für die .NET- und Android-Plattformen bei JetBrains und spricht häufig auf Branchenveranstaltungen weltweit. Auf software2cents.wordpress.com und auf Twitter unter @despos lässt er uns wissen, welche Softwarevision er verfolgt.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Jon Arne Saeteras