CQRS Event Sourcing: Validate UserName uniqueness

Weźmy prosty przykład "rejestracji konta", oto przepływ:

  • strona odwiedzana przez Użytkownika
  • Kliknij przycisk "Zarejestruj się" i wypełnij formularz, kliknij przycisk "Zapisz"
  • MVC Controller: zweryfikuj unikalność nazwy użytkownika przez odczyt z ReadModel
  • RegisterCommand: ponownie potwierdź unikalność nazwy użytkownika (oto pytanie)

Oczywiście możemy zweryfikować unikalność nazwy użytkownika poprzez odczyt z ReadModel w kontrolerze MVC, aby poprawić wydajność i doświadczenie użytkownika. Jednak nadal musimy ponownie zweryfikować unikalność w RegisterCommand i oczywiście nie powinniśmy uzyskiwać dostępu do ReadModel w poleceniach.

Jeśli nie używamy Event Sourcing, możemy odpytywać model domeny, więc nie ma problemu. Ale jeśli korzystamy z Event Sourcing, nie jesteśmy w stanie odpytywać modelu domeny, więc jak możemy zweryfikować unikalność nazwy użytkownika w RegisterCommand?

Notice: Klasa użytkownika ma właściwość Id, a nazwa użytkownika nie jest kluczowa właściwość klasy User. Obiekt domeny możemy uzyskać tylko po Id podczas korzystania z event sourcing.

BTW: w wymaganiu, jeśli wprowadzona nazwa użytkownika jest już zajęta, strona powinna pokazać komunikat o błędzie "Sorry, the user name XXX is not available" dla odwiedzającego. Nie jest dopuszczalne wyświetlanie wiadomości, powiedzmy, "tworzymy Twoje konto, proszę czekać, wyślemy Ci wynik rejestracji pocztą e-mail później", do odwiedzającego.

Jakieś pomysły? Wiele dzięki!

[UPDATE]

Bardziej złożony przykład:

Wymaganie:

Podczas składania zamówienia system powinien sprawdzić historię zamówień klienta, jeśli jest on cennym klientem( jeśli klient złożył co najmniej 10 zamówień miesięcznie w ciągu ostatniego roku, jest cenny), dokonujemy 10% rabatu na zamówienie.

Realizacja:

Tworzymy polecenie PlaceOrderCommand, a w poleceniu musimy sprawdzić historię zamówień, aby sprawdzić, czy klient jest cenny. Ale jak możemy to zrobić? Nie powinniśmy uzyskiwać dostępu do ReadModel w Komendzie! Jak powiedział Mikael , możemy użyć poleceń kompensacyjnych w przykładzie rejestracji konta, ale jeśli użyjemy tego również w tym przykładzie, byłoby to zbyt skomplikowane, a kod może być zbyt trudny do utrzymania.

Author: Community, 2012-02-29

7 answers

Jeśli zatwierdzisz nazwę Użytkownika za pomocą modelu read przed wysłaniem polecenia, mówimy o oknie warunków wyścigu o wartości kilkuset milisekund, w którym może się zdarzyć prawdziwy stan wyścigu, który w moim systemie nie jest obsługiwany. Jest to po prostu zbyt mało prawdopodobne, aby się zdarzyć w porównaniu do kosztów radzenia sobie z tym.

Jednakże, jeśli czujesz, że musisz się tym zająć z jakiegoś powodu lub jeśli po prostu czujesz, że chcesz wiedzieć, jak opanować taką sprawę, oto jeden ze sposobów: {]}

Nie powinieneś mieć dostępu model odczytu z funkcji obsługi poleceń ani domeny podczas korzystania z event sourcing. Możesz jednak użyć usługi domeny, która nasłuchiwałaby zdarzenia UserRegistered, w którym ponownie uzyskasz dostęp do modelu read I sprawdziłaby, czy nazwa użytkownika nadal nie jest duplikatem. Oczywiście musisz użyć UserGuid tutaj, jak również Twój model odczytu mógł zostać zaktualizowany z użytkownikiem, który właśnie utworzyłeś. Jeśli znaleziono duplikat, masz szansę wysłać polecenia kompensacyjne, takie jak zmiana nazwy użytkownika i powiadomienie użytkownika, że nazwa użytkownika została zajęta.

To jest jedno podejście do problemu.

Jak zapewne widzisz, nie jest możliwe, aby to zrobić w sposób synchroniczny żądanie-odpowiedź. Aby to rozwiązać, używamy SignalR do aktualizacji interfejsu użytkownika, gdy jest coś, co chcemy wcisnąć do Klienta(jeśli nadal są połączone, to znaczy). Co robimy, to pozwalamy klientowi web subskrybować wydarzenia, które zawierają informacje, które są przydatne dla klienta aby zobaczyć natychmiast.

Update

dla bardziej skomplikowanej sprawy:

Powiedziałbym, że złożenie zamówienia jest mniej skomplikowane, ponieważ możesz użyć modelu read, aby dowiedzieć się, czy klient jest wartościowy, zanim wyślesz polecenie. W rzeczywistości możesz zapytać, czy podczas ładowania formularza zamówienia, ponieważ prawdopodobnie chcesz pokazać klientowi, że otrzyma 10% zniżki przed złożeniem zamówienia. Wystarczy dodać rabat do PlaceOrderCommand i być może powód dla rabat, dzięki czemu można śledzić, dlaczego są cięcia zysków.

Ale z drugiej strony, jeśli naprawdę musisz obliczyć rabat po złożeniu zamówienia z jakiegoś powodu, ponownie użyj usługi domeny, która nasłuchiwałaby OrderPlacedEvent i polecenie "kompensujące" w tym przypadku prawdopodobnie byłoby DiscountOrderCommand lub czymś takim. To polecenie wpłynie na kolejność zagregowanego korzenia i informacje mogą być propagowane do odczytywanych modeli.

dla zduplikowanej nazwy użytkownika case:

Możesz wysłać ChangeUsernameCommand jako polecenie kompensujące z usługi domeny. A nawet coś bardziej konkretnego, co opisywałoby powód zmiany nazwy użytkownika, co również mogłoby skutkować utworzeniem zdarzenia, które web client mógłby subskrybować, abyś mógł pozwolić użytkownikowi zobaczyć, że nazwa użytkownika jest duplikatem.

W kontekście usługi domenowej powiedziałbym, że masz również możliwość użycia innych środków powiadamiania użytkownika, takich jak wysyłanie e-mail, który może być przydatny, ponieważ nie możesz wiedzieć, czy użytkownik jest nadal połączony. Być może ta funkcja powiadomień może zostać zainicjowana przez to samo zdarzenie, do którego subskrybuje web client.

Jeśli chodzi o SignalR, używam SignalR Hub, że użytkownicy łączą się, gdy ładują określoną formę. Używam funkcjonalności SignalR Group, która pozwala mi stworzyć grupę, którą nazwę wartością Guid wysyłam w poleceniu. To może być userGuid w Twoim przypadku. Wtedy Ja czy EventHandler, który subskrybuje zdarzenia, które mogą być przydatne dla klienta, a gdy zdarzenie nadejdzie, mogę wywołać funkcję javascript na wszystkich klientach w grupie SignalR (która w tym przypadku byłaby tylko jednym klientem tworzącym zduplikowaną nazwę użytkownika w Twoim przypadku). Wiem, że to brzmi skomplikowanie, ale tak naprawdę nie jest. Na stronie SignalR Github znajdują się świetne dokumenty i przykłady.

 36
Author: Mikael Östberg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-29 12:46:51

Myślę, że jeszcze nie masz zmiany nastawienia do ewentualnej spójności i natury pozyskiwania zdarzeń. Miałem ten sam problem. W szczególności odmówiłem przyjęcia, że powinieneś zaufać poleceniom klienta, które na twoim przykładzie mówią "Złóż to zamówienie z rabatem 10%" bez potwierdzenia domeny, że rabat powinien iść do przodu. Jedną z rzeczy, które naprawdę trafiły do mnie, było coś, co sam Udi powiedział do mnie (Sprawdź komentarze zaakceptowanej odpowiedzi).

W zasadzie zdałem sobie sprawę, że nie ma powodu, aby nie ufać klientowi; wszystko po stronie odczytu zostało wyprodukowane z modelu domeny, więc nie ma powodu, aby nie przyjmować poleceń. Cokolwiek w czytanej stronie, co mówi, że klient kwalifikuje się do Rabatu, zostało tam umieszczone przez domenę.

BTW: w wymaganiu, jeśli wprowadzona nazwa użytkownika jest już zajęta, strona powinna pokazać komunikat o błędzie "Sorry, the user name XXX is not available" dla odwiedzającego. Nie jest dopuszczalne wyświetlanie wiadomości, powiedzmy, "tworzymy Twoje konto, proszę czekać, wyślemy Ci wynik rejestracji pocztą e-mail później", do odwiedzającego.

Jeśli zamierzasz przyjąć event sourcing i ewentualną spójność, musisz zaakceptować, że czasami nie będzie możliwe wyświetlanie komunikatów o błędach natychmiast po wysłaniu polecenia. Z unikalnym przykładem nazwy użytkownika szanse na to są tak małe (biorąc pod uwagę, że sprawdzasz przeczytaną stronę przed wysłaniem polecenie) nie warto martwić się o zbyt wiele, ale kolejne powiadomienie musiałoby być wysłane do tego scenariusza, a może poprosić ich o inną nazwę Użytkownika przy następnym logowaniu. Wspaniałą rzeczą w tych scenariuszach jest to, że sprawia, że myślisz o wartości biznesowej i o tym, co jest naprawdę ważne.

Aktualizacja: Październik 2015

Chciałem tylko dodać, że w rzeczywistości, gdy chodzi o publiczne strony internetowe-wskazanie, że e-mail jest już zajęty jest właściwie przeciwko najlepszym praktykom bezpieczeństwa. Zamiast tego, Rejestracja powinna wyglądać na pomyślne poinformowanie użytkownika o wysłaniu weryfikacyjnej wiadomości e-mail, ale w przypadku, gdy istnieje nazwa użytkownika, e-mail powinien poinformować go o tym i poprosić o zalogowanie się lub zresetowanie hasła. Chociaż działa to tylko podczas używania adresów e-mail jako nazwy użytkownika, co moim zdaniem jest wskazane z tego powodu.

 19
Author: David Masters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:03:05

Nie ma nic złego w tworzeniu od razu spójnych modeli odczytu (np. Nie w rozproszonej sieci), które są aktualizowane w tej samej transakcji co polecenie.

Uzyskanie spójności modeli odczytu w rozproszonej sieci pomaga w skalowaniu modelu odczytu dla ciężkich systemów odczytu. Ale nie ma nic do powiedzenia, że nie możesz mieć specyficznego dla domeny modelu odczytu, który jest natychmiast spójny.

Od razu spójny model odczytu jest używany tylko do sprawdzanie i odbieranie danych przed wydaniem polecenia (tak naprawdę jest to usługa dla polecenia), nigdy nie należy go używać do bezpośredniego wyświetlania odczytywanych danych użytkownikowi(np. z żądania GET web lub podobnego). Użyj ostatecznie consitent, skalowalne modele odczytu do tego.

 11
Author: Gaz_Edge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-24 13:35:15

Podobnie jak wiele innych podczas implementacji systemu opartego na event sourced napotkaliśmy problem unikalności.

Na początku byłem zwolennikiem umożliwienia klientowi dostępu do strony zapytania przed wysłaniem polecenia, aby dowiedzieć się, czy nazwa użytkownika jest unikalna, czy nie. Ale potem zobaczyłem, że posiadanie back-endu, który ma zerową walidację unikalności, jest złym pomysłem. Po co w ogóle egzekwować cokolwiek, skoro można opublikować polecenie, które uszkodziłoby system ? Back-end powinien sprawdzać wszystkie / align = "left" /

Stworzyliśmy index po stronie polecenia. Na przykład, w prostym przypadku nazwy użytkownika, która musi być unikalna, wystarczy utworzyć UserIndex z polem nazwa użytkownika. Teraz strona poleceń może sprawdzić, czy nazwa użytkownika jest już w systemie, czy nie. Po wykonaniu polecenia można bezpiecznie zapisać nową nazwę użytkownika w indeksie.

Coś takiego może również działać na problem z rabatem zamówienia.

The zaletą jest to, że Twój back-end poleceń poprawnie sprawdza wszystkie dane wejściowe, dzięki czemu nie można przechowywać niespójnych danych.

Minusem może być to, że potrzebujesz dodatkowego zapytania dla każdego ograniczenia wyjątkowości i wymuszasz dodatkową złożoność.

 5
Author: Jonas Geiregat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-31 12:41:53

Myślę, że w takich przypadkach możemy użyć mechanizmu jak "advisory lock with expiration".

Przykładowe wykonanie:

  • Sprawdź, czy nazwa użytkownika istnieje lub nie w docelowym spójnym modelu odczytu
  • Jeśli nie istnieje; używając redis-couchbase, takiego jak keyvalue storage lub cache; spróbuj wcisnąć nazwę użytkownika jako pole klucza z pewnym wygaśnięciem.
  • If successful; then raise userRegisteredEvent.
  • Jeśli nazwa użytkownika istnieje w modelu read lub pamięci podręcznej, poinformuj użytkownika, że nazwa użytkownika została zajęta.

Nawet ty możesz użyć bazy danych sql; wstawić nazwę użytkownika jako klucz główny jakiejś tabeli blokad; a następnie zaplanowane zadanie może obsłużyć wygasanie.

 4
Author: Safak Ulusoy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-14 10:23:52

Czy rozważałeś użycie "działającej" pamięci podręcznej jako rodzaj RSVP? Trudno to wyjaśnić, ponieważ działa to trochę w cyklu, ale zasadniczo, gdy nowa nazwa użytkownika jest "potwierdzona" (to znaczy, polecenie zostało wydane, aby ją utworzyć), umieszczasz nazwę użytkownika w pamięci podręcznej z krótkim terminem wygaśnięcia (wystarczająco długo, aby uwzględnić kolejne żądanie przechodzące przez kolejkę i denormalizowane do modelu odczytu). Jeśli jest to jedna instancja usługi, to w pamięci pewnie by zadziałało, w przeciwnym razie scentralizuj ją za pomocą Redis lub coś.

Następnie, gdy następny użytkownik wypełnia formularz (zakładając, że istnieje front end), asynchronicznie sprawdzasz model odczytu pod kątem dostępności nazwy użytkownika i ostrzegasz użytkownika, jeśli jest już zajęty. Po przesłaniu polecenia, sprawdzasz bufor (nie Model odczytu) w celu zweryfikowania żądania przed zaakceptowaniem polecenia( przed zwróceniem 202); jeśli nazwa jest w buforze, nie Akceptuj polecenia, jeśli nie, dodaj ją do bufora; jeśli dodanie nie powiedzie się (zduplikuj klucz, ponieważ jakiś inny proces Cię do niego uprzedził), a następnie przyjmij nazwę-następnie odpowiednio zareaguj na klienta. Między tymi dwoma rzeczami, nie sądzę, że będzie wiele okazji do kolizji.

Jeśli nie ma front endu, możesz pominąć szukanie asynchroniczne lub przynajmniej poprosić API o podanie punktu końcowego, aby go wyszukać. Naprawdę nie powinieneś pozwalać klientowi mówić bezpośrednio do modelu poleceń, a umieszczenie API przed nim pozwoliłoby ci na niech API będzie pośrednikiem między komendą a hostami read.

 1
Author: Sinaesthetic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-30 19:32:18

O wyjątkowości zaimplementowałem:

  • Pierwsze polecenie typu "StartUserRegistration". UserAggregate zostanie utworzony bez względu na to, czy użytkownik jest unikalny, czy nie, ale ze statusem rejestracji wymaganym.

  • Na "UserRegistrationStarted" zostanie wysłana asynchroniczna wiadomość do bezpaństwowej usługi "UsernamesRegistry". byłoby coś w stylu "RegisterName".

  • Serwis będzie próbował zaktualizować (bez zapytań, "powiedz nie pytaj") tabelę, która obejmowałoby unikalne ograniczenie.

  • Jeśli się powiedzie, usługa odpowie inną wiadomością (asynchronicznie), z rodzajem autoryzacji "UsernameRegistration", stwierdzając, że nazwa użytkownika została pomyślnie zarejestrowana. Możesz dołączyć kilka żądań, aby śledzić w przypadku jednoczesnej kompetencji (mało prawdopodobne).

  • Emitent powyższej wiadomości ma teraz autoryzację, że nazwa została zarejestrowana przez siebie, więc teraz może bezpiecznie oznaczyć agregat UserRegistration jako sukcesy W przeciwnym razie Zaznacz jako odrzucone.

:

  • Takie podejście nie wymaga zapytań.

  • Rejestracja użytkownika byłaby zawsze tworzona bez walidacji.

  • Proces potwierdzania wymagałby dwóch asynchronicznych wiadomości i jednej wstawki db. Tabela nie jest częścią modelu odczytu, ale usługi.

  • Na koniec jedno polecenie asynchroniczne, aby potwierdzić, że użytkownik jest poprawny.

  • W tym punkt, denormalizer może zareagować na Zdarzenie UserRegistrationConfirmed i utworzyć model odczytu dla użytkownika.

 0
Author: Daniel Vasquez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-30 00:48:09