CQRS
CQRS steht für Command Query Responsibility Segregation und formuliert die Erkenntnis, dass das Abfragen von Daten und Änderungen durch Commands in der Logik getrennt behandelt werden sollte. Denn wenn Aggregate über IDs verknüpft werden (siehe Domain-Driven Design-, dann können diese Typen nicht für Aggregat-übergreifende Abfragen auch innerhalb eines Landes per se verwendet werden. Dieses Muster beachtet Separation of Concerns.
Das Problem verschärft sich, wenn länderübergreifende Abfragen erstellt werden müssen. Siehe hierzu: Länderübergreifende Abfragen.
Aggregat-übergreifende Abfragen im Land
Der eigentliche Ursprung von CQRS ist CQS (Command Query Segregation), das aus der Objektorientierung stammt. Das Prinzip besagt, dass eine Methode entweder ein Command ist, das den Zustand ändert oder eine Query, die den Zustand abfragt, also lesend nutzt. Als einfaches Beispiel betrachten wir die Klasse Würfel
. Klassisch würde man eine Operation int Wirf()
zum Würfeln implementieren. Diese würde dann eine Augenzahl zurückgeben. Gemäß CQS hat der Würfel aber ein Feld int sichtbareAugenzahl
, das über eine Property abgerufen werden kann. Die Methode zum Würfeln hat dann die Signatur void Wirf()
die die Member-Variable sichtbareAugenzahl
bei jedem Aufruf neu berechnet.
CQRS ist somit quasi eine Verallgemeinerung auf Serviceebene, bei der schreibende Logik von lesender getrennt wird und jeweils eigene Typen verwendet werden.
CQRS - Light
Im Folgenden wird die einfachste Implementierung von CQRS innerhalb eines Landes dargestellt, die für viele Anwendungsfälle schon recht weit trägt. Im ersten Fall sollen nur die Daten mehrerer Aggregate des eigenen Landes gelesen werden. Weiter unten wird beschrieben, wie dies auch Land-übergreifend implementiert werden kann.
In diesem Muster wird keine eigene Datenbank, sondern zunächst eine eigene Datenbank-View verwendet, um übergreifende Abfragen zu implementieren. Um es möglichst einfach zu halten, wird das Abfragemodell durch die Contracts implementiert. So ist keine Synchronisation notwendig und das Mapping sehr einfach. In Summe ist die Implementierung trivial.
Das folgende Beispiel zeigt die Implementierung eines Query Service für Bücher.
Entsprechend leichtgewichtig ist die Implementierung:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)] [ErrorHandlerBehavior(typeof(UnhandledFaultAssembler<UnhandledFaultContract>))] public class BuchQueryService : QueryService<BuchStatusInfoContract>, IBuchQueryService { ... public virtual async Task<QueryResponse> QueryAsync(QueryRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } HqlQuery hqlQuery = HqlQuery .SelectAll.From<BuchStatusInfoContract>(); ... IEnumerable<BuchStatusInfoContract> queryResult = await QueryAsync(hqlQuery, request.MaxResults); return new QueryResponse(queryResult.ToList()); } private class BuchQueryRepositoryConfigurator : NHibernateConfigurator { protected override IEnumerable<Type> MappingTypes => new[] { typeof(BuchStatusInfoContractMap) }; } private class BuchStatusInfoContractMap : ClassMap<BuchStatusInfoContract> { public BuchStatusInfoContractMap() { Schema(DatabaseInfo.DatabaseSchema); Table("V_BuchQuery"); // Hier ist eine View angeschlossen Not.LazyLoad(); Id(x => x.Id); Map(x => x.AusgeliehenAm).CustomType<DateTimeOffsetToDateTimeConverter>().Not.LazyLoad(); // DateTimeOffset (DB) => DateTime Map(x => x.AusgeliehenBis).CustomType<DateTimeOffsetToDateTimeConverter>().Not.LazyLoad(); ... } } }
Dies ist die einfachste Implementierung dieses Ansatzes. Weitere Ausbaustufen sind auf dieser Basis einfach abzuleiten. So können separate Tabellen für die Abfrage verwendet werden und die Aktualisierung dieser asynchron erfolgen. Im Allgemeinen werden die Daten in einer sogenannten View-Database extrahiert, was nicht zwangsläufig ein RDMS sein muss. In Schleupen.CS wird allerdings auf häufig in der Literatur zu findendes Event-Sourcing aufgrund zu großer Komplexität verzichtet.
Caching fremder Daten
Das Pattern CQRS-Light kann auch mit Daten anderer Services kombiniert werden. Fremde Länder lösen im allgemeinen Business-Events aus. Diese werden dann lokal gecached und bei Abfragen wie oben beschrieben zurückgegeben. Damit können die Abfragen leichter den Performance-Anforderungen genügen. Die Implementierung kann dann in den oben beschriebenen Ausbaustufen erfolgen.
Wichtig ist hierbei, die Kopplung zwischen den Bausteinen zu beachten. Dementsprechend ist zu überlegen, ob übergreifende Abfragen besser in einer Geschäftsprozesskomponente oder in einem Land zu implementieren sind.