Juni 2016

Band 31, Nummer 6

Innovation – Erstellen eines CRUD-Verlaufssystems, Teil 2

Von Dino Esposito | Juni 2016

Dino EspositoAus konzeptioneller Sicht ist ein CRUD-Verlaufssystem (Create, Read, Update, Delete, dt. Erstellen, Lesen, Aktualisieren, Löschen) das klassische CRUD-System, das mit einem zusätzlichen Parameter (einem Datum) erweitert wurde. Ein CRUD-Verlaufssystem ermöglicht Ihnen das Hinzufügen, Aktualisieren und Löschen von Datensätzen in einer Datenbank sowie das Abfragen des Status, den die Datenbank zu einem bestimmten Zeitpunkt hatte. Ein CRUD-Verlaufssystem bietet Ihren Anwendungen eine integrierte Infrastruktur für Business Intelligence- und erweiterte Berichterstellungsfeatures.

In meiner Kolumne vom letzten Monat (msdn.com/magazine/mt703431) habe ich die theoretischen Grundlagen von CRUD-Verlaufssystemen vorgestellt. In diesem Artikel finden Sie eine praktische Demonstration.

Vorstellung des Beispielszenarios

In diesem Artikel beschäftige ich mich mit einem einfachen Buchungssystem. Dabei kann es sich z. B. um das System handeln, das ein Unternehmen intern nutzt, damit Mitarbeiter Besprechungsräume buchen können. Eine solche Software ist schlussendlich ein einfaches CRUD-System, in dem ein neuer Datensatz erstellt wird, um ein Zeitfenster zu reservieren. Dieser Datensatz wird aktualisiert, sobald eine Besprechung auf eine andere Uhrzeit verschoben wird, oder gelöscht, wenn die Besprechung abgesagt wird.

Wenn Sie ein solches Buchungssystem wie ein herkömmliches CRUD-System programmieren, kennen Sie zwar den neuesten Status des Systems, verlieren aber sämtliche Informationen zu aktualisierten oder gelöschten Besprechungen. Ist das wirklich ein Problem? Das kommt ganz darauf an. Es ist wahrscheinlich kein Problem, wenn Sie sich bloß auf den Einfluss von Besprechungen auf das tatsächliche Geschäft beschränken. Wenn Sie jedoch nach Möglichkeiten suchen, die allgemeine Leistung von Mitarbeitern zu verbessern, dann kann ein CRUD-Verlaufssystem zum Nachverfolgen des Aktualisierens und Löschens von Datensätzen ggf. aufdecken, dass Besprechungen viel zu oft verschoben oder abgesagt werden. Dies kann ein Zeichen suboptimaler interner Prozesse oder einer falschen Einstellung sein.

Abbildung 1 zeigt eine realistische Benutzeroberfläche eines Raumbuchungssystems. Die zugrunde liegende Datenbank ist eine SQL Server-Datenbank mit verschiedenen verknüpften Tabellen: Räume und Buchungen.

Die Benutzeroberfläche des Front-Ends eines Buchungssystems
Abbildung 1: Die Benutzeroberfläche des Front-Ends eines Buchungssystems

Die Beispielanwendung ist als ASP.NET MVC-Anwendung konzipiert. Wenn der Benutzer klickt, um die Anforderung zu stellen, schaltet sich eine Controllermethode ein und verarbeitet die gesendeten Informationen. Der folgende Codeausschnitt veranschaulicht den Code zum Verarbeiten der Anforderung auf Serverseite:

[HttpPost]
public ActionResult Add(RoomRequest room)
{
  service.AddBooking(room); 
  return RedirectToAction("index", "home");
}

Die Methode gehört zu einer „BookingController“-Klasse und delegiert den Aufwand der Organisation der tatsächlichen Aufgaben an eine eingeschleuste Worker-Dienstklasse. Ein interessanter Aspekt der Implementierung der Methode ist, dass nach Erstellen der Buchung eine Umleitung zur Startseite in Abbildung 1 erfolgt. Als Resultat des Vorgangs zum Hinzufügen einer Buchung wird keine explizite Sicht erstellt. Dies ist ein Nebeneffekt der Wahl einer CQRS-Architektur (Command and Query Responsibility Segregation, Zuständigkeitstrennung für Befehle und Abfragen). Der Befehl zum Hinzufügen einer Buchung wird an das Back-End übermittelt und ändert den Status des Systems. So weit, so gut. Hätte die Beispielanwendung zum Übermitteln AJAX verwendet, gäbe es keine Notwendigkeit, irgendetwas zu aktualisieren, sodass der Befehl ein eigenständiger Vorgang ohne sichtbare Verknüpfung mit der Benutzeroberfläche wäre.

Der Hauptunterschied zwischen einem klassischen CRUD-System und einem CRUD-Verlaufssystem besteht darin, dass letztgenanntes alle Vorgänge nachverfolgt, die den Status des Systems seit seiner Inbetriebnahme ändern. Zum Planen eines CRUD-Verlaufssystems müssen Sie sich Unternehmensvorgänge als Befehle, die Sie dem System erteilen, und als einen Mechanismus zum Nachverfolgen dieser Befehle vorstellen. Jeder Befehl ändert den Status des Systems, und ein CRUD-Verlaufssystem verfolgt alle Status nach, die das System erreicht. Jeder erreichte Status wird als Ereignis protokolliert. Ein Ereignis ist die bloße und unveränderbare Beschreibung von etwas, das passiert ist. Sobald Sie die Liste der Ereignisse haben, können Sie basierend darauf mehrere Projektionen von Daten erstellen. Die beliebteste davon ist der aktuelle Status beteiligter Unternehmensentitäten.

In einer Anwendung entstammen Ereignisse direkt der Ausführung von Benutzerbefehlen oder indirekt von anderen Befehlen oder externen Eingaben. In diesem Beispielszenario erwarten Sie, dass der Benutzer auf eine Schaltfläche klickt, um eine Buchungsanfrage zu senden.

Verarbeiten des Befehls

Es folgt eine mögliche Implementierung der „AddBooking“-Methode des Controllers der Anwendung:

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  var saga = new BookingSaga();
  var response = saga.AddBooking(command);
  // Do something based on the outcome of the command
}

Die „RoomRequest“-Klasse ist ein einfaches Datenübermittlungsobjekt, das von der ASP.NET MVC-Bindungsschicht mithilfe der gesendeten Daten aufgefüllt wird. Die „RequestBookingCommand“-Klasse speichert hingegen die Eingabeparameter, die zum Ausführen des Befehls erforderlich sind. Bei diesem einfachen Szenario überschneiden sich die beiden Klassen beinahe. Wie würden Sie den Befehl verarbeiten? Abbildung 2 zeigt die drei wesentlichen Schritte zum Verarbeiten eines Befehls.

Die Kette der wichtigsten Schritte zum Verarbeiten eines Befehls
Abbildung 2: Die Kette der wichtigsten Schritte zum Verarbeiten eines Befehls

Der Handler ist die Komponente, die den Befehl empfängt und ihn verarbeitet. Ein Handler kann über einen direkten Aufruf im Speicher im Worker-Dienstcode aufgerufen werden, oder es kann mittendrin einen Bus geben, wie hier gezeigt:

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  // Place the command on the bus for
  // registered components to pick it up
  BookingApplication.Bus.Send(command);
}

Ein Bus kann verschiedene Vorteile bringen. Einer davon ist, dass Sie mühelos Szenarien in den Griff bekommen, in denen mehrere Handler ggf. am selben Befehl interessiert sind. Ein weiterer Vorteil ist, dass ein Bus als zuverlässiges Übermittlungstool konfiguriert werden kann, das die Übermittlung der Nachricht im Verlauf der Zeit und Behebung möglicher Verbindungsprobleme sicherstellt. Darüber hinaus kann ein Bus bloß eine Komponente sein, die die Fähigkeit zum Protokollieren des Befehls bietet.

Der Handler kann eine einfache einmalige Komponente sein, die dieselbe Anforderung startet und beendet. Es kann sich aber auch um einen lange ausgeführten Workflow handeln, der mehrere Stunden oder Tage benötigt und unterbrochen werden kann, um an bestimmten Stellen menschliche Genehmigungen abzuwarten. Handler, die keine Ausführungsinstrumente für einmalige Aufgaben sind, werden auch als „Sagas“ bezeichnet.

Im Allgemeinen verwenden Sie einen Bus oder eine Warteschlange, wenn es bestimmte Anforderungen hinsichtlich Skalierbarkeit und Zuverlässigkeit gibt. Wenn Sie lediglich ein CRUD-Verlaufssystem anstelle eines klassischen CRUD-Systems erstellen möchten, benötigen Sie wahrscheinlich keinen Bus. Ob Bus oder nicht, der Befehl erreicht an einem bestimmten Punkt seinen einmaligen oder lange ausgeführten Handler. Vom Handler wird erwartet, dass die er anstehende Aufgaben erledigt. Bei den meisten Aufgaben handelt es sich um Kernvorgänge in einer Datenbank.

Protokollieren des Befehls

Bei einem klassischen CRUD-System bedeutet das Schreiben von Informationen in eine Datenbank das Hinzufügen eines Datensatzes, der die übergebenen Werte anordnet. Bei einem CRUD-Verlaufssystem stellt dagegen der neu hinzugefügte Datensatz das erstellte Buchungsereignis dar. Das erstellte Buchungsereignis ist eine unabhängige und unveränderbare Information, die einen eindeutigen Bezeichner für das Ereignis, einen Zeitstempel, einen Namen und eine Liste für das Ereignis spezifischer Argumente enthält. Zu den Argumenten eines erstellten Ereignisses zählen üblicherweise alle Spalten, die Sie für einen neu hinzugefügten Buchungsdatensatz in einer klassischen Buchungstabelle ausfüllen würden. Die Argumente eines aktualisierten Ereignisses sind jedoch auf die Felder begrenzt, die tatsächlich aktualisiert werden. Demzufolge weisen alle aktualisierten Ereignisse ggf. nicht denselben Inhalt auf. Die Argumente eines gelöschten Ereignisses sind schließlich auf die Werte begrenzt, die die Buchung eindeutig bestimmen.

Jeder Vorgang in einem CRUD-Verlaufssystem besteht aus zwei Schritten:

  1. Protokollieren des Ereignisses und seiner dazugehörigen Daten.
  2. Sicherstellen, dass der aktuelle Status des Systems sofort und schnell abfragbar ist.

Auf diese Weise ist der aktuelle Status des Systems stets verfügbar und auf dem neuesten Stand, und alle Vorgänge, die zu ihm geführt haben, stehen für weitere Analysen auch zur Verfügung. Beachten Sie, dass der „aktuelle Status des Systems“ lediglich der einzige Status ist, den ein klassisches CRUD-System zeigen kann. Um im Kontext eines einfachen CRUD-Systems effektiv zu sein, muss der Schritt der Protokollierung des Ereignisses und Aktualisieren des Status des Systems synchron und innerhalb derselben Transaktion erfolgen (siehe Abbildung 3).

Abbildung 3: Protokollieren eines Ereignisses und Aktualisieren des Systems

using (var tx = new TransactionScope())
{
  // Create the "regular" booking in the Bookings table   
  var booking = _bookingRepository.AddBooking(
    command.RoomId, ...);
  if (booking == null)
  {
    tx.Dispose();   
    return CommandResponse.Fail;
  }
  // Track that a booking was created
  var eventToLog = command.ToEvent(booking.Id);
    eventRepository.Store(eventToLog);
  tx.Complete();
  return CommandResponse.Ok;
}

Nach Lage der Dinge halten Sie bei jedem Hinzufügen, Bearbeiten oder Löschen eines Buchungsdatensatzes die Gesamtliste der Buchungen auf dem aktuellen Stand und kennen die genaue Abfolge von Ereignissen, die zum aktuellen Status geführt hat. Abbildung 4 zeigt die beiden am Beispielszenario beteiligten SQL Server-Tabellen und ihren Inhalt nach einem Einfüge- und Aktualisierungsvorgang.

Die Tabellen „Bookings“ und „LoggedEvents“ nebeneinander
Abbildung 4: Die Tabellen „Bookings“ und „LoggedEvents“ nebeneinander

Die Tabelle „Bookings“ enthält alle im System gefundenen eindeutigen Buchungen und gibt für jede den aktuellen Status zurück. Die Tabelle „LoggedEvents“ listet alle Ereignisse für die verschiedenen Buchungen in der Reihenfolge ihrer Aufzeichnung auf. Buchung 54 wurde beispielsweise an einem bestimmten Datum erstellt und einige Tage später geändert. Bei diesem Beispiel ist in der Spalte „Cargo“ in der Abbildung der mit JSON serialisierte Datenstrom des ausgeführten Befehls gespeichert.

Verwenden protokollierter Ereignisse auf der Benutzeroberfläche

Angenommen, ein autorisierter Benutzer möchte die Details einer anstehenden Buchung anzeigen. Wahrscheinlich gelangt der Benutzer über eine Kalenderliste oder eine zeitbasierte Abfrage zur Buchung. In beiden Fällen sind die grundlegenden Fakten der Buchung (wann, wie lange und wer) bereits bekannt, weshalb die Detailansicht ggf. nicht besonders nützlich ist. Es kann aber wirklich hilfreich sein, wenn Sie den gesamten Verlauf der Buchung anzeigen könnten, wie in Abbildung 5 gezeigt.

Verwenden protokollierter Ereignisse auf der Benutzeroberfläche
Abbildung 5: Verwenden protokollierter Ereignisse auf der Benutzeroberfläche

Indem Sie die protokollierten Ereignisse durchlesen, können Sie ein Ansichtsmodell erstellen, das eine Liste der Status für dieselbe Gesamtentität enthält: Buchung 54. Wenn der Benutzer in der Beispielanwendung klickt, um die Details einer Buchung einzublenden, wird ein modales Fenster angezeigt und JSON-Code im Hintergrund heruntergeladen. Der Endpunkt, der den JSON-Code zurückgibt, wird hier gezeigt:

public JsonResult BookingHistory(int id)
{
  var history = _service.History(id);
  var dto = history.ToJavaScriptSlotHistory();
  return Json(dto, JsonRequestBehavior.AllowGet);
}

Die „History“-Methode für den Worker-Dienst erledigt hier die meiste Arbeit. Der Hauptteil dieser Arbeit besteht im Abfragen aller Ereignisse im Zusammenhang mit der angegebenen Buchungs-ID:

var events = new EventRepository().All(aggregateId);
foreach (var e in events)
{
  var slot = new SlotInfo();
  switch (e.Action)
  {
    :
  }
  history.Changelist.Add(slot);
}

Beim Durchlaufen der protokollierten Ereignisse in einer Schleife wird das entsprechende Objekt an das zurückzugebende Datenübermittlungsobjekt angefügt. Einige in „ToJavaScriptSlotHistory“ erfolgende Transformationen beschleunigen und vereinfachen das Anzeigen des Unterschieds zwischen zwei aufeinander folgenden Status in der in Abbildung 5 gezeigten Form.

Bemerkenswert ist jedoch, dass wenngleich die Protokollierung von Ereignissen innerhalb eines CRUD-Systems für nützliche Verbesserungen der Benutzeroberfläche sorgt, der größte Nutzen auf der Tatsache basiert, dass Sie über alles Bescheid wissen, was jemals im System passiert ist. Zudem können Sie diese Daten so verarbeiten, dass eine beliebige Projektion von Daten extrahiert werden kann, die Sie an einem bestimmten Punkt ggf. brauchen. Sie können beispielsweise eine Statistik von Aktualisierungen erstellen, mit deren Hilfe Analytiker erkennen, dass der gesamte Prozess des Anforderns von Besprechungsräumen im Unternehmen nicht funktioniert, da die Mitarbeiter zu häufig buchen und anschließend die Buchungen aktualisieren oder löschen. Sie können außerdem problemlos erkennen, wie die Buchungssituation an einem bestimmten Tag war, indem Sie einfach protokollierte Ereignisse bis zu diesem Datum abfragen und den nachfolgenden Stand der Dinge berechnen. Kurz gesagt, eröffnet ein CRUD-Verlaufssystem für Anwendungen ganz neue Möglichkeiten.

Zusammenfassung

Ein CRUD-Verlaufssystem ist schlicht eine intelligentere Möglichkeit zur Entwicklung einfacher CRUD-Anwendungen. Dennoch wurden bei dieser Diskussion Schlagworte und Muster gestreift, die wesentlich mehr Potenzial aufweisen, wie z. B. CQRS, Event-Sourcing, Bus und Warteschlangen sowie nachrichtenbasierte Geschäftslogik. Wenn Sie diesen Artikel hilfreich fanden, empfehle ich Ihnen die Lektüre meiner Kolumnen vom Juli 2015 (msdn.com/magazine/mt238399) und August 2015 (msdn.com/magazine/mt185569). In Anbetracht dieses Beispiels finden Sie diese Artikel ggf. noch anregender.


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 twitter.com/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