DDD & Co., Teil 7: CQRS

the next big thing  –  2 Kommentare

Das Verarbeiten von Kommandos und Erzeugen der fachlichen Ereignisse ist seit der vergangenen Folge in semantisch sinnvollem Code abgebildet. Auf dem Weg lassen sich die Konsistenz und Integrität effizient sicherstellen, auch das Speichern der Ereignisse passt dank Event Sourcing konzeptionell dazu. Wie steht es um das Lesen der Daten?

Aggregate kümmern sich um die Konsistenz der Daten in einem dedizierten Bereich der Anwendung. Sie verarbeiten Kommandos und erzeugen fachliche Ereignisse, die in einem Event Store persistiert werden können. Replays und Snapshots ermöglichen den raschen Zugriff auf alle Ereignisse eines Aggregats.

Das funktioniert zwar, legt den Schwerpunkt aber auf jene Vorgänge, die den internen Zustand der Anwendung verändern. Das gilt aber nicht für jeden Zugriff: Werden ausschließlich Daten gelesen, indem beispielsweise Abfragen ausgeführt werden, wird kein Zustand verändert.

Die Arbeit mit dem Event Store weist außerdem auch einige Probleme auf. So ist der Zugriff auf die fachlichen Ereignisse beispielsweise nur an Hand der ID des Aggregats möglich. Eine Liste aller Aggregate ist zunächst nicht vorgesehen.

Eine solche ist aber in vielen Fällen unabdingbar – beispielsweise um in der TodoMVC-Anwendung eine Liste der offenen oder der bereits erledigten Aufgaben anzuzeigen. In einem Satz lässt sich daher zusammenfassen, dass die bisher vorgestellten Konzepte gut zum Schreiben von Daten passen, jedoch weniger zum Lesen geeignet sind.

Das deckt sich mit dem Entwurfsmuster CQS (Command Query Separation), das von Bertrand Meijer erdacht wurde. Es besagt, dass jede Funktion eines Objekts entweder als Command oder als Query entworfen sein soll.

Der Unterschied zwischen Commands und Queries besteht darin, dass Commands den Zustand eines Objekts verändern und keine Daten zurückgeben, wogegen Queries sich genau gegenteilig verhalten: Sie dienen dazu, den Zustand eines Objekts abzufragen, dürfen ihn aber nicht verändern.

Die bislang vorgestellten Konzepte von DDD machen deutlich, dass sich Aggregate zwar um Kommandos (Commands) kümmern, nicht jedoch um Abfragen (Queries). Das ist wenig verwunderlich, denn Abfragen haben keinen Einfluss auf die transaktionalen Konsistenzgrenzen der Anwendung.

Aus den zuvor genannten Gründen eignet sich ein Event Store allerdings auch kaum für Abfragen. Zwar lassen sich auf Basis der gespeicherten fachlichen Ereignisse zahlreiche Fragen beantworten, das erfordert aber eine aufwierige und zeitaufwändige Analyse. Das mag für Reports akzeptabel sein, aber nicht für den tagtäglichen Betrieb, in dem Daten möglichst performant zur Verfügung stehen müssen.

Die Lösung besteht darin, das zunächst auf einzelne Objekte bezogene Entwurfsmuster CQS auf die gesamte Anwendungsarchitektur zu übertragen. Eine Anwendung besteht dann nicht mehr aus einem einzigen System, das sich um das Schreiben und Lesen von Daten gleichermaßen kümmert. Stattdessen wird sie in zwei Teilsysteme zerlegt, die jeweils auf einen der beiden Vorgänge spezialisiert sind.

Der Ansatz wird als CQRS (Command Query Responsibility Seggregation) bezeichnet. Wie auch beim Event Sourcing gilt, dass CQRS und DDD unabhängig voneinander eingesetzt werden können, sie aber hervorragend zueinander passen.

Die Idee ist, den Event Store um eine zweite Datenbank zu ergänzen, die gezielt auf das Lesen ausgelegt wird. Da die Konsistenz bereits von den Aggregaten in Verbindung mit dem Event Store sichergestellt wird, muss die Lesedatenbank nicht zwingend normalisiert werden. Das bedeutet im Umkehrschluss, dass sie denormalisiert werden darf, wenn das für das effiziente Ausführen von Abfragen sinnvoll ist.

So könnte es beispielsweise eine dedizierte Tabelle geben, die alle offenen Aufgaben der TodoMVC-Anwendung enthält. Die zugehörige Abfrage reduziert sich auf dem Weg auf die folgende, simple SQL-Anweisung:

SELECT * FROM noted_todos

Das gleiche gilt für alle denkbaren Abfragen. Komplizierter als ein einfaches SELECT, höchstens ergänzt um ein WHERE oder ORDER BY, sollten sie nicht sein. Auf die Art entfällt insbesondere jegliche Notwendigkeit, Abfragen mit Hilfe von JOIN über mehrere Tabellen auszuführen.

Das führt zu ausgesprochen effizienten Lesevorgängen. Da die Lesedatenbank zudem nicht für das Sichern der Konsistenz zuständig ist, lässt sie sich problemlos replizieren. Kann eine Instanz nicht länger die gewünschte Leseleistung erbringen, kann das Lesen auf dem Weg praktisch beliebig skaliert werden.

Das alles wirft lediglich die Frage auf, wie die Schreib- und die Leseseite der Anwendung synchronisiert werden. Dazu dient üblicherweise ein eigener Prozess, der auf fachliche Ereignisse reagiert und die Lesetabellen gemäß der gewünschten Interpretation aktualisiert.

Ob ein Ereignis in einer Lesetabelle dabei als INSERT, UPDATE oder DELETE verarbeitet wird, hängt von der jeweiligen Semantik ab. Letztlich kommt hier also tatsächlich wieder CRUD ins Spiel, allerdings in entschärfter Form, da es nur noch dem Aktualisieren der Lesetabellen dient. Die fachliche Konsistenzprüfung findet mit Hilfe der fachlichen Ereignisse und der Aggregate statt.

Dass das Vorgehen Seiteneffekte aufweist, liegt auf der Hand. Immerhin vergeht eine gewisse (wenn auch kurze) Zeit zwischen dem Schreiben der fachlichen Ereignisse im Event Store und dem Aktualisieren der Lesetabellen. Welche Auswirkungen das hat und was dabei zu berücksichtigen ist, wird Thema der nächsten Folge sein.

tl;dr: Das Entwurfsmuster CQRS beschreibt das Trennen der Schreib- von der Leseseite einer Anwendung. CQRS und DDD gehören nicht zwingend zusammen, ergänzen einander aber ausgezeichnet, weshalb sich ihre Kombination in der Praxis häufig anbietet.