CQRS, EventSourcing zasada działania - kilka pytań

CQRS, EventSourcing zasada działania - kilka pytań
M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
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:

Kopiuj
interface QueryResult {
}
Kopiuj
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:

Kopiuj
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?
Kopiuj
RegisterUserCommand implements Command {
 String login;
 char[] password;
}
Kopiuj
UserRegisteredEvent implements Event {
 UserId userId;
 String login;
 char[] password;
}
Kopiuj
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
 }
}
Kopiuj
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?
Kopiuj
UserRegisteredHandler implements EventHandler<UserRegisteredEvent >  {
	
	void handle(UserRegisterdEvent event) {
		//update read model.
	}
}
Kopiuj
UserRegisteredEventStoreHandler implements EventHandler<UserRegisteredEvent > {
	
	void handle(UserRegisterdEvent event) {
		//update event store
	}
}
edytowany 1x, ostatnio: mario60
TD
Wołam @katelx bo podobno lubi cqrs :)
katelx
juz predzej cers (s/query/event/) :)
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
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?

Kopiuj
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.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 2x, ostatnio: jarekr000000
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?

Kopiuj
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?

Kopiuj
 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:

Kopiuj
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..

jarekr000000
Czy pisałeś coś może w Lagom, albo w Axon?
M6
Jeszcze niestety nie..generalnie w temacie siedzie od niedawna i staram się go w wolnej chwili zrozumieć, btw który z tych frameworkow polecasz? Ponad to zastanawiam się nad sensem wykorzystywania frameworków do samego CQRSa który jest w sumie prostym wzorcem, uzasadnienie to ma pewnie w przypadku połączenie ES i CQRS chociaż też się zastanawiam czy po dokładnym jego zrozumieniu wymaga to aż dodatkowego frameworka...
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
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?


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 4x, ostatnio: jarekr000000
Zobacz pozostały 1 komentarz
jarekr000000
@tdudzik - o ile event handlery w Event Sourcingu w zasadzie są konieczne (bo nie wiadomo ile różnych "projekcji" trzeba updatować w przyszłości). To już Command Handlery.. tak sobie - zwykle można w zasadzie bezpośredniow w komendzie pisać kod execute() - (fakt, że mało kto tak robi - nie do końca wiem czemu). Ale Query już zupelnie nie rozumiem po co ? Query w CQRS działa na konkertnych "baza danych" więc nie ma co sobie komplikować.
TD
@jarekr000000: co do handlerów, to kiedy pisałem sobie for fun jakąś grę multiplayer, który wykorzystywała koncept komend i eventów to problemem z komendami bez command handlerów było przekazywanie parametrów. Jeżeli chciałem żeby jakaś komenda miała zależności typu UserRepository i parametry typu userId to przekazywanie tego wszystkiego przez konstruktor byłoby trochę uciążliwe i raczej niezbyt eleganckie. Ułatwieniem byłoby pewnie stworzenie jakiegoś CommandFactory czy coś, ale najbardziej podoba mi się połączenie Command + CommandHandler.
TD
No chyba że coś przeoczyłem. W przypadku Query też widzę podobny problem, jak przekazywać tam zależności i parametry?
jarekr000000
To na czym ten command jest wywołany? :)
TD
W jakim sensie na czym? Command u mnie miał tylko dane do wykonania komendy i wykonywany był przez commandDispatcher właśnie, który zawierał mapę Command -> CommandHandler. Nie wiem czy to odpowiada na Twoje pytanie. :D
katelx
  • Rejestracja:prawie 10 lat
  • Ostatnio:4 miesiące
  • Lokalizacja:Hong Kong
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)

VZ
Chciałem zapytać, czy w tym wypadku, każdy inny aktor podpięty swoim EventHandlerem to osobny wątek działający sobie w aplikacji i reagujący na rzucony event?
VZ
Inna opcją jest to że klasa obslugujaca kolejke z commandami, rzuca eventy do zarejestrowanych subscriberów (aktorów), wtedy ta klasa też działa działa w osobnym wątku i poll'uje cały czas na eventy w kolejce?
katelx
@Vincent_zyx: to troche niefortunnie nazwalam aktorami, bardziej chodzi o moduly aplikacji czy jak to napisales subscriberow. i tak - jakis epoll do tego
M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
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).

Kopiuj
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:

Kopiuj

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?

edytowany 1x, ostatnio: mario60
M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
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..

jarekczek
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad 4 lata
  • Lokalizacja:Siemianowice Śląskie
  • Postów:500
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ę :)


Przeważnie ignoruję niezarejestrowanych użytkowników.
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
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:

Kopiuj
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:

Kopiuj
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)


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 21x, ostatnio: jarekr000000
M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
0

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

Tylko nadal nie do końca to rozumiem:

Kopiuj
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?

Kopiuj
Command<E> {
	void execute(E aggreggate); //komenda przyjmuje agregat?
}
Kopiuj
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?

edytowany 4x, ostatnio: mario60
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
1
mario60 napisał(a):
Kopiuj
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"?


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 7x, ostatnio: jarekr000000
M6
Ok, dzieki za info ;)
TD
  • Rejestracja:ponad 10 lat
  • Ostatnio:ponad 4 lata
  • Postów:380
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.

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
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).

jeden i pół terabajta powinno wystarczyć każdemu
edytowany 5x, ostatnio: jarekr000000
TD
Dzięki, ma to sens
TD
  • Rejestracja:ponad 10 lat
  • Ostatnio:ponad 4 lata
  • Postów:380
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?

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
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,


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
M6
Właśnie ten "stan" też nie do końca rozumiem.. Czy stan obiektu jest ładowany na podstawie strumienia eventów? W sensie, agregat jest "wypełniany" danymi które były odłożone w bazie w postaci eventów? a nie tak jak to jest zazwyczaj na podstawie "jednego" aktualnego rekordu reprezentującego aktualny stan agreagatu? (mam tu na myśli całą graf relacji w bazie)
jarekr000000
@mario60 w ES stan obiektu musi wynikać z sekwencji eventów. Tylko. Tak jak napisałem korzysta sie ze snapshotów, czyli niejako scachowanej wersji - ale to tylko ze wzgledów wydajnościowych. I zawsze powinieneś móc usunać/olać snapshoty i odtworzyć z eventów. Co zresztą się robi jesli zmieniasz poważnie model/graf obiektów.
M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
0

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

M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
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)?
edytowany 2x, ostatnio: mario60
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
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.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
M6
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad rok
  • Postów:13
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)?

mad_penguin
mad_penguin
  • Rejestracja:ponad 10 lat
  • Ostatnio:około 3 lata
  • Lokalizacja:Rzeszów
0

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

M6
Też mi się wydaje, że to może być problem...ale chyba nie do końca rozumiem ten mechanizm
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)