O ile rozumiem, główną ideą CQRS są 2 różne modele danych do obsługi poleceń i zapytań. Są to tak zwane „model zapisu” i „model odczytu”.
Rozważmy przykład klonowania aplikacji na Twitterze. Oto polecenia:
- Użytkownicy mogą się zarejestrować.
CreateUserCommand(string username)
emitujeUserCreatedEvent
- Użytkownicy mogą obserwować innych użytkowników.
FollowUserCommand(int userAId, int userBId)
emitujeUserFollowedEvent
- Użytkownicy mogą tworzyć posty.
CreatePostCommand(int userId, string text)
emitujePostCreatedEvent
Chociaż używam powyższego terminu „zdarzenie”, nie mam na myśli zdarzeń „pozyskiwania zdarzeń”. Mam na myśli sygnały, które uruchamiają aktualizacje odczytu modelu. Nie mam sklepu z wydarzeniami i do tej pory chcę się skoncentrować na samym CQRS.
A oto pytania:
- Użytkownik musi zobaczyć listę swoich postów.
GetPostsQuery(int userId)
- Użytkownik musi zobaczyć listę swoich obserwujących.
GetFollowersQuery(int userId)
- Użytkownik musi zobaczyć listę obserwowanych użytkowników.
GetFollowedUsersQuery(int userId)
- Użytkownik musi zobaczyć „kanał znajomych” - dziennik wszystkich działań swoich znajomych („Twój przyjaciel John właśnie utworzył nowy post”).
GetFriedFeedRecordsQuery(int userId)
Aby sobie z CreateUserCommand
tym poradzić , muszę wiedzieć, czy taki użytkownik już istnieje. W tym momencie wiem, że mój model zapisu powinien zawierać listę wszystkich użytkowników.
Aby sobie z FollowUserCommand
tym poradzić , muszę wiedzieć, czy użytkownik A już obserwuje użytkownika B, czy nie. W tym momencie chcę, aby mój model zapisu miał listę wszystkich połączeń użytkownik-użytkownik-użytkownik.
I na koniec, CreatePostCommand
nie sądzę, że potrzebuję niczego innego, ponieważ nie mam takich poleceń UpdatePostCommand
. Gdybym je miał, musiałbym upewnić się, że post istnieje, więc potrzebowałbym listy wszystkich postów. Ale ponieważ nie mam tego wymagania, nie muszę śledzić wszystkich postów.
Pytanie nr 1 : czy rzeczywiście jest poprawne użycie terminu „napisz model” w taki sposób, w jaki go używam? Czy też „model zapisu” zawsze oznacza „magazyn zdarzeń” w przypadku ES? Jeśli tak, to czy istnieje jakakolwiek separacja między danymi potrzebnymi do obsługi poleceń a danymi potrzebnymi do obsługi zapytań?
Do obsługi GetPostsQuery
potrzebowałbym listy wszystkich postów. Oznacza to, że mój model odczytu powinien mieć listę wszystkich postów. Zamierzam utrzymać ten model, słuchając PostCreatedEvent
.
Aby obsłużyć jedno GetFollowersQuery
i drugie GetFollowedUsersQuery
, potrzebowałbym listy wszystkich połączeń między użytkownikami. Aby utrzymać ten model, będę go słuchał UserFollowedEvent
. Oto pytanie nr 2 : czy praktycznie jest OK, jeśli użyję tutaj zapisu listy połączeń modelu? A może powinienem stworzyć osobny model do odczytu, ponieważ w przyszłości może potrzebuję więcej szczegółów niż w modelu do pisania?
Wreszcie, aby sobie z tym poradzić GetFriendFeedRecordsQuery
, musiałbym:
- Słuchać
UserFollowedEvent
- Słuchać
PostCreatedEvent
- Dowiedz się, którzy użytkownicy śledzą, którzy inni użytkownicy
Jeśli użytkownik A podąża za użytkownikiem B, a użytkownik B zaczyna podążać za użytkownikiem C, powinny pojawić się następujące rekordy:
- Dla użytkownika A: „Twój znajomy użytkownik B właśnie zaczął obserwować użytkownika C”
- Dla użytkownika B: „Właśnie zacząłeś obserwować użytkownika C”
- Dla użytkownika C: „Użytkownik B obserwuje teraz użytkownika”
Oto pytanie nr 3 : jakiego modelu należy użyć, aby uzyskać listę połączeń? Czy powinienem używać modelu zapisu? Czy powinienem używać modelu odczytu - GetFollowersQuery
/ GetFollowedUsersQuery
? Czy powinienem sprawić, by GetFriendFeedRecordsQuery
sam model obsługiwał UserFollowedEvent
i utrzymywał własną listę wszystkich połączeń?
źródło
Odpowiedzi:
Greg Young (2010)
Jeśli myślisz w kategoriach separacji zapytań poleceń Bertranda Meyera , możesz pomyśleć o tym, że model ma dwa odrębne interfejsy, jeden obsługujący polecenia i drugi, który obsługuje zapytania.
Wgląd Grega Younga polegał na tym, że można to podzielić na dwa osobne obiekty
Po oddzieleniu obiektów masz teraz możliwość oddzielenia struktur danych, które utrzymują stan obiektu w pamięci, dzięki czemu możesz zoptymalizować dla każdego przypadku; lub przechowuj / utrzymuj stan odczytu niezależnie od stanu zapisu.
Termin WriteModel jest zwykle rozumiany jako zmienna reprezentacja modelu (tj .: obiekt, a nie magazyn trwałości).
TL; DR: Myślę, że twoje użycie jest w porządku.
To „w porządku” - ish. Koncepcyjnie nie ma nic złego w modelu odczytu i modelu zapisu dzielącym te same struktury.
W praktyce, ponieważ zapisy do modelu zwykle nie są atomowe, istnieje potencjalny problem, gdy jeden wątek próbuje zmodyfikować stan modelu, podczas gdy drugi wątek próbuje go odczytać.
Zastosowanie modelu zapisu jest złą odpowiedzią.
Pisanie wielu modeli odczytu, w których każdy jest dostosowany do konkretnego przypadku użycia, jest całkowicie uzasadnione. Mówimy „model odczytu”, ale rozumie się, że może istnieć wiele modeli odczytu, z których każdy jest zoptymalizowany pod kątem konkretnego przypadku użycia i nie implementuje przypadków, w których nie ma to sensu.
Na przykład możesz zdecydować się na użycie magazynu klucz-wartość do obsługi niektórych zapytań i bazy danych grafów dla innych zapytań lub relacyjnej bazy danych, w której ten model zapytań ma sens. Konie na kursy.
W konkretnych okolicznościach, w których wciąż uczysz się wzoru, sugeruję, aby projekt był „prosty” - mieć jeden model odczytu, który nie dzieli struktury danych modelu zapisu.
źródło
Zachęcam do rozważenia posiadania jednego koncepcyjnego modelu danych.
Następnie model zapisu jest materializacją tego modelu danych zoptymalizowanego pod kątem aktualizacji transakcyjnych. Czasami oznacza to znormalizowaną relacyjną bazę danych.
Model odczytu to zmaterializowanie tego samego modelu danych zoptymalizowanego do wykonywania zapytań potrzebnych aplikacji. Może to być relacyjna baza danych, choć celowo zdenormalizowana do obsługi zapytań z mniejszą liczbą sprzężeń.
(# 1) W przypadku CQRS model zapisu nie musi być magazynem zdarzeń.
(# 2) Nie spodziewałbym się, że model odczytu zapisuje wszystko, co nie jest w modelu zapisu, na przykład dlatego, że w CQRS nikt nie aktualizuje modelu odczytu z wyjątkiem mechanizmu przekazywania, który utrzymuje model odczytu w synchronizacji ze zmianami w napisz model.
(# 3) W CQRS zapytania powinny być sprzeczne z modelem odczytu. Możesz zrobić inaczej: jest ok, po prostu nie przestrzegaj CQRS.
Podsumowując, istnieje tylko jeden konceptualny model danych. CQRS oddziela sieć poleceń i możliwości od sieci zapytań i możliwości. Biorąc pod uwagę to rozdzielenie, model zapisu i model odczytu mogą być hostowane przy użyciu bardzo różnych technologii jako optymalizacji wydajności.
źródło