Cześć! Tematyka ostatnich wpisów w Cesarstwie-Dev to istna sinusoida pomiędzy tematami wysokopoziomowymi a implementacyjnymi. Ostatnio poruszyliśmy temat języku biznesu. Dzisiaj skupimy się na implementacji jednego z popularniejszych wzorców architektonicznych. Większość z nas wie czym jest CQRS. W końcu temat ten jest poruszany niemal na każdej konferencji! Prezentacje te jednak często nie zawierają przykłady w kodzie, które głównie interesują żądnych technicznych detali programistów. Mam nadzieję, że ten wpis ich dostarczy! Zapraszam Was do wędrówki od krótkiego wstępu teoretycznego do samej implementacji. Zapraszam!

Alberto diagram of CQRS
https://sbg.technology/2015/04/14/ddd-cqrs-alberto-brandolini/

Czym jest wzorzec CQRS?

Wzorce, według definicji, powinny być rozwiązaniem pewnego problemu. Od niego więc zacznijmy opisywanie! Wyobraźmy sobie sytuację, w której interesariusze proszą nas o przygotowanie strony prezentującej liczne dane na temat produktu. W naszej bazie danych informacje te są rozsiane po licznych tabelach, co wymusza na stosowanie wielu operacji złączeń. W związku z tym odpytywanie bazy danych o wszystkie dane, jakie system potrzebuje do wypełniania widoku, potrafi być nie tylko czasochłonne, ale też zasobochłonne. Również analiza zapytań (najczęściej stworzonych przez ORM) może należeć do skomplikowanych.

Oczywiście wzorzec ten ma więcej zalet, lecz obecnie przejdźmy do rozwinięcia skrótu! Command Query Responsibility Segregation – Segregacja odpowiedzialności Polecenie-Zapytanie. Te dosłowne tłumaczenie wspaniale opisuje ideę naszego wzorca – podzielenie implementacji na dwie części. Część Command zawiera wszelkie modele, które posiadają logikę związaną z modyfikacją ich stanu, lecz nie będą zawierać żadnych getterów. Cała logika biznesowa zawiera się w tej właśnie warstwie. Część Query naszego systemu jest bardzo prosta – pobiera ona wyłącznie potrzebne modele widoku ze swojego magazynu. Warto zwrócić uwagę na to, że te modele to zdenormalizowane (na różne sposoby) obiekty domenowe. Nie odzwierciedlają one bezpośrednio encji czy agregatów, lecz dostosowane są pod konkretne potrzeby widoków użytkowników. Mówiąc ogólnie – CQRS zakłada separację modeli do aktualizacji oraz modeli do odczytu.

CQRS wymaga dwóch baz?

Nieraz widziałem opinię, że CQRS musi być zaimplementowany przy użyciu rozdzielnych magazynów danych dla obu części systemu. Oczywiście takie podejście pozwala korzystać z kolejnych zalet jak np. osobne skalowanie baz danych, jednak ma również swoje wady – między innymi należy poradzić sobie ze spójnością ostateczną bądź transakcjami rozproszonymi (ich po prostu nie róbcie). Prawdą jest, że CQRS nie narzuca korzystania z wielu baz. Osobiście uważam, że w mniejszych projektach warto rozpocząć pracę od jednej bazy, jednak tak zaprojektowanej, by łatwo byłą ją rozproszyć w ramach potrzeby. Implementacja dla jednej bazy jest też prostsza pod względem logicznym – aktualizacja modelu domenowego (do aktualizacji) zachodzi w tej samej transakcji co aktualizacje modeli do odczytu.

Warto zwrócić uwagę na liczbę mnogą na końcu poprzedniego zdania. Łatwo możemy wyobrazić sobie sytuację, podczas której potrzebujemy różnych modeli do odczytu dla jednego bytu. Weźmy na przykład wygląd produktu w systemie e-commerce. Inne dane widzi klient przy liście produktów, inne natomiast gdy wejdziemy na jego szczegóły. Jest to niewątpliwy plus tego wzroca – jak na dłoni widać udostępnione światu read-modele. Często pomijaną zaletą tego wzorca jest też łatwiejsze rozdzielenie zadań pomiędzy członków zespołu. Skomplikowaną część Command możemy powierzyć najbardziej doświadczonym członkom zespołu, by równolegle mniej doświadczone osoby mogły szlifować swoje umiejętności w części Query.

Opis problemu

Prawie 500 słów za nami, a jeszcze żadnego kodu! Czas najwyższy to zmienić. Dziś będziemy się poruszać w innym kontekście niż w poprzednich postach, jednak zostaniemy przy projekcie Blogging. Przejdziemy z kontekstu wytwarzania wpisu, do tego, który odpowiedzialny jest za udostępnianie ich treści na blogu. Jest to dobre miejsce na użycie tego wzorca, gdyż skoncentrowane jest na pokazywaniu użytkownikowi wpisu w różnych postaciach (skrót, pełny wpis, w liście, w proponowanych). Dodatkowo bardzo ważna będzie wydajność tej części systemu. Kto w końcu chciałby czekać na wczytanie listy wpisów? Na początku powinna wystarczyć jedna wspólna baza, jednak możliwość ich rozdzielenia powinna być maksymalnie uproszczona. Przejdźmy do rzeczy!

Część Command

Przyjrzyjmy się uproszczonemu modelowi wpisu w tym kontekście. Przed kodem zaznaczę jednak, że przytoczony przykład ma być najprostszym przedstawieniem opisywanej idei. Mogą pojawić się w nim uproszczenia, bądź braki. Poniższy model wpisu zawiera jedynie funkcjonalności związane z dodaniem oceny przez użytkownika.

Jak widać, powyższy model zawiera wyłącznie elementy do aktualizacji. Nie posiada on praktycznie żadnych elementów do odczytu (osobiście zawsze zostawiam publiczny getter do pobierania identyfikatora). Zobaczmy teraz przykładową implementację części aplikacyjnej.

CQRS command

Na pierwszym screenie widać komendę – niemutowalny obiekt, który jest argumentem handlera z drugiego screenu. Zobaczmy na schemacie, jak wygląda proces, który zaimplementowany jest w powyższym kodzie.

Łącząc wszystkie aspekty z tego rozdziału dostajemy złożoną część Command. Zawiera ona zasady biznesowe oraz kod odpowiedzialny za modyfikację stanu agregatów.

Część Query

Ta część naszego systemu powinna być zoptymalizowana pod odczyt. Nie powinna ona zawierać logiki biznesowej, lecz wyłącznie logikę związaną z pobieraniem danych z magazynu. Zobaczmy najprostszą możliwą implementację.

CQRS query
CQRS query handler

Na pierwszym screenie ponownie widzimy niemutowalny obiekt. Na drugim natomiast znajduje się trywialna implementacja handlera. Warto zauważyć, że część Command oraz Query korzystają z różnych repozytoriów. Repozytorium w części Command zawiera wyłącznie metodę do pobrania agregatu po identyfikatorze. Te z części Query posiada natomiast metody do odczytu specyficznych read-modeli. Co więcej, w moim projekcie znajdują się również w osobnych projektach! Moduł aplikacji podzieliłem na dwie części: Blogging.Application.Command oraz Blogging.Application.Query. W naszej implementacji wciąż brakuje jednej rzeczy – synchronizacji pomiędzy dwoma częściami naszego systemu. Nie została dotychczas uwzględniona, gdyż chciałem przedstawić dwie najprostsze ścieżki do uzyskania takiej synchronizacji.

Synchronizacja danych – Widoki

Korzystanie ze wzorca CQRS przy pomocy rozdzielnych magazynów wiąże się z koniecznością obsługi spójności ostatecznej. Tego problemu nie posiadamy przy korzystaniu z pojedynczej bazy danych, gdyż aktualizacja modelu do odczytu może nastąpić w tej samej transakcji bazodanowej co aktualizacja agregatu.

Najprostszym, w mojej opinii, sposobem na stworzenie warstwy do odczytu jest wykorzystanie widoków w bazie danych. Takie podejścia zapewnia zarówno spójność natychmiastową, jak i brak przywiązania do schematu samej bazy. Przy takim podejściu wystarczy utworzyć widoki na bazie za pomocą ulubionego sposobu (np. poprzez migrację w Entity Framework’u), a następnie odczytać dane z widoku. Zobaczmy jak może wyglądać repozytorium postów zaimplementowane przy pomocy Dappera.

Oczywiście takie repozytorium można napisać również za pomocą ORM-a takiego jak Entity Framework, jednak osobiście wolę użyć do tego prostszego narzędzia. Takie podejście nie gwarantuje nam jednak wysokiej wydajności, przynajmniej jeśli nie skorzystamy ze zmaterializowanych widoków.

Synchronizacja danych – Projekcja

Pomyślmy więc nad bardziej wydajnym sposobem, jednak wymagającym większych nakładów pracy. Zacznijmy od lekkiego zmodyfikowania metody obsługi komend. Tym razem napiszemy handler związany z pierwszym publikowaniem wpisu.

Jak widać, pojawiło się wywołanie jednej metody przed zatwierdzeniem zmian w bazie. ProjectionEventSender powinien opublikować zdarzenie, związane z edycją pewnego agregatu. Czyżby był to krok w stronę Event Sourcingu? Faktem jest, że wzorzec CQRS dobrze współpracuje ze wzorcem Event Sourcing. Korzystając z okazji! Ostatnio analizowałem przykłady udostępnione przez Oskara Dudycz. Stąd zapraszam Was do obejrzenia przykładowej implementacji Event Sourcingu na jego repozytorium. Wracając, my skupimy się na najprostszej implementacji takiego rozwiązania. Możemy sobie wyobrazić, że istnieje teraz wiele różnych projekcji dostosowujących post do swoich potrzeb (a w zasadzie do swoich read-modeli). Zobaczmy przykładową implementację takiej projekcji.

Powyższa implementacja jest bardzo prosta, gdyż polega wyłącznie na stworzeniu read-modelu na podstawie zdarzenia, które wywołało daną projekcję. Model ten następnie jest zapisywany w bazie w swojej tabeli. Dzięki czemu otrzymuje całkowitą separację modeli Query/Command na poziomie pojedynczej bazy danych. Takie podejście daje nam podział nie tylko logiczny, ale też techniczny (np. dzięki zastosowaniu osobnych schematów). Warto zwrócić uwagę, że nie mamy w tym miejscu zatwierdzenia transakcji bazodanowej – to odbywa się dopiero po zastosowaniu wszystkich projekcji. Samo repozytorium może być napisane zarówna za pomocą ORM-a, jak i Dappera. Warto tutaj dodać, że w takim przypadku read-modele są osobnymi tabelami w bazie danych, a nie wyłącznie widokami. Dzięki temu odczyt z nich (najczęściej po identyfikatorze) jest bardzo szybki. Niech każdy podejmie decyzję za siebie!

Podsumowanie

Na początku podsumowania, chciałbym zwrócić uwagę, że CQRS można zaimplementować jeszcze na wiele sposobów! Nie jest to w żadnym stopniu jedyna słuszna implementacja. Mam nadzieję, że powyższy wpis przybliżył Wam ideę, jaką niesie ze sobą CQRS. Wzorzec ten ma swoje wady i zalety. Do tych pierwszych na pewno można uznać dodatkową złożoność implementacyjną – jest wiele nowych problemów, które należy rozwiązać. Jednym z nich jest właśnie aktualizacja części do odczytu. Przedstawione przykłady nie zawierają również złożonej obsługi błędów czy radzenia sobie z równoległością zapytań. Chciałem skupić się na głównych częściach tego wzorca, jak i umożliwić każdemu zaimplementowanie przynajmniej prostej aplikacji korzystając z tego stylu. W tym celu muszę dodać, że proces implementacji znacząco ułatwia biblioteka MediatR.

Na sam koniec podkreślę, że mimo swoich zalet, CQRS nie nadaje się do wszystkiego. Na pewno też nie jest wzorcem na poziomie systemu. Decyzję, aby używać CQRS-a, powinniśmy podejmować wyłącznie w ramach określonych kontekstów ograniczonych. Dodatkowo powinniśmy mieć naprawdę dobre powody, by go stosować. Jeśli znajdziemy miejsce, do którego nadaje się CQRS, to użycie tego wzorca da nam liczne korzyści, które mogliśmy poznać w ramach tego wpisu.

A co Wy uważacie na temat CQRS? Mieliście okazję już go używać? Może użyliście innego sposobu synchronizacji danych? Natrafiliście na ciekawe problemy czy innowacje? Dajcie znać!

Mam nadzieję, że spędziliście razem ze mną miłe chwile w Cesartwie-Dev! Jeśli odczuwacie ochotę przeczytania kolejnego wpisu, to co powiecie na temat integracji z zewnętrznym system za pomocą testów? Nie wykluczam, że bardziej zainteresuje Was przetwarzanie bezserwerowe na platformie Azure. Niezależnie, dziękuję Wam za uwagę! Widzimy się za tydzień z okazji kolejnego wpisu w Cesarstwie-Dev!

Książkowy update:

W kolejce:

  • TDD. Sztuka tworzenia dobrego kodu
  • Mikroserwisy w akcji

W trakcie:

  • Netflix – to się nigdy nie uda

Skończone:

  • Software Craftsman
  • Kubernetes – Wzorce projektowe

5 Komentarzy

Wojtek(szogun1987) · 2020-10-23 o 09:56

W początkowej fazie można spokojnie zrezygnować z widoków i rzeźbić SQLem.
Trzeba też dodać że nawet na wspólnej bazie SQL dane przeglądowe zazwyczaj pobieramy przy niższym stopniu izolacji transakcyjnej. Często na liście nikomu to nie przeszkadza, a przyspiesza działanie aplikacji.
W części komend zazwyczaj zależy nam bardziej na spójności danych.

    Cesarz · 2020-10-23 o 10:15

    Cześć! Dzięki za komentarz. Jasne, jest to jedno z podejść. Zdecydowanie można zostawić wyłącznie rzeźbienie SQL-em. Wtedy jednak uważam, że CQRS jest wzorcem projektowym, a nie architektonicznym. Nie wiem, czy jest to oficjalne rozdzielone w taki sposób, jednak ja lubię tak uważać 🙂
    Niezależnie, jasne! Takie podejście też jest spotykane i całkiem poprawne. Dobry punkt z poziomami izolacji!
    Pozdrawiam!

Jarek Żeliński · 2020-10-22 o 10:11

Jeżeli sednem tego wzorca jest separacja dziedzinowa repozytoriów, to jaki sens ma ich łączenie i nadal nazywanie tego CQRS? Jedną z kluczowych zalet tego wzorca jest to, że oba zbiory mają inną strukturę. Jeżeli „pod spodem” będzie jedna i ta sama relacyjna struktura to to po prostu już nie jest CQRS….

    Cesarz · 2020-10-22 o 10:16

    Cześć! Dzięki za komentarz. Rozumiem Twoje uwagi i masz rację. Zauważ, że nie korzystam ze wspólnego interfejsu repozytoriów. W zależności od wykonania synchronizacji (widoki, bądź projekcja) istnieją 2, bądź 3 interfejsy repozytorium (dla Query, Command i projekcji). W artykule zwróciłem uwagę na separację tych struktur czy to za pomocą widoków, czy osobnych tabel dla Read-Modeli. Zgadzam się, że wzorzec architektoniczny CQRS wymaga osobnych repozytoriów i struktur – możliwe, że źle przekazałem swoje myśli. Za to – przepraszam! Niezależnie uważam, że można korzystać z CQRS jako wzorca projektowego, który ma na celu logiczną separację zagadnień, bez wpływu na strukturę bazy. Jednak tego podejście nie opisywałem w tym artykule. Pozdrawiam!

dotnetomaniak.pl · 2020-10-19 o 16:05

CQRS na jednej bazie – Czy zawsze musimy rozpraszać? – Cesarstwo Dev

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *