Nehmen wir ein einfaches Beispiel für die "Kontoregistrierung". Hier ist der Ablauf:
- Benutzer besuchen Website
- Klicken Sie auf die Schaltfläche "Registrieren" und füllen Sie das Formular aus. Klicken Sie auf die Schaltfläche "Speichern"
- MVC Controller: Überprüfen Sie die Eindeutigkeit von Benutzernamen, indem Sie aus ReadModel lesen
- RegisterCommand: Überprüfen Sie die Eindeutigkeit des Benutzernamens erneut (hier ist die Frage).
Natürlich können wir die Eindeutigkeit von Benutzernamen überprüfen, indem wir von ReadModel in MVC Controller lesen, um die Leistung und Benutzererfahrung zu verbessern. Allerdings müssen wir noch die Einzigartigkeit wieder in RegisterCommand zu validieren , und natürlich sollten wir nicht den Zugang ReadModel in Befehle.
Wenn wir Event Sourcing nicht verwenden, können wir das Domänenmodell abfragen, sodass dies kein Problem darstellt. Wenn wir jedoch Event Sourcing verwenden, können wir das Domänenmodell nicht abfragen. Wie können wir also die Eindeutigkeit von Benutzernamen in RegisterCommand überprüfen?
Hinweis: Die Benutzerklasse verfügt über eine Id-Eigenschaft, und UserName ist nicht die Schlüsseleigenschaft der User-Klasse. Wir können das Domänenobjekt nur bei Verwendung der Ereignisbeschaffung anhand der ID abrufen.
Übrigens: Wenn der eingegebene Benutzername bereits vergeben ist, sollte auf der Website dem Besucher die Fehlermeldung "Entschuldigung, der Benutzername XXX ist nicht verfügbar" angezeigt werden. Es ist nicht akzeptabel, dem Besucher eine Nachricht mit der Aufschrift "Wir erstellen Ihr Konto, bitte warten Sie, wir senden Ihnen das Registrierungsergebnis später per E-Mail" zu zeigen.
Irgendwelche Ideen? Danke vielmals!
[AKTUALISIEREN]
Ein komplexeres Beispiel:
Anforderung:
Wenn Sie eine Bestellung aufgeben, sollte das System die Bestellhistorie des Kunden überprüfen. Wenn er ein wertvoller Kunde ist (wenn der Kunde im letzten Jahr mindestens 10 Bestellungen pro Monat aufgegeben hat, ist er wertvoll), erhalten Sie 10% Rabatt auf die Bestellung.
Implementierung:
Wir erstellen PlaceOrderCommand und müssen im Befehl den Bestellverlauf abfragen, um festzustellen, ob der Client wertvoll ist. Aber wie können wir das machen? Wir sollten nicht im Befehl auf ReadModel zugreifen! Wie Mikael sagte , können wir im Beispiel für die Kontoregistrierung Kompensationsbefehle verwenden. Wenn wir diese jedoch auch in diesem Bestellbeispiel verwenden, wäre dies zu komplex und der Code möglicherweise zu schwierig zu pflegen.
quelle
Ich denke, Sie müssen noch die Denkweise auf eventuelle Konsistenz und die Art der Event-Beschaffung umstellen. Ich hatte das gleiche Problem. Insbesondere habe ich mich geweigert zu akzeptieren, dass Sie Befehlen des Kunden vertrauen sollten, die in Ihrem Beispiel "Diese Bestellung mit 10% Rabatt aufgeben" sagen, ohne dass die Domain bestätigt, dass der Rabatt erfolgen soll. Eine Sache, die mich wirklich beeindruckt hat, hat Udi selbst zu mir gesagt (siehe Kommentare der akzeptierten Antwort).
Grundsätzlich wurde mir klar, dass es keinen Grund gibt, dem Kunden nicht zu vertrauen. Alles auf der Leseseite wurde aus dem Domänenmodell erstellt, daher gibt es keinen Grund, die Befehle nicht zu akzeptieren. Was auch immer auf der Leseseite steht, dass der Kunde für einen Rabatt qualifiziert ist, wurde von der Domain dort angegeben.
Wenn Sie Event Sourcing und eventuelle Konsistenz übernehmen möchten, müssen Sie akzeptieren, dass es manchmal nicht möglich ist, Fehlermeldungen sofort nach dem Senden eines Befehls anzuzeigen. Mit dem Beispiel für einen eindeutigen Benutzernamen sind die Chancen, dass dies geschieht, so gering (vorausgesetzt, Sie überprüfen die Leseseite, bevor Sie den Befehl senden), dass es sich nicht lohnt, sich über zu viele Gedanken zu machen, aber für dieses Szenario müsste eine nachfolgende Benachrichtigung gesendet werden, oder fragen Sie vielleicht sie für einen anderen Benutzernamen, wenn sie sich das nächste Mal anmelden. Das Tolle an diesen Szenarien ist, dass Sie über den Geschäftswert nachdenken und darüber, was wirklich wichtig ist.
UPDATE: Okt. 2015
Ich wollte nur hinzufügen, dass tatsächlich, wenn es um öffentlich zugängliche Websites geht, die Angabe, dass eine E-Mail bereits vergeben ist, tatsächlich gegen bewährte Sicherheitsmethoden verstößt. Stattdessen sollte die Registrierung erfolgreich abgeschlossen worden sein und den Benutzer darüber informiert haben, dass eine Bestätigungs-E-Mail gesendet wurde. Wenn jedoch der Benutzername vorhanden ist, sollte die E-Mail ihn darüber informieren und ihn auffordern, sich anzumelden oder sein Kennwort zurückzusetzen. Dies funktioniert zwar nur, wenn E-Mail-Adressen als Benutzername verwendet werden, was ich aus diesem Grund für ratsam halte.
quelle
Es ist nichts Falsches daran, einige sofort konsistente Lesemodelle zu erstellen (z. B. nicht über ein verteiltes Netzwerk), die in derselben Transaktion wie der Befehl aktualisiert werden.
Wenn Lesemodelle möglicherweise über ein verteiltes Netzwerk konsistent sind, wird die Skalierung des Lesemodells für schwere Lesesysteme unterstützt. Es gibt jedoch nichts zu sagen, dass Sie kein domänenspezifisches Lesemodell haben können, das sofort konsistent ist.
Das sofort konsistente Lesemodell wird immer nur zum Überprüfen von Daten verwendet, bevor ein Befehl ausgegeben wird. Sie sollten es niemals zum direkten Anzeigen von Lesedaten für einen Benutzer verwenden (dh aus einer GET-Webanforderung oder ähnlichem). Verwenden Sie dazu eventuell konsistente, skalierbare Lesemodelle.
quelle
In Bezug auf die Einzigartigkeit habe ich Folgendes implementiert:
Ein erster Befehl wie "StartUserRegistration". UserAggregate wird unabhängig davon erstellt, ob der Benutzer eindeutig ist oder nicht, jedoch mit dem Status RegistrationRequested.
Bei "UserRegistrationStarted" wird eine asynchrone Nachricht an einen zustandslosen Dienst "UsernamesRegistry" gesendet. wäre so etwas wie "RegisterName".
Der Dienst würde versuchen, die Tabelle zu aktualisieren (keine Abfragen, "Tell Don't Ask"), die eine eindeutige Einschränkung enthalten würde.
Bei Erfolg antwortet der Dienst mit einer anderen Nachricht (asynchron) mit einer Art Autorisierung "UsernameRegistration", die besagt, dass der Benutzername erfolgreich registriert wurde. Sie können eine requestId einfügen, um bei gleichzeitiger Kompetenz den Überblick zu behalten (unwahrscheinlich).
Der Aussteller der obigen Nachricht hat jetzt die Berechtigung, dass der Name selbst registriert wurde, sodass das UserRegistration-Aggregat jetzt sicher als erfolgreich markiert werden kann. Andernfalls als verworfen markieren.
Zusammenfassung:
Dieser Ansatz beinhaltet keine Abfragen.
Die Benutzerregistrierung wird immer ohne Validierung erstellt.
Der Bestätigungsprozess würde zwei asynchrone Nachrichten und eine Datenbankeinfügung umfassen. Die Tabelle ist nicht Teil eines Lesemodells, sondern eines Dienstes.
Schließlich ein asynchroner Befehl, um zu bestätigen, dass der Benutzer gültig ist.
Zu diesem Zeitpunkt könnte ein Denormalisierer auf ein UserRegistrationConfirmed-Ereignis reagieren und ein Lesemodell für den Benutzer erstellen.
quelle
Ich denke, in solchen Fällen können wir einen Mechanismus wie "Beratungssperre mit Ablauf" verwenden.
Beispielausführung:
Sogar Sie können eine SQL-Datenbank verwenden; Geben Sie den Benutzernamen als Primärschlüssel für eine Sperrtabelle ein. und dann kann ein geplanter Job Ablaufzeiten verarbeiten.
quelle
Wie viele andere sind wir bei der Implementierung eines ereignisbasierten Systems auf das Eindeutigkeitsproblem gestoßen.
Zuerst habe ich den Client vor dem Senden eines Befehls auf die Abfrageseite zugreifen lassen, um herauszufinden, ob ein Benutzername eindeutig ist oder nicht. Aber dann habe ich festgestellt, dass es eine schlechte Idee ist, ein Back-End zu haben, das keine Validierung der Eindeutigkeit aufweist. Warum überhaupt etwas erzwingen, wenn es möglich ist, einen Befehl zu veröffentlichen, der das System beschädigen würde? Ein Back-End sollte alle Eingaben überprüfen, sonst sind Sie offen für inkonsistente Daten.
Wir haben eine
index
Tabelle auf der Befehlsseite erstellt. Im einfachen Fall eines Benutzernamens, der eindeutig sein muss, erstellen Sie einfach eine Tabelle user_name_index, die die Felder enthält, die eindeutig sein müssen. Jetzt kann die Befehlsseite die Eindeutigkeit eines Benutzernamens abfragen. Nachdem der Befehl ausgeführt wurde, ist es sicher, den neuen Benutzernamen im Index zu speichern.So etwas könnte auch für das Problem des Bestellrabattes funktionieren.
Die Vorteile sind, dass Ihr Befehls-Backend alle Eingaben ordnungsgemäß überprüft, sodass keine inkonsistenten Daten gespeichert werden können.
Ein Nachteil könnte sein, dass Sie für jede Eindeutigkeitsbeschränkung eine zusätzliche Abfrage benötigen und zusätzliche Komplexität erzwingen.
quelle
Haben Sie darüber nachgedacht, einen "funktionierenden" Cache als eine Art RSVP zu verwenden? Es ist schwer zu erklären, da es in einem kleinen Zyklus funktioniert. Wenn jedoch ein neuer Benutzername "beansprucht" wird (dh der Befehl wurde erteilt, um ihn zu erstellen), platzieren Sie den Benutzernamen mit einem kurzen Ablauf im Cache ( lang genug, um eine weitere Anforderung zu berücksichtigen, die durch die Warteschlange gelangt und in das Lesemodell denormalisiert wird). Wenn es sich um eine Dienstinstanz handelt, würde der Speicher wahrscheinlich funktionieren, andernfalls wird er mit Redis oder Ähnlichem zentralisiert.
Während der nächste Benutzer das Formular ausfüllt (vorausgesetzt, es gibt ein Front-End), überprüfen Sie asynchron das Lesemodell auf Verfügbarkeit des Benutzernamens und benachrichtigen den Benutzer, wenn es bereits vergeben ist. Wenn der Befehl gesendet wird, überprüfen Sie den Cache (nicht das Lesemodell), um die Anforderung zu validieren, bevor Sie den Befehl annehmen (bevor Sie 202 zurückgeben). Wenn sich der Name im Cache befindet, akzeptieren Sie den Befehl nicht. Wenn dies nicht der Fall ist, fügen Sie ihn dem Cache hinzu. Wenn das Hinzufügen fehlschlägt (doppelter Schlüssel, weil Sie von einem anderen Prozess geschlagen wurden), nehmen Sie an, dass der Name vergeben ist, und antworten Sie dem Client entsprechend. Ich glaube nicht, dass zwischen den beiden Dingen viel Gelegenheit für eine Kollision besteht.
Wenn es kein Front-End gibt, können Sie die asynchrone Suche überspringen oder zumindest Ihre API den Endpunkt für die Suche bereitstellen lassen. Sie sollten dem Client sowieso nicht erlauben, direkt mit dem Befehlsmodell zu sprechen, und wenn Sie eine API davor platzieren, können Sie die API als Vermittler zwischen dem Befehl und den gelesenen Hosts einsetzen.
quelle
Es scheint mir, dass das Aggregat hier vielleicht falsch ist.
Wenn Sie im Allgemeinen sicherstellen müssen, dass der zu Y gehörende Wert Z innerhalb der Menge X eindeutig ist, verwenden Sie X als Aggregat. X ist schließlich der Ort, an dem die Invariante tatsächlich existiert (nur ein Z kann in X sein).
Mit anderen Worten, Ihre Invariante ist, dass ein Benutzername möglicherweise nur einmal im Bereich aller Benutzer Ihrer Anwendung angezeigt wird (oder ein anderer Bereich sein kann, z. B. innerhalb einer Organisation usw.), wenn Sie einen aggregierten "ApplicationUsers" haben und senden Wenn Sie den Befehl "RegisterUser" verwenden, sollten Sie in der Lage sein, über das zu verfügen, was Sie benötigen, um sicherzustellen, dass der Befehl gültig ist, bevor Sie das Ereignis "UserRegistered" speichern. (Und natürlich können Sie dieses Ereignis dann verwenden, um die Projektionen zu erstellen, die Sie benötigen, um beispielsweise den Benutzer zu authentifizieren, ohne das gesamte Aggregat "ApplicationUsers" laden zu müssen.
quelle