Event-getriebene Systeme, Teil 1

In klassischen Anwendungen mit normalisierter Datenbank wird für Entitäten stets deren aktueller Zustand gespeichert. Das bedeutet im Umkehrschluss, dass der alte Zustand stets zugunsten des neuen überschrieben wird.
Im Gegensatz dazu gibt es event-getriebene Systeme (event sourced systems), bei denen nur Domain-Events, also die Datenänderung von Entitäten, gespeichert werden, nicht die tatsächlichen Werte von Entitäten. Dies kann durchaus Vorteile bezüglich der Flexibilität gegenüber zukünftigen Änderungswünschen haben. Dieser Beitrag erklärt, wie ein eventgetriebenes System funktioniert und welche Vor- und Nachteile es bietet.

Eventgetriebenes System am Beispiel erklärt

Beispiel: Wir haben ein User-Objekt (= Entität) mit den Feldern ID, username, password, email.
Zur Speicherung der Events, die User-Attribute verändern, nutzen wir eine relationale Datenbank mit der Tabelle „user-events“:

  CREATE TABLE `user-events` (  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,  `user_id` INT(10) UNSIGNED NOT NULL,  `field` VARCHAR(255) NOT NULL,  `value` VARCHAR(255) NOT NULL,  PRIMARY KEY (`id`),  KEY `user_id` (`user_id`) ) 

Angenommen unsere Beispiel-Anwendung bietet die Möglichkeit einen Nutzer über die Oberfläche anzulegen. Dazu werden einfach in die Tabelle user-events die Änderungen hinzugefügt:

id user_id field value time
1 1 username Hans 2015-01-01 12:00
2 1 password abcde 2015-01-01 12:00
3 1 email hans@example.org 2015-01-01 12:00

Wenn in der Zwischenzeit Daten geändert werden, wird einfach die Datenänderung hinzugefügt:

id user_id field value time
1 1 username Hans 2015-01-01 12:00
2 1 password abcde 2015-01-01 12:00
3 1 email hans@example.org 2015-01-01 12:00
4 1 email hansi@domain.com 2015-01-02 10:00

In einer anderen Funktion unserer Beispiel-Anwendung soll ein Newsletter an die Nutzer verschickt werden. Dazu werden die aktuellen Daten jedes Nutzers benötigt.
Das kann z.B. folgendermaßen umgesetzt werden:
Es gibt ein UserRepository, das an den Controller „SendNewsletter“ per Dependency Injection übergeben wird. Dieses Repository enthält die Methode findAllUsers(), die alle dem System bekannten Nutzer in Form von User-Objekten mit den aktuellen Usernamen und E-Mail-Adressen zurückliefert. Diese User-Objekte kann der Controller „SendNewsletter“ dann weiter verarbeiten.
Zur Erklärung ist hier der (unvollständige) Code der UserRepository-Klasse:

 
class UserRepository {        
   public function findAllUsers() {             
      $sql = "SELECT evt.user_ID, evt.field, evt.value FROM (
      SELECT MAX(ID) AS maxID 
      FROM `user-events`
      GROUP BY user_id, field) 
      AS newest INNER JOIN `user-events` evt 
      ON newest.maxID=evt.ID";
      $users = [];
      foreach ($this->dbh->query($sql) as $row) { 
         if(!isset($users[$row['user_ID']])) { 
              $users[$row['user_ID']] = new User();
         } 
         $users[$row['user_ID']]->$row['field'] = $row['value'];
      }
      return $users;
   } 
} 

Wir können demzufolge den aktuellen Stand der User-Objekte aus den aufgezeichneten Events jederzeit wiederherstellen.

Vorteil: Flexibilität für neue Funktionen

Irgendwann kommt nun der Product Owner auf die Idee, eine Hinweismeldung einzublenden für Nutzer, die Ihr Passwort in den letzten 3 Monaten nicht geändert haben. Kein Problem, denn wir haben ja die Events gespeichert und wissen, wann jeder User zuletzt sein Passwort geändert hat. Hätten wir nur die einzelnen User-Attribute username, password und email in der Datenbank gespeichert, hätten wir schon hier ein Problem, da der Zeitpunkt der letzten Passwortänderung nicht aufgezeichnet wurde.

An einem anderen Tag möchte der Product Owner eine Übersicht aller Nutzer, die irgendwann mal eine E-Mail-Adresse mit der Endung @example.org angegeben hatten. Kein Problem, denn wir haben ja die komplette Historie. Mit einem klassischen Datenbankschema mit den Spalten username, password und email hätten wir auch hier ein unlösbares Problem, da die Informationen mit jeder Änderung überschrieben werden.

Vorteil: Nachvollziehbarkeit

Ein weiterer Vorteil ist, dass in einem event-gesteuerten System ganz einfach nachvollzogen werden kann, was passiert ist, damit der aktuelle Zustand eingetreten ist. Ein Audit Log ist also fest in das Konzept eingebaut.
Im Umkehrschluss kann ein eventgesteuertes System auch ganz einfach in den Zustand zu einem gegebenen Zeitpunkt wiederhergestellt werden, indem man die Events nur bis zu diesem Zeitpunkt berücksichtigt, wenn die Entitäts-Ojekte erstellt werden.

Faktisch ist das Speichern der einzelnen Events nichts Neues: Zum Beispiel in der Buchhaltung werden alle Geldein- und -ausgänge einzeln aufgezeichnet, sodass man jederzeit nachvollziehen kann, wie sich eine bestimmte Gesamtsumme ergeben hat. Auch im Bankwesen ist dies so: Es werden alle Transaktionen aufgezeichnet, die zum aktuellen Kontostand geführt haben.
Man kann aber eben nicht nur Geldtransaktionen in dieser Art abbilden sondern alle Zustandsänderungen. Ein anderes klassisches Beispiel ist ein Warenkorb. In einem eventgetriebenen System werden die einzelnen Transaktionen aufgezeichnet: Produkt 1 hinzugefügt, Produkt 2 hinzugefügt, Produkt 1 gelöscht. Und wenn man alle Events wieder abspielt, erhält man einen Warenkorb, in dem nur Produkt 2 liegt.

Der entscheidende Unterschied einer event-getriebenen Models gegenüber einem klassischen CRUD-Ansatz ist, dass man nie von Anfang an alle Funktionalitäten kennen kann, die sich in Zukunft mal jemand wünschen wird. Wenn aber jede Änderung des Systems aufgezeichnet wird und man nichts überschreibt oder gar löscht, können neue Funktionen jederzeit auch für Daten-Änderungen aus der Vergangenheit bereit gestellt werden.

Nachteile

Als erstes fällt natürlich auf, dass die Datenbank durch das Aufzeichnen aller Events wesentlich größer wird als bei einem CRUD-System. Ein wirklicher Nachteil ist das aber nicht, da die Daten ja nicht umsonst gespeichert werden, sondern ggf. früher oder später nochmal gebraucht werden.

Natürlich erkauft man sich die gewonnene Flexibilität mit der Auflösung der referenziellen Integrität in der Datenbank (denn Fremdschlüssel sind natürlich in diesem Fall nicht möglich).
Aber an dieser Stelle muss man sich überlegen, wozu eine Datenbank in einer Anwendung eigentlich dient: Sie ist ein Persistenzsystem, mit dem man Daten zwischen mehreren Aufrufen speichern und daraus Objekte wiederherstellen können soll. Welche Werte für bestimmte Datenbankfelder erlaubt sind, ist aber eine Business Rule, also Geschäfts-Logik. Demzufolge sind auch Fremdschlüssel eine Art Logik – und die hat in der Datenbank eigentlich nichts zu suchen.
Die Business Rules werden innerhalb der Anwendung umgesetzt und zwar entweder in den Setter-Methoden der Entitäts-Objekte oder über Value Objects, wobei Value Objects zu bevorzugen sind, da dadurch die Setter-Methode sich auf Ihre Single Responsibility – nämlich das Speichern des übergebenen Parameters – konzentrieren kann, während das Value Object sich um seine Single Responsibility – nämlich das Validieren eines Wertes gegenüber einer Regel – kümmern kann.

Fazit

Event-getriebene Systeme sind eine gute Alternative zum klassischen CRUD-Ansatz bei Anwendungen, bei denen sich Daten über die Zeit ändern und bei denen die Anforderungen nicht von Anfang an zu 100% feststehen bzw. bei denen neue Anforderungen später hinzukommen könnten. Für Anwendungen mit reiner Datenhaltung reicht hingegen ein CRUD-System aus.

PS: Faktisch habe ich in diesem User-Beispiel ein wenig getrickst, da ich die Datenänderungen aufgezeichnet habe und nicht die Events, die zu den Datenänderungen geführt haben. Aber dazu mehr im nächsten Beitrag…



Deprecated: Directive 'allow_url_include' is deprecated in Unknown on line 0