CQRS bez DDD i bez (lub z?) ES - co to jest model zapisu, a co model odczytu?

11

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 CreateUserCommandtym 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 FollowUserCommandtym 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, CreatePostCommandnie 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 GetPostsQuerypotrzebował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 GetFollowersQueryi 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 GetFriendFeedRecordsQuerysam model obsługiwał UserFollowedEventi utrzymywał własną listę wszystkich połączeń?

Andrey Agibalov
źródło
Należy pamiętać, że w systemach CQRS model danych zapytań i model danych poleceń może pobierać dane z różnych baz danych. Oba modele mogą żyć niezależnie od siebie (różne aplikacje). Biorąc to pod uwagę, odpowiedź brzmi „zależy” (jak zwykle). Może to być interesujące
Laiv

Odpowiedzi:

7

Greg Young (2010)

CQRS to po prostu stworzenie dwóch obiektów, w których wcześniej był tylko jeden.

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.

interface IChangeTheModel {
    void createUser(string username)
    void followUser(int userAId, int userBId)
    void createPost(int userId, string text)
}

interface IDontChangeTheModel {
    Iterable<Post> getPosts(int userId)
    Iterable<Follower> getFollowers(int userId)
    Iterable<FollowedUser> getFollowedUsers(int userId)
}

class TheModel implements IChangeTheModel, IDontChangeTheModel {
    // ...
}

Wgląd Grega Younga polegał na tym, że można to podzielić na dwa osobne obiekty

class WriteModel implements IChangeTheModel { ... }
class ReadModel  implements IDontChangeTheModel {...}

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.

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

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.

Oto pytanie nr 2: czy praktycznie jest OK, jeśli użyję tutaj zapisu listy połączeń modelu?

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

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? A może powinienem sprawić, aby sam model GetFriendFeedRecordsQuery obsługiwał UserFollowedEvent i utrzymywał własną listę wszystkich połączeń?

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.

VoiceOfUnreason
źródło
1

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.

Erik Eidt
źródło