Oskar Dudycz

Pragmatic about programming

Jak zrobić Event Store w bazie relacyjnej? Odpowiadam!

2019-09-29 oskar dudyczEvent Sourcing

front

Cześć!

Kolejny tydzień, kolejny newsletter, tak jak Łonie kaloryfer splata się z przemijaniem, tak mi ten cotygodniowy mailing też powoli wchodzi w rytm tygodnia. Fajne to. W każdym razie dla mnie.

W zeszłym tygodniu wróciliśmy się do podstaw, dowiedzieliśmy się co to jest zdarzenie i co to jest strumień zdarzeń. Nie przeczytany jeszcze? To zachęcam do zerknięcia w historię skrzynki mailowej. Jeśli przeczytane to tym bardziej się cieszę, bo ważne dla mnie jest to, żeby nie leciała ta moja pisanina w eter.

Złożyłem tez tydzień temu obietnicę, że w tym tygodniu napiszę o tym jak przechowywać zdarzenia w bazie relacyjnej.

Jak zaczynałem swoją historię z Event Sourcing to miałem wyobrażenie, że jak chce się robić Event Sourcing to potrzebny jest jakiś super wyrafinowany i skomplikowany sposób zapisu. Myślałem, że trzeba używać jakiś tajemnicznych systemów, które gdzieś to sobie zapisują, potem odczytują, pewnie binarnie czy siakoś inaczej. Czarna magia.

Okazało się jak zwykle, że wilk ma wielkie oczy, a “storage” Event Sourcingowy można bez problemu zrealizować przy pomocy baz relacyjnych. Jak? Chwilunia - wróćmy jeszcze na chwilę do samych zdarzeń. Załóżmy, że robimy system do zarządzania projektami. Powiedzmy, że naszymi pierwszymi zdarzeniami są:

  • utworzenie projektu,
  • przypisanie menadżera,
  • rozpoczęcie projektu.

Takie operacje biznesowe w nim zachodzą. Spróbujmy je sobie rozpisać - jakie mają pola itd. Tak jak pewnie pamiętasz (z poprzedniego maila) zdarzenia nazywamy w formie przeszłej. Przejdźmy też dla wygody na język angielski (ja ostatni swój kod w języku polskim napisałem chyba z 10 lat temu).

ProjectCreated

  • Id: Guid - id projektu
  • Name: string - nazwa projektu
  • CreationDate: DateTime - data utworzenia

ProjectManagerAssigned

  • Id: Guid - id projektu
  • ProjectManagerName: string - nazwa menadżera projektu (dla uproszczenia przyjmijmy, że to po prostu nazwa, równie dobrze moglibyśmy tutaj dać tez identyfikator PMa)
  • AssignmentDate: DateTime - data przypisania

ProjectStarted

  • Id: Guid - id projektu
  • StartDate: DateTime - data rozpoczęcia

Tak jak zwykle w zagadkach mamy znaleźć dziesięć szczegółów różniących te same obrazki, tak teraz spróbujmy zrobić odwrotnie - co je łączy. Już? Masz swoje odpowiedzi?

Oto moje:

  • typ zdarzenia - każdy ma swoją unikalną nazwę, która go opisuje (ProjectCreated, ProjectManagerAssigned, ProjectStarted),
  • id rekordu/strumienia - pamiętasz? Strumień zdarzeń odpowiada rekordowi w klasycznym podejściu, ma swój unikalny identyfikator - w tym przypadku Id projektu,
  • data zdarzenia - informuje o tym kiedy zdarzenie zaszło, może być użyte do posortowania chronologicznego zdarzeń - jak pamiętasz pewnie zdarzenia w strumieniu zachodzą jeden po drugim - dokładnie tak jak w życiu. Musimy więc wiedzieć po czym posortować,
  • dane zdarzenia - czyli wszystkie pola dodatkowe, które opisują zdarzenie (Name dla ProjectCreated, ProjectManagerName dla ProjectManagerAssigned, dla ProjectStarted nie ma, bo wystarczy sama data zdarzenia).

Jak moglibyśmy zatem dane zdarzenia zapisać? Może JSONem?

{ "type": "ProjectCreated",         "id": "c5693101-b2e0-424b-8578-fc7391df57a0", "Name": "Event Sourcing Newsletter",  "CreationDate": "2019-09-09"}
{ "type": "ProjectManagerAssigned", "id": "c5693101-b2e0-424b-8578-fc7391df57a0", "ProjectManagerName": "Oskar Dudycz", "AssignmentDate": "2019-09-10"}
{ "type": "ProjectStarted",         "id": "c5693101-b2e0-424b-8578-fc7391df57a0",                                       "StartDate": "2019-09-29"}
{ "type": "ProjectCreated",         "id": "7263d9fa-5f0f-41d3-8b14-e1f6179a1a65", "Name": "Event Sourcing Blog",        "CreationDate": "2019-09-29"}

Formatowanie nie jest przypadkowe. Przypomina Ci tabelaryczną strukturę? Powinno.

Zatem jak zapisać to w tabeli? Odpowiadam (w Postgresowym zapisie):

CREATE TABLE events
(
    id          uuid         NOT NULL, -- klucz główny, identyfikator zdarzenia
    type        varying(500) NOT NULL, -- typ zdarzenia
    stream_id   uuid,        NOT NULL, -- id strumienia
    timestamp   timestampz,  NOT NULL, -- data zdarzenia
    data        jsonb        NOT NULL, -- pozostałe dane zdarzenia zapisane w postaci JSON

    CONSTRAINT pk_events PRIMARY KEY (id),
)

Zagraj to jeszcze raz sam!

| id                                     | type                     | stream_id                               | timestamp    | data                                       |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| "8a61fc77-9ed2-4b16-81ee-1c93fd10ef61" | "ProjectCreated"         | "c5693101-b2e0-424b-8578-fc7391df57a0", | "2019-09-09" | "{ Name: "Event Sourcing Newsletter" }"    |
| "8a61fc77-9ed2-4b16-81ee-1c93fd10ef61" | "ProjectManagerAssigned" | "c5693101-b2e0-424b-8578-fc7391df57a0", | "2019-09-09" | "{ ProjectManagerName: "Oskar Dudycz" }"   |
| "8a61fc77-9ed2-4b16-81ee-1c93fd10ef61" | "ProjectCreated"         | "c5693101-b2e0-424b-8578-fc7391df57a0", | "2019-09-09" | "{}"                                       |
| "8a61fc77-9ed2-4b16-81ee-1c93fd10ef61" | "ProjectCreated"         | "c5693101-b2e0-424b-8578-fc7391df57a0", | "2019-09-09" | "{ Name: " Event Sourcing Blog" }"         |

A więc możliwe! Dlaczego Postgres? Bo ma rewelacyjne wsparcie dla JSON, nieporównywalne z innymi bazami relacyjnymi. Zobacz sam: link. Dodatkowo jest darmowy, dojrzały i w pełni używalny zarówno w mniejszych aplikacjach jak i dużych enterprisowych systemów. To jest też fundamentem, na którym powstał Marten.

Czy to koniec rozważań? Oczywiście, że nie, sama np. struktura tabeli zdarzeń często jest rozszerzana o kolumnę z wersją. Jest ona lepszym źródłem chronologii zdarzeń oraz pomaga w “optimistic concurrency”. Jesteś ciekaw tego tematu? Daj znać, chętnie opiszę go w kolejnych mailingach.

Często też dodaje się pomocniczą tabelę strumieni, gdzie zapisuje się dane o strumieniach (takie jak id, typ, itd.) po to by zoptymalizować operację w Event Sourcingu.

Sam temat guidowych identyfikatorów jest też tematem do osobnych rozważań.

No ale to już detale, sama idea jak widzisz powyżej nie jest wcale taka skomplikowana i nie do wykonania. Również w MSSQL. Chcesz zobaczyć jak to zrobić w praktyce i pobrudzić sobie ręce? Nie brzydzisz się SQLem, znasz lub chcesz poznać Dapper? Zerknij do mojego repozytorium, gdzie znajdziesz zadania, które pozwolą Ci to przećwiczyć samemu: link.

Polecam też lekturę wpisu Grega Younga, który opisał jak zrobić takiego Event Store’a już 8 lat temu: klik.

Ok, kończę ten nieco długi wpis i pozdrawiam serdecznie.

Do zobaczenia za tydzień!

Oskar

p.s. daj znać czy taka tematyka i forma Ci się podoba, co mógłbym Ci jeszcze przybliżyć (niekoniecznie z Event Sourcing), co mógłbym zmienić, aby te niedzielne maile były dla Ciebie jeszcze ciekawsze

p.s.2. Microsoft/.NET Foundation ogłosiło OSS Maturity Ladder - zobacz tutaj i tutaj. Czyli pomysł na certyfikację projektów open source’owych. Temat jest bardzo kontrowersyjny. Polecam zobaczyć dyskusję na Github (ja również napisałem tam swoje zdanie na ten temat) - klik. Warto to obserwować, nawet jak się nie jest kontrybutorem w Open Source, bo temat co najmniej pośrednio dotknie każdego z nas.

  • © Oskar Dudycz 2019-2020