CQRS ohne DDD und ohne (oder mit?) ES - Was ist ein Schreibmodell und was ist ein Lesemodell?

11

Soweit ich weiß, besteht die große Idee hinter CQRS darin, zwei verschiedene Datenmodelle für die Verarbeitung von Befehlen und Abfragen zu haben. Diese werden als "Schreibmodell" und "Lesemodell" bezeichnet.

Betrachten wir ein Beispiel für den Klon einer Twitter-Anwendung. Hier sind die Befehle:

  • Benutzer können sich registrieren. CreateUserCommand(string username)emittiertUserCreatedEvent
  • Benutzer können anderen Benutzern folgen. FollowUserCommand(int userAId, int userBId)emittiertUserFollowedEvent
  • Benutzer können Beiträge erstellen. CreatePostCommand(int userId, string text)emittiertPostCreatedEvent

Während ich oben den Begriff "Ereignis" verwende, meine ich nicht "Ereignisbeschaffungsereignisse". Ich meine nur Signale, die Lesemodellaktualisierungen auslösen. Ich habe keinen Event Store und möchte mich bisher auf CQRS selbst konzentrieren.

Und hier sind die Fragen:

  • Ein Benutzer muss die Liste seiner Beiträge sehen. GetPostsQuery(int userId)
  • Ein Benutzer muss die Liste seiner Follower sehen. GetFollowersQuery(int userId)
  • Ein Benutzer muss die Liste der folgenden Benutzer anzeigen. GetFollowedUsersQuery(int userId)
  • Ein Benutzer muss den "Freund-Feed" sehen - ein Protokoll aller Aktivitäten seiner Freunde ("Ihr Freund John hat gerade einen neuen Beitrag erstellt"). GetFriedFeedRecordsQuery(int userId)

Um damit umgehen zu können, muss CreateUserCommandich wissen, ob ein solcher Benutzer bereits existiert. An diesem Punkt weiß ich also, dass mein Schreibmodell eine Liste aller Benutzer haben sollte.

Um damit umgehen zu können, muss FollowUserCommandich wissen, ob BenutzerA bereits BenutzerB folgt oder nicht. An dieser Stelle möchte ich, dass mein Schreibmodell eine Liste aller Benutzer-folgt-Benutzer-Verbindungen enthält.

Und schließlich CreatePostCommandglaube ich nicht, dass ich etwas anderes brauche, weil ich keine Befehle wie habe UpdatePostCommand. Wenn ich diese hätte, müsste ich sicherstellen, dass der Beitrag existiert, also würde ich eine Liste aller Beiträge benötigen. Da ich diese Anforderung nicht habe, muss ich nicht alle Beiträge verfolgen.

Frage 1 : Ist es tatsächlich richtig, den Begriff "Schreibmodell" so zu verwenden, wie ich ihn verwende? Oder steht "Schreibmodell" bei ES immer für "Event Store"? Wenn ja, gibt es eine Trennung zwischen den Daten, die ich für Befehle benötige, und den Daten, die ich für Abfragen benötige?

Um damit fertig zu werden GetPostsQuery, würde ich eine Liste aller Beiträge benötigen. Dies bedeutet, dass mein Lesemodell eine Liste aller Beiträge enthalten sollte. Ich werde dieses Modell beibehalten, indem ich es mir anhöre PostCreatedEvent.

Um beides GetFollowersQueryund zu handhaben GetFollowedUsersQuery, würde ich eine Liste aller Verbindungen zwischen Benutzern benötigen. Um dieses Modell beizubehalten, werde ich es mir anhören UserFollowedEvent. Hier ist eine Frage Nr. 2 : Ist es praktisch in Ordnung, wenn ich hier die Verbindungsliste des Schreibmodells verwende? Oder sollte ich besser ein separates Lesemodell erstellen, da ich es in Zukunft möglicherweise mehr Details benötigen muss als das Schreibmodell?

Um damit fertig zu werden, GetFriendFeedRecordsQuerymüsste ich:

  • Zuhören UserFollowedEvent
  • Zuhören PostCreatedEvent
  • Wissen, welche Benutzer welchen anderen Benutzern folgen

Wenn Benutzer A Benutzer B folgt und Benutzer B Benutzer C folgt, sollten die folgenden Datensätze angezeigt werden:

  • Für Benutzer A: "Ihr Freund Benutzer B hat gerade begonnen, Benutzer C zu folgen."
  • Für Benutzer B: "Sie haben gerade begonnen, Benutzer C zu folgen."
  • Für Benutzer C: "Benutzer B folgt Ihnen jetzt"

Hier ist die Frage Nr. 3 : Welches Modell soll ich verwenden, um die Liste der Verbindungen zu erhalten? Soll ich ein Schreibmodell verwenden? Soll ich das Lesemodell verwenden - GetFollowersQuery/ GetFollowedUsersQuery? Oder sollte ich das GetFriendFeedRecordsQueryModell selbst dazu bringen, das zu handhaben UserFollowedEventund eine eigene Liste aller Verbindungen zu führen?

Andrey Agibalov
quelle
Beachten Sie, dass in CQRS-Systemen das Abfragedatenmodell und das Befehlsdatenmodell möglicherweise Daten aus verschiedenen Datenbanken verbrauchen. Und beide Modelle leben möglicherweise unabhängig voneinander (unterschiedliche Apps). Davon abgesehen lautet die Antwort "abhängig" (wie üblich). Es könnte interessant sein
Laiv

Antworten:

7

Greg Young (2010)

CQRS ist einfach die Erstellung von zwei Objekten, bei denen zuvor nur eines vorhanden war.

Wenn Sie in Bezug auf Bertrand Meyers Command Query Separation denken, können Sie sich vorstellen , dass das Modell zwei unterschiedliche Schnittstellen hat, eine, die Befehle unterstützt, und eine, die Abfragen unterstützt.

interface IChangeTheModel {
    void createUser(string username)
    void followUser(int userAId, int userBId)
    void createPost(int userId, string text)
}

interface IDontChangeTheModel {
    Iterable<Post> getPosts(int userId)
    Iterable<Follower> getFollowers(int userId)
    Iterable<FollowedUser> getFollowedUsers(int userId)
}

class TheModel implements IChangeTheModel, IDontChangeTheModel {
    // ...
}

Greg Youngs Erkenntnis war, dass man dies in zwei getrennte Objekte aufteilen konnte

class WriteModel implements IChangeTheModel { ... }
class ReadModel  implements IDontChangeTheModel {...}

Nachdem Sie die Objekte getrennt haben, haben Sie jetzt die Möglichkeit, die Datenstrukturen zu trennen, die den Objektstatus im Speicher halten, damit Sie für jeden Fall optimieren können. oder den Lesezustand getrennt vom Schreibzustand speichern / beibehalten.

Frage 1: Ist es tatsächlich richtig, den Begriff "Schreibmodell" so zu verwenden, wie ich ihn verwende? Oder steht "Schreibmodell" bei ES immer für "Event Store"? Wenn ja, gibt es eine Trennung zwischen den Daten, die ich für Befehle benötige, und den Daten, die ich für Abfragen benötige?

Unter dem Begriff WriteModel wird normalerweise die veränderbare Darstellung des Modells verstanden (dh das Objekt, nicht der Persistenzspeicher).

TL; DR: Ich denke, Ihre Verwendung ist in Ordnung.

Hier ist eine Frage Nr. 2: Ist es praktisch in Ordnung, wenn ich hier die Verbindungsliste des Schreibmodells verwende?

Das ist "gut" - ish. Konzeptionell ist nichts falsch daran, dass das Lese- und Schreibmodell dieselben Strukturen aufweist.

In der Praxis gibt es möglicherweise ein Problem, wenn ein Thread versucht, den Status des Modells zu ändern, während ein zweiter Thread versucht, ihn zu lesen, da Schreibvorgänge in das Modell normalerweise nicht atomar sind.

Hier ist die Frage Nr. 3: Welches Modell soll ich verwenden, um die Liste der Verbindungen zu erhalten? Soll ich ein Schreibmodell verwenden? Sollte ich das Lesemodell GetFollowersQuery / GetFollowedUsersQuery verwenden? Oder sollte ich das GetFriendFeedRecordsQuery-Modell selbst dazu bringen, das UserFollowedEvent zu handhaben und eine eigene Liste aller Verbindungen zu führen?

Die Verwendung des Schreibmodells ist die falsche Antwort.

Das Schreiben mehrerer Lesemodelle, bei denen jedes auf einen bestimmten Anwendungsfall abgestimmt ist, ist völlig sinnvoll. Wir sagen "das Lesemodell", aber es versteht sich, dass es viele Lesemodelle geben kann, von denen jedes für einen bestimmten Anwendungsfall optimiert ist und die Fälle nicht implementiert, in denen es keinen Sinn ergibt.

Sie können beispielsweise einen Schlüsselwertspeicher zur Unterstützung einiger Abfragen und eine Diagrammdatenbank für andere Abfragen oder eine relationale Datenbank verwenden, in der dieses Abfragemodell sinnvoll ist. Pferde für Kurse.

In Ihrem speziellen Fall, in dem Sie das Muster noch lernen, würde ich vorschlagen, Ihr Design "einfach" zu halten - ein Lesemodell zu haben, das nicht die Datenstruktur des Schreibmodells teilt.

VoiceOfUnreason
quelle
1

Ich möchte Sie ermutigen zu berücksichtigen, dass Sie ein konzeptionelles Datenmodell haben.

Das Schreibmodell ist dann eine Materialisierung dieses Datenmodells, die für Transaktionsaktualisierungen optimiert ist. Manchmal bedeutet dies eine normalisierte relationale Datenbank.

Das Lesemodell ist eine Materialisierung desselben Datenmodells, das für die Ausführung der Abfragen optimiert ist, die Ihre Anwendung (en) benötigen. Möglicherweise handelt es sich weiterhin um eine relationale Datenbank, die jedoch absichtlich denormalisiert wurde, um Abfragen mit weniger Verknüpfungen zu verarbeiten.


(# 1) Für CQRS muss das Schreibmodell kein Ereignisspeicher sein.

(# 2) Ich würde nicht erwarten, dass das Lesemodell etwas speichert, das nicht im Schreibmodell enthalten ist, zum einen, weil in CQRS niemand das Lesemodell aktualisiert, außer dem Weiterleitungsmechanismus, der das Lesemodell mit den Änderungen am synchronisiert Modell schreiben.

(# 3) In CQRS sollten Abfragen gegen das Lesemodell gerichtet sein. Sie können anders vorgehen: Es ist in Ordnung, CQRS nicht zu folgen.


Zusammenfassend gibt es nur ein konzeptionelles Datenmodell. CQRS trennt das Befehlsnetzwerk und die Funktionen vom Abfragenetzwerk und den Funktionen. Angesichts dieser Trennung können das Schreibmodell und das Lesemodell als Leistungsoptimierung mit sehr unterschiedlichen Technologien gehostet werden.

Erik Eidt
quelle