CQRS, EventSourcing zasada działania - kilka pytań

3

Witam wszystkich,

Mam kilka pytań dotyczących CQRS + ES + DDD.

Co do CQRS jego założenie jest proste chodzi o podział zapytań na Commandy który zmieniają stan i nic nie zwracają oraz Query które nie zmieniają stanu i zwracają rezultat. Ponad to dzielimy model danych na model Write i model Read (logicznie i fizycznie), gdzię model Read może być oczywiście znacznie uproszczony architektonicznie.

  1. Jak się zachować w sytuacji kiedy MUSIMY zwrócić wartość do klienta po wykonaniu Commanda. Oczywiście wiem, że zawsze da się tak zaprojektować system aby Command nie musial nic zwracać... ale założmy czysto teoretycznie że chciałbym taką operacje wykonać, jak to zrobić w sensowny sposób? (Do głowy przychodzi mi jeden spobób. Przypadek dodawania usera i zwracania jego identyfikatora. Zamiast generować identyfikator w logice biznesowej moglibyśmy go przekazać w Commandzie (GUID) a następnie zwrócić na poziomie controllera. Jednak ten sposób nie wydaje się elegancki bo w pewnym sensie i tak łamiemy założenie nie zwracania rezultatu...)

  2. Parametryzowanie zapytań.
    W wielu żródłach widziałem poniższy sposób implementowania części Query:

interface QueryResult {
}
interface QueryHandler<T extends QueryResult> { 
	T handle();
}

A co jeżeli chce dodac filtry lub paging do warunków zapytania? .Jak w powyższym przykładzie ten przypadek jest obsługiwany? Czy nie można zrobić po prostu:

interface QueryHandler<T extends QueryResult, K extens QueryParam> {
	T handle(K filters);
}
  1. Aktualizacja modelu Read.
    Założmy że mamy przypadek rejestracji usera w systemie. Po przechwyceniu Commanda przez odpowiedni handler wykonujemy operacje stworzenia nowego usera (User jest agregatem). Rozumiem, że po wykonaniu akcji powinniśmy opublikować event który zostanie złapany przez odpowiedni handler służący do aktualizacji modelu read?
RegisterUserCommand implements Command {
 String login;
 char[] password;
}
UserRegisteredEvent implements Event {
 UserId userId;
 String login;
 char[] password;
}
RegisterCommandHandler implements CommandHandler<RegisterUserCommand> {

 UserRepository userRepo;
 IdGenerator generator;
 DomainEventPublisherFactory domainEventPublisherFactory
	
 void handle(RegisterUserCommand cmd) {
	UserId userId = UserID.createFrom(generator.generate());
	User user = new User(userId, new UserData(cmd.getLogin, cmd.getPassword()));
	userRepo.save(user);	
	domainEventPublisherFactory.instance().publish(new UserRegisteredEvent(userId, user.getLogi(), user.getPassworD()) //mozemy tez opublikowac event w konstruktorze
 }
}
UserRegisteredHandler implements EventHandler<UserRegisteredEvent > {
	
	void handle(UserRegisterdEvent event) {
		//update read model.
	}
}
  1. Odnosząc się do powyższego pseudo kodu. Załóżmy że chciałbym go rozszerzyć o event sourcing, czy w takim wypadku muszę stworzyć nową "tabele" która przechowuję całą historię zdarzeń (wszystkie eventy) aplikacji? Czyli oprócz modelu Read i Write mamy jeszcze historie stanu naszego agregatu oraz dodatkowe handlery służące do zapisu eventów w EventStory? Wychodzi na to, że każdy event powinien mieć przynajmniej 2 EventHandlery?
UserRegisteredHandler implements EventHandler<UserRegisteredEvent >  {
	
	void handle(UserRegisterdEvent event) {
		//update read model.
	}
}
UserRegisteredEventStoreHandler implements EventHandler<UserRegisteredEvent > {
	
	void handle(UserRegisterdEvent event) {
		//update event store
	}
}
1

ad. 1 . Uzupełnie jak będe miał czas.
Czasami można sobie uprościć i zwracać z commend wyniki. Założenie jest takie, że robisz to szybko i masz jakąś defaultową projekcję (in memory). Lagom tak robi. I dośc ten skrót lubię.
ad 2. Na grzyba?

class MyQuery implements QueryHandler {
 private final K filters;
 T handle( ) {
  .....
  }

}
 

add 3 i 4 Nie pomyślałem, żeby zapis do eventów (globalnych dla aplikacji) robić handlerem do aktualizacji modeli.
To robię osobno i niezależnie w publish.
Jak będziesz odtwarzał eventy od nowa to nie chciałbyś żeby na nowo się dopisały do listy eventów. Miałbyś eventowe perpetum mobile.

0
jarekr000000 napisał(a):

ad. 1 . Uzupełnie jak będe miał czas.
Czasami można sobie uprościć i zwracać z commend wyniki. Założenie jest takie, że robisz to szybko i masz jakąś defaultową projekcję (in memory). Lagom tak robi. I dośc ten skrót lubię.
ad 2. Na grzyba?

class MyQuery implements QueryHandler {
 private final K filters;
 T handle( ) {
  .....
  }

}
 

add 3 i 4 Nie pomyślałem, żeby zapis do eventów (globalnych dla aplikacji) robić handlerem do aktualizacji modeli.
To robię osobno i niezależnie w publish.
Jak będziesz odtwarzał eventy od nowa to nie chciałbyś żeby na nowo się dopisały do listy eventów. Miałbyś eventowe perpetum mobile.

ad.1
Ok cierpliwie poczekam bo chętnie się czegoś więcej dowiem na temat tych projekci ;)
ad.2
Chodzi Ci o przekazanie filtrów w konstruktorze rozumiem? To chyba nie bardzo bo przecież QueryHandlery są beanmi i odpowiedni handler jest wywoływany przez QueryDispatcher za pomocą typu T. Więc w jaki sposób miał bym w tym handlerze ustawić filtry które użytkownik przekazał na wejściu? Chyba, że czegoś nie widzę..Jedyna opcja to chyba tworzyć wtedy ręcznie handlery i przekazywać warunki zapytania w konstruktorze i potem dodatkowo dowiązać wymagane komponenty.

ad 3

ok, czyli robisz w skrócie coś takiego?

 UserRegisteredHandler implements EventHandler<UserRegisterdHandler> {
   void handle(UserRegisterEvent event) {
	//update model write
	//update EventStore
   }
 
}

Odnosząc się jeszcze do moim pytań, czy model Write, model Read i EventStore są zawsze niezależnymi bytami?
Czyli:
Model Read wiadomo jest używany do odczytu .
Model Write używamy do operacji biznesowych
A event store używamy jeżeli chcemy przesledzić jakiś proces lub ewentualnie go odtworzyć?

Mam wrażenie, że koncepcyjnie coś mieszam... Weźmy dla przykłądu taką metode (z jakiegoś tutoriala) do pobierania agregatu:

public T GetById<T>(Guid id) where T : class
{
    using (var conn = new SqlConnection(_settings.ConnectionString))
    {
        const string sql = "SELECT * FROM Events WHERE AggregateId=@id";
        var listOfEventData = conn.Query<EventData>(sql, new { id });
        var events = listOfEventData.Select(x => x.DeserializeEvent());
        var aggregate = _factory.Create<T>(events);
        return aggregate;
    }
}

Rozumiem to w taki sposób że agregat jest tworzony na podstawie strumienia eventów które które wystąpiły na tym agregacie (czyli zgodnie z koncepcją nie mamy jednego stanu obiektu tylko serie jego zmian). Więc tak jakby nasz model Write w tym przypadku to EventStore. Z tego by wynikało że mamy albo relacyjną bazkę albo event store..

0
_mario60 napisał(a):

Chodzi Ci o przekazanie filtrów w konstruktorze rozumiem? To chyba nie bardzo bo przecież QueryHandlery są beanmi i odpowiedni handler

Hej. Przepraszam, ale coś sobie zepsułem w planowaniu i nie mam za bardzo czasu, a nie umiem szybko , skrótowo napisać (jakbym był dobry w temacie to bym umiał :-) ).
W zdaniu powyżej zawarty jest poblem. U mnie nie są beanami. Nigdy nie mam w swoim kodzie beanow, żadnych. A jak dostaje od kogoś to w miare możliwości odbeaniam. Z czego Ty korzystasz?

0

moze sprobuj sobie podzielic sobie komunikaty latajace miedzy komponentami na a) commands b) events + wprowadz sobie jakas replikowana kolejke ktora bedzie przyjmowala commandy i rzucala eventami. wtedy z grubsza kazdy inny aktor w systemie bedzie mogl sie do niej podpiac swoim EventHandlerem (do budowania swojego stanu) i powiedzmy CommandProxy (do wszelkich zapytan, subskrybcji etc)

0
jarekr000000 napisał(a):
_mario60 napisał(a):

Chodzi Ci o przekazanie filtrów w konstruktorze rozumiem? To chyba nie bardzo bo przecież QueryHandlery są beanmi i odpowiedni handler

Hej. Przepraszam, ale coś sobie zepsułem w planowaniu i nie mam za bardzo czasu, a nie umiem szybko , skrótowo napisać (jakbym był dobry w temacie to bym umiał :-) ).
W zdaniu powyżej zawarty jest poblem. U mnie nie są beanami. Nigdy nie mam w swoim kodzie beanow, żadnych. A jak dostaje od kogoś to w miare możliwości odbeaniam. Z czego Ty korzystasz?

@jarekr000000 W ramach sprostowania, sposób w jaki ja zaimplementowałem częśc Command wygląda następująco.

Command - Interfejs markerowy określający typ commanda oraz dane wejściowe. Implementacje tego interfejsu to POJO'sy
CommandHandler - Klasy go implementujące to serwisy aplikacyjne dotyczące zawsze jednego Commanda. Każda implementacja CommandHandler jest componentem.
CommandBus - Jest to klasa odpowiedzialna za pobranie odpowiedniego Handlera z kontekstu za pomocą typu Command i uruchomienie go z przekazaniem Commanda. Czyli w skrócie jest tam mapa<command_type, handler> (to o czym pisał @tdudzik).

SpringDispatcher implemetns CommandBus {
 
 Map<Class<?>, String> handlers; //key-command type, value- nazwa implementacji handlera
 BeanFactory beanFactory;
 
 void execute(Command command) {
	Strinh handlerName = handlers.get(getTypeFor(command));
	beanFactory.getBean(handlerName).execute(command)
 }
}

I potem:


ExampleController {
	ComamndBus commandBus;
	
	ResponseEntity<Void> foo1(data) {
		commandBus.execute(new CommandOne(data));
	}
	
	ResponseEntity<Void> foo2(data2) {
		commandBus.execute(new CommandTwo(data2));
	}

}

Uważam że ten sposób jest o tyle dobry że jeżeli chcemy wystawić jakąś usługę udostępniającą naszą logike biznesową to nie musimy w tej usłudze tworzyć referencji do każdego Handlera ale tylko i wyłącznie do CommandBusa który na podstawie odpowiedniego Commanda wykona oczekiwany handler. To samo generalnie tyczy się częsci Query, bo sposób ich obsługi może być analogiczny.
W części Query mój problem polegał na tym sposobie przekazania warunków zapytania. Ale dochodze do wniosku że w tym przypadku najlepiej zrobić QueryHandler który przyjmuje typ Query.

zwykle można w zasadzie bezpośredniow w komendzie pisać kod execute()

Wydaje mi się że to kwestia podziału odpowiedzialności, przecież jak wystawiasz jakąs usługe to nie piszesz logiki w np: kontorlerze ( commanHandler.execute(...) ) tylko delegujesz to wywołanie gdzieś do części biznesowej. Ale to na pewno kwestia projektu albo nie zrozumiałem tego podejścia.. Jeżeli nie robisz handlerów jako komponenty to w jakis sposób uzyskujesz tam dostęp chociaż by do repozytoriów?

0
katelx napisał(a):

moze sprobuj sobie podzielic sobie komunikaty latajace miedzy komponentami na a) commands b) events + wprowadz sobie jakas replikowana kolejke ktora bedzie przyjmowala commandy i rzucala eventami. wtedy z grubsza kazdy inny aktor w systemie bedzie mogl sie do niej podpiac swoim EventHandlerem (do budowania swojego stanu) i powiedzmy CommandProxy (do wszelkich zapytan, subskrybcji etc)

Według mnie to o czym piszez jest ok ale nie chodzi mi o jakis konkretny sposób implementacji, według mnie idea jest w miare prosta i napisać to można na różne sposoby. Głównie chodzi mi o zrozumienie samej koncepcji połączenia CQRS z ES. Najbardziej nurtują mnie kwestie o których wspomniałem w poprzednich postach czyli (ad3 i ad4) + to co poniżej:

Odnosząc się jeszcze do moim pytań, czy model Write, model Read i EventStore są zawsze niezależnymi bytami?
Czyli:
Model Read wiadomo jest używany do odczytu .
Model Write używamy do operacji biznesowych
A event store używamy jeżeli chcemy przesledzić jakiś proces lub ewentualnie go odtworzyć?

Mam wrażenie, że koncepcyjnie coś mieszam... Weźmy dla przykłądu taką metode (z jakiegoś tutoriala) do pobierania agregatu:

public T GetById<T>(Guid id) where T : class
{
using (var conn = new SqlConnection(_settings.ConnectionString))
{
const string sql = "SELECT * FROM Events WHERE AggregateId=@id";
var listOfEventData = conn.Query<EventData>(sql, new { id });
var events = listOfEventData.Select(x => x.DeserializeEvent());
var aggregate = _factory.Create<T>(events);
return aggregate;
}
}
Rozumiem to w taki sposób że agregat jest tworzony na podstawie strumienia eventów które które wystąpiły na tym agregacie (czyli zgodnie z koncepcją nie mamy jednego stanu obiektu tylko serie jego zmian). Więc >tak jakby nasz model Write w tym przypadku to EventStore. Z tego by wynikało że mamy albo relacyjną bazkę albo event store..

0
mario60 napisał(a):
  1. Jak się zachować w sytuacji kiedy MUSIMY zwrócić wartość do klienta po wykonaniu Commanda. [...] Zamiast generować identyfikator w logice biznesowej moglibyśmy go przekazać w Commandzie (GUID) a następnie zwrócić na poziomie controllera. Jednak ten sposób nie wydaje się elegancki bo w pewnym sensie i tak łamiemy założenie nie zwracania rezultatu...)

Ten punkt najbardziej mnie ciekawi. Kluczowe słowa do googlowania to cqrs create id. Wychodzi na to, że zdania uczonych są podzielone. Zwracanie id utworzonego obiektu przez niektórych nie jest uznawane za odstępstwo od CQS. Przeciwne zdanie ma Mark Seeman i wyczerpuje temat tworzenia obiektu, podając kilka metod. Dla purystów jest schemat wygenerowania guid przez klienta, a później wywołanie getFriendlyId(guid), jeżeli naprawdę jest to konieczne.

Z kolei z punktu widzenia użytkownika CQRS jest bez sensu. Użytkownik musi dostać wartość. Po stronie klienta nie aplikujemy reguł CQRS, tylko po stronie aplikacji, projektowania obiektowego. CQS doesn't apply to REST API design, z Marka Seemana, link powyżej.

Ogólnie fajny wątek. Ja zatrzymałem się ze wzorcami na poziomie Gamma i z radością pogłębiam wiedzę :)

2

Uwaga. Jestem raczej leszczem CQRS niż teoretykiem - więc nie prezentuje poniżej wiedzy książkowej. Gorzej. Wywodów takich gości jak Vernon często nie rozumiem (a zwykle rozumiem dopiero jak się na czymś wywale).

Z drugiej strony w sumie niechcący od wielu lat bawie sie rozwiązaniami typu CQRS/ES (zaczęło się od robienia gry sieciowej (specyficzny RTS)- gdzie ten wzorzec sam wychodzi :-), potem mnie wessało ).

I tak moje uwagi:

SpringDispatcher implemetns CommandBus {
 
 Map<Class<?>, String> handlers; //key-command type, value- nazwa implementacji handlera
 BeanFactory beanFactory;
 
 void execute(Command command) {
    Strinh handlerName = handlers.get(getTypeFor(command));
    beanFactory.getBean(handlerName).execute(command)
 }
}

Nie wiem ile razy to widziałem jako implementacje command pattern.
Nienawidziłem tego, jeszcze jak byłem wyznawcą Spring/ JavaEE. (Syf w kodzie duży - zysk ujemny(tu będzie gwiazdka)).

A popatrz na to:

void execute(Command<E> command) {
  .... 
     E  aggregateState = loadeAggregate(uid);
     command.execute(aggregateState);
   ...
 }

Aggreagate uid jest podawany razem z komendą, albo wręcz komenda jest do aggreagatu wysyłana.
Koniec.
Query to samo - tylko dostaje uchwyt na konkretną bazę/projekcję.
Koniec.

(Gwiazdka) Dobra, z tym ujemnym zystkiem do nie do końca. Rozdziałem na CommandData i CommandHandler - w teorii zapewniamy sobie dobrą izolację, bo nikt. np, w konstruktorze do Command nie wrzuci jakiegoś innego aggregate czy czegoś, czego potem nie powinniśmy do modelu przekazać (mętne tłumaczenie, ale jak ktoś się na tym wyrżnie to już wie o co chodzi, nie ma nic fajniejszego niż "niechcący" w ramach Command.execute skorzystać z ReadSide).
Ale:

  • jak się to robi na jakimś Springu to i tak nie masz żadnej izolacji :-) (wszystko wszystkim wstrzykniesz - więc trochę zabawa jak w perfumowanie kupy),
  • izolację lepszą można uzyskać odpowiednio rozdzielając system na projekty i wydzielając interfejsy (jednostki kompilacji (jar-y)) -
    wtedy np. w kodzie gdzie piszemy implementacje Command nie mamy nawet dostępu do elementów Engine ( typu CommandBus), Query i musimy operować tylko na udostępnionych z agregatów/stanów metodach.

Dla równowagi zarówno Axon jak i Lagom korzystają z tego rozdziału na CommandData i CommandHandler. O ile Axon jest "głupi" i po prostu nawet nie chcę mi sie go krytykować. O tyle w Lagom (którego ogólnie lubie) dokładnie to podejście mnie lekko denerwuje. Bardzo nie przeszkadza, ale wyoływanie state() aby sie dostać do aktualnego stanu przypomina mi najgorsze wspomnienia z JavaEE (normalnie jak getCurrentSession() ).

Natomiast uważam, że jak najbardziej ma sens podział na EventData i EventHandler przy event sourcingu. Bo tutaj jednemu eventowi możemy przypisać wiele różnych handlerów.

Filozofowanie

( to tylko moja wizja -może chora)

Ja bym CQRS prosto zdefiniował tak
Command =( S) -> (S', P1,P2... PN) - funkcja przyjmująca pewien Stan (aggregat /stan aggregatu właściwie ) i tworząca nowy stan oraz N projekcji.
Query = funkcja(P) -> R - funkcja działająca na projekcji i dająca pewną odpytywaną wartość.

Jak ograniczamy interakcję z systemem do tych dwóch typów funkcji to mamy łatwiejsze rozpraszanie, skalowalność i względnie prostą historię (seria komend). Łatwo poprawiać wydajnośc (odczytów) przez wprowadzanie nowych projekcji.

CQRS z ES to tylko dodatkowy element pośredni -Eventy
mamy Command => (S) -> E1, E2....EN - funkcja dla danego stanu obiektu tworząca wektor eventów.
mamy Event => S ,P1,P2, P3, ,PN' -> S' , P1'..PN' - funkcja updatujaca ( w praktyce tu jest wektor funkcji (osobna dla kazdej projekcji)
mamy Query => (P) -> R (jak wyzej)

To ma te zlaety co wyżej + super prostą historię.

Jak ktoś ma zmysł matematyczny to zauważy, że nie ma specjalnej różnicy miedzy historią komend, a eventów... więc po co sie bawić?

Ale tu sprawa jest prosta - Komendy widzi klient i klient je tworzy. Jak to z klientami bywa => moga one mieć błędne dane - np. CreateUser("Damianek") (throw NoSuchNameException("Dozwolone są Brajanek, Dżesika"). Jeżeli odtwarzamy system na podstawie komend (Command Sourcing) - to musimy za każdym razem walczyć z walidacjami (i jeszcze paroma podobnymi problemami ).
Eventy natomiast są izolowane od klienta. Eventy to niejako zwalidowana komenda, która juz wiadomo, że się uda i na pewno bedzie działać.
(jesli robimy replikację na podstawie eventów to jeden node - ten który odebrał dane sprawdza je i przetwarza na eventy. a potem wszystkie juz przetwarzają eventy bez sprawdzania - bo wiadomo, że dobre... oszczędzamy).

Co do oryginalnego pytania... przecież klient chce jakieś dane otrzymać - jak to zrobić:
Znam dwa podejścia :

  • Promise/Future -- tak jest w Lagom - jeśli komenda stworzy eventy, a eventy zostaną przetworzone to odpalany jest nasz Promise i możemy sobie zrobić wtedy zapytanie i je zwrócić klientowi. Czyli command engine, eventy działaja asynchronicznie, ale klient w zasadzie może nawet się synchronicznie blokować na tym Promise.
  • aktorskie przegięcie - nie wiem jak to sie nazywa - ale jak już mi odbije i wszystko gwoździe przybijam akkowym młotkiem - to po przetworzeniu eventów (w Akka persistent actor) - wysyłam wyniki jako kolejny message do innego actora. Czyli np. parametrem (polem) komendy jest Referencja (akkowa) do aktora, który ma być powiadomiony. A cześciej klient jest juz zrejestrowany jako obserwer. Nie polecam tego "młotka" - bo łatwo sie pogubić - mnie on sprawia frajdę w amatorskich projekcikach - ale nie używam w niczym co choć troche zbliża się do produkcji. ( wiem jak w pracy z cudzym kodem przydatny jest debugger, a tego sie nie da debuggować).

Kiedyś zrobiłem system w stylu CQRS ... na SQL w trybie INSERT ONLY + widoki (VIEW). To jest taki CQRS/ES dla ubogich duchem, ale całkiem się sprawdza i jest łatwy do ogarnięcia dla początkujących. I dobrze działa z replikacjami baz danych. (Źle działa z małymi dyskami :-) ).

A problem CREATE.
Ten problem ma wiele rozwiązań ( wszystkie złe), łatwo je widać jesli przestanie się utożsamiac aggregate root z wierszem w tabelce.
To moje przykladowe aggregate rooty:

  • wszystkie! nazwy produktów na literę N,
  • wszystkie! produkty na literę N,
  • wszyscy użytkownicy systemu,
  • cały System ( :-) ).

( a i przepraszm, za literówki - ogarniam ten post 20 minut - i nadal nie daję rady doczyścić - znowu mi nawalił podsystemik do tego)

0

Dzięki za szczegółową odpowiedź.

Tylko nadal nie do końca to rozumiem:

void execute(Command<E> command) {
  .... 
     E  aggregateState = loadeAggregate(uid);
     command.execute(aggregateState);
   ...
 }

Czyli co, jest sobie jakaś klasa typu CommandExecutor która wywołuje konkretną strategię Commanda?

Command<E> {
	void execute(E aggreggate); //komenda przyjmuje agregat?
}
CommandExecutor<Command> {
	void execute(Command cmd) {
	    E  aggregateState = loadeAggregate(uid);
		cmd.execute(aggregateState)
	}
}

Ten uuid agregatu jest wtedy przekazany w commandzie tak? W przypadku przekazania do agregatu Commanda to rozumiem że w agregacie sa po prostu różne metodki wystawione na różne Commandy? Implementacja commanda następnie zmienia stan agregatu, zapisuje go a następnine tworzy eventy które mogą posłużyć do aktualizacji modelu read lub do innych celów?

Nienawidziłem tego, jeszcze jak byłem wyznawcą Spring/ JavaEE

Jakiego stosu technlogicznego teraz używasz w projektach typu cqrs/es? Ja dopiero od jakiegoś roku wyznaje Springa ;)

A problem CREATE.
Ten problem ma wiele rozwiązań ( wszystkie złe), łatwo je widać jesli >przestanie się utożsamiac aggregate root z wierszem w tabelce.
To moje przykladowe aggregate rooty:

wszystkie! nazwy produktów na literę N,
wszystkie! produkty na literę N,
wszyscy użytkownicy systemu,
cały System ( :-) ).

A tutaj do czego nawiązujesz bo nie bardzo rozumiem?

1
mario60 napisał(a):
Ten uuid agregatu jest wtedy przekazany w commandzie tak? W przypadku przekazania do agregatu Commanda to rozumiem że w agregacie sa po prostu różne metodki wystawione na różne Commandy? Implementacja commanda następnie zmienia stan agregatu, zapisuje go a następnine tworzy eventy które mogą posłużyć do aktualizacji modelu read lub do innych celów? 

Kanoniczie w ES. Implementacja komend na podstawie stanu tworzy Eventy. Eventy są perzystowane i (poprzez handlery) służa do updatu stanu i do updatu projekcji (read side).

Nienawidziłem tego, jeszcze jak byłem wyznawcą Spring/ JavaEE

Jakiego stosu technlogicznego teraz używasz w projektach typu cqrs/es? Ja dopiero od jakiegoś roku wyznaje Springa ;)

Różnie.

  1. W Pracy najczęściej JavaEE/Spring (ale usuwam z tego ile się da i przerabiam na normalna Javę - zostaje wydmuszka, serwisy / REST na Spring. Reszta normalna ).
    Mam kawałki zrobione w stylu sqlowego CQRS/ES (właśnie np. jako INSERT ONLY baza SQL + widoki). (Tu powód to były odległe centra serwerowe - z replikacją na Oracle MASTER -MASTER. (Czyli jak klient ma pecha i w dwoch centrach jeden rekord będzie updatowany w tym samym momencie to dostajemy konflikt na replikacji. + wiele innych problemów - normalne było, że najpierw przychodziła komenda o kasowaniu dokumentu, a później dopiero dokument). INSERT ONLY rozwiązuje niektóre problemy (kosztem nieczytelnych tabel z powtarzającymi się danymil). Sam nie wiem na ile ten patent jest utrzymywalny - problem techniczny jest rozwiązany, ale czy przyszli utrzymujący ogarną ten pattern i nie wyslą na mnie płatnego zabójcy to nie wiem).

(I tu chyba mam dla Ciebie hint - może jak się zaczynasz bawić z CQRS .. to nie rób całego systemu na tym. Wybierz jeden kawałek i ten zrób (np. można zrobić user management) . Najlepiej to mieć powód jeszcze.)

  1. Też w pracy (ale i w hobby projektach) robię czasem pomniejsze projekty na Prevaylerze :-) (to jakieś raporty z build tooli, demka/POC dla biznesu i tego typu bzdurki) - nic krytycznego. (Chociaż jak nie działa to dostaję zjebki - norma).
    Prevayler to Event sourcing + domyślna projekcja w RAM - zabawne, szybkie i skuteczne. Kawałki robię w CQRS (ale nie jakoś konsekwentnie wszystko). No i dodatkowo Prevayler jest na tyle szybki, że zwracanie danych z komend nie jest żadnym problemem. W prevaylerze jeszcze robie najczęściej skrót i używam Command Sourcingu. (Prevaylera + Ratpack względnie łatwo przemycić na produkcję, bo wymagania żadne - a jak powiesz, że potrzebujesz dockera do tego, to jeszcze wszyscy pomagają :-). Czasem moda się przydaje).

  2. Promowałem jakis czas Lagom, ale nie udało mi się tego wepchnać w pracy jako platformy na produkcje (może kiedyś wlezie - raczej się podoba, ale przeskok mentalny z JavaEE jest za duży). Ale tą platformę polecam jak ktoś chce sie uczyć nowoczesnej javy, CQRS i szuka zastępstwa dla JavaEE. (Mikroserwisy).
    Sam w swoich projektach Lagoma nie używam - bo nie potrzebuję takiej kobyły.

  3. mam jeszcze dziwny projekt na CQRS (Akka actors i persistence jest pod spodem między innymi), ale to hobby/eksperyment na zasadzie - nie wiem co robię, ale jest fajnie. (OK koncept ogólny to CMS/Wiki działający bez serwera). Czasem przez chwilę wiem co robię i daje z tego prezentacje (pewnie jeszcze powrócę). (każdy aggregat to actor (norma) - po zmianie (event) wysyła do obserwatorów, kórymi są projekcje event (i one się updatują). (Projekcje potem są normalnie querowane w GUI np.).

A problem CREATE.

A tutaj do czego nawiązujesz bo nie bardzo rozumiem?

Ktoś wzmiankował w jakimś komentarzy problem Create. Bo to jest zabwany filozoficzny problem w CQRS.
Bo jeśli Command działa na aggreagate. To na jakim aggregate działa "NEW/ CREATE"?

0

@jarekr000000: a jak rozwiązać ten problem, że przychodzą dwie komendy CreateUserCommand z tą samą nazwą użytkownika i druga jest przetwarzana zanim stan został zaktualizowany po otrzymaniu zdarzenia UserCreatedEvent? W ten sposób druga komenda przejdzie walidację, bo użytkownik z taką nazwą rzeczywiście jeszcze nie istnieje.

1
tdudzik napisał(a):

@jarekr000000: a jak rozwiązać ten problem, że przychodzą dwie komendy CreateUserCommand z tą samą nazwą użytkownika i druga jest przetwarzana zanim stan został zaktualizowany po otrzymaniu zdarzenia UserCreatedEvent? W ten sposób druga komenda przejdzie walidację, bo użytkownik z taką nazwą rzeczywiście jeszcze nie istnieje.

Na Akka persistence - problemu nie ma, bo przetwarzanie Komendy i Eventów jest poszeregowane
W ramach obrabiania komendy wywołujesz (synchronizcznie) updateState (eventy) i żadne stare, ani nowe eventy Ci się nie wmieszają.

W przypadku systemu gdzie nie masz takiej silnej serializacji zdarzeń trzeba by był jakoś kompensować.
Ja tu sobię lubię gdybać i mam taki patent - wyobrażam sobie, że system działa w galaktyce (droga mleczna) i mamy dwa centra odległe od siebie o 100 tysięcy lat świetlnych.

Wtedy rozwiązania są (przykładowo) takie:

  • nie można robić konfliktu - musi być guid jako nazwa usera,
  • po wykryciu konfliktu user dalszy od centrum galaktyki dostaje nową (losową) nazwę (wysyłamy mu to mailem z dopiskiem sorry),
  • dopuszczamy nie unikatowość nazwy (przez jakis czas) -tego nie rozważałem jeszcze (aleciekawy case).
0

@jarekr000000: to jeszcze jedno pytanie, jak ten obecny stan powinien być przechowywany w pamięci? Są do tego jakieś biblioteki czy samemu można stworzyć obiekt State czy może jeszcze inaczej?

1

@tdudzik: stan to po prostu obiekt - taki jaki sobie zamodelowałeś. Najłatwiej miec go w pamięci. (Ale może to być stan w jakiejs bazie). I w Lagom i w Akka-persistence to jest w pamięci. Ze wzgledow wydajnościowych jest dobrze jak jest serializowalny (np. do JSON) bo wtedy taki framework może sobie zrzucić snapshot. (W innym przypadku po restarcie systemu musiałby wszystkie eventy od początku wszeświata przetworzyć - co czasem trwa). Praktycznie wszystkie frameworki/biblioteki do ES wspierają snapshoty,

0

Dzięki za wszelkie wyjaśnienia, widzę światelko w tunelu ;) pzdr!

0

Cześć, wszystkim.
W związku z tym wątkiem mam jeszcze kilka wątpliwości...

  1. Jak to jest z tym stanem agregatu w ES

Najłatwiej miec go w pamięci. (Ale może to być stan w jakiejs bazie)

Nie rozumiem jak stan agreagtu może nie być w bazie...a tylko w pamięci? Jak dla mnie miało by to sens jeżeli w systemie może istnieć tylko JEDEN obiekt danego typu ale jak mam przechowywać stany agregatów o różnych id? czegoś tu nie rozumiem ;D

Ja to rozumiem tak, jeżeli mamy jakiś zbiór eventów: E1 -> E2 -> E3 i wykonujemy akcje na danym agregacie to dopiero przed wykonaniem konkretnej akcji ładujemy dany strumień eventów (dla danego id agregatu) do pamięci i odtwarzamy po kolei eventy co powoduje odwtorzenie stanu obiektu, dobrze myślę?

  1. I tutaj dopiero możemy dodatkowo wprowadzić 'snapshoty', czyli nie odtwarzać eventów od wersji 1 tylko od ostatniego snapshotu. Od razu pytanie, czy snapshot traktujemy po prostu jako kolejny event ale w tym przypadku zawierający cały zserializowany graf agregatu? I wtedy taki event może być tworzony np; przez jakiś zewnętrzny proces lub trigger uruchamiany na jakimś warunku (np co N eventów)?
1

Ad1. A co przeszkadza Ci przechowywac wiele obiektow tego samego typu w pamieci? Cos sie w javie zepsuło ostatnio?
Ad 2. Snapshot nie jest eventem. Snapshoty sa niezaleznie przchowywane. Wazne tylko zeby bylo wiadomo ktore eventy nastepują po snapshocie. Snapshotow moze nie byc
Mozna wywalic. Triggerowane moga byc np. Co noc, na zadanie, na command, co iles eventow tez....ale nie na event.

0

Ad1.Ok, czyli przy restarcie systemu ładuje eventy z bazy do pamięci, a przy modyfikacji stanu po prostu dorzucam kolejny event do kontenerka w pamięci oraz zapisuje nowy event w bazie, tak?

Ad2.Więc używając snapshotów łądujemy do pamięci eventy tylko od określonego snapshota..., jeżeli strumień eventów przedstawia zmiane stanu N użytkowników i N artykułów to wtedy trigger po spełnieniu wymaganego warunku tworzy N snapshotów tych użytkowników i N snapshotów artykułów? Czy może mamy jeden całościowy snapshot danych (ale to chyba bez sensu)?

0

Czy trzymanie wszystkiego w pamięci nie jest problemem, jak system się rozrośnie?

1 użytkowników online, w tym zalogowanych: 0, gości: 1