Gdybym używał RDBMS (np. SQL Server) do przechowywania danych dotyczących źródeł zdarzeń, jak mógłby wyglądać schemat?
Widziałem kilka odmian, o których mówiono w abstrakcyjnym sensie, ale nic konkretnego.
Na przykład załóżmy, że istnieje jednostka „Produkt”, a zmiany w tym produkcie mogą przybrać postać: ceny, kosztu i opisu. Nie wiem, czy:
- Miej tabelę „ProductEvent”, która zawiera wszystkie pola produktu, gdzie każda zmiana oznacza nowy rekord w tej tabeli, plus „kto, co, gdzie, dlaczego, kiedy i jak” (WWWWWH). W przypadku zmiany kosztu, ceny lub opisu dodawany jest cały nowy wiersz reprezentujący Produkt.
- Przechowuj Koszt, Cena i Opis produktu w oddzielnych tabelach połączonych z tabelą Produkt za pomocą relacji klucza obcego. Gdy wystąpią zmiany tych właściwości, napisz nowe wiersze z WWWWWH, jeśli to konieczne.
- Przechowuj WWWWWH oraz serializowany obiekt reprezentujący zdarzenie w tabeli „ProductEvent”, co oznacza, że samo zdarzenie musi zostać załadowane, zdeserializowane i ponownie odtworzone w kodzie mojej aplikacji w celu odtworzenia stanu aplikacji dla danego Produktu .
Szczególnie martwi mnie opcja 2 powyżej. W skrajnym przypadku tabela produktów byłaby prawie jedną tabelą na właściwość, gdzie załadowanie stanu aplikacji dla danego produktu wymagałoby załadowania wszystkich zdarzeń dla tego produktu z każdej tabeli zdarzeń produktu. Ta eksplozja stołu źle mi pachnie.
Jestem pewien "to zależy" i chociaż nie ma jednej "poprawnej odpowiedzi", staram się wyczuć, co jest akceptowalne, a co całkowicie nie do przyjęcia. Zdaję sobie również sprawę, że NoSQL może tu pomóc, gdzie zdarzenia mogą być przechowywane w zagregowanym katalogu głównym, co oznacza tylko jedno żądanie do bazy danych, aby uzyskać zdarzenia w celu odbudowania obiektu, ale nie używamy bazy danych NoSQL w chwilę, więc szukam alternatyw.
źródło
Odpowiedzi:
Magazyn zdarzeń nie powinien potrzebować informacji o konkretnych polach lub właściwościach zdarzeń. W przeciwnym razie każda modyfikacja modelu spowodowałaby konieczność migracji bazy danych (podobnie jak w przypadku dobrej, staromodnej trwałości opartej na stanie). Dlatego w ogóle nie polecałbym opcji 1 i 2.
Poniżej znajduje się schemat używany w Ncqrs . Jak widać, tabela „Zdarzenia” przechowuje powiązane dane jako CLOB (tj. JSON lub XML). Odpowiada to Twojej opcji 3 (tylko, że nie ma tabeli „ProductEvents”, ponieważ potrzebujesz tylko jednej ogólnej tabeli „Events”. W Ncqrs mapowanie na Twoje Aggregate Roots odbywa się za pośrednictwem tabeli „EventSources”, gdzie każde EventSource odpowiada rzeczywistej Agregat główny).
Mechanizm trwałości SQL implementacji magazynu zdarzeń Jonathana Olivera składa się w zasadzie z jednej tabeli o nazwie „Commits” z polem BLOB „Payload”. Jest to prawie to samo, co w Ncqrs, tyle że serializuje właściwości zdarzenia w formacie binarnym (który na przykład dodaje obsługę szyfrowania).
Greg Young zaleca podobne podejście, co zostało obszernie udokumentowane na stronie internetowej Grega .
Schemat jego prototypowej tabeli „Zdarzenia” brzmi:
źródło
Projekt GitHub CQRS.NET zawiera kilka konkretnych przykładów tego, jak można zrobić EventStores w kilku różnych technologiach. W chwili pisania tego tekstu istnieje implementacja w języku SQL przy użyciu Linq2SQL i zgodny ze schematem SQL , jeden dla MongoDB , jeden dla DocumentDB (CosmosDB, jeśli jesteś na Azure) i jeden wykorzystujący EventStore (jak wspomniano powyżej). Na platformie Azure jest więcej, takich jak Table Storage i Blob Storage, które są bardzo podobne do płaskiego magazynu plików.
Myślę, że głównym celem jest to, że wszystkie są zgodne z tym samym zleceniem / umową. Wszystkie przechowują informacje w jednym miejscu / kontenerze / tabeli, używają metadanych do identyfikacji jednego zdarzenia od innego i „po prostu” przechowują całe zdarzenie tak, jak było - w niektórych przypadkach serializowane, w technologiach pomocniczych, tak jak było. W zależności od tego, czy wybierzesz bazę danych dokumentów, relacyjną bazę danych lub nawet plik płaski, istnieje kilka różnych sposobów, aby osiągnąć ten sam cel magazynu zdarzeń (jest to przydatne, jeśli w dowolnym momencie zmienisz zdanie i stwierdzisz, że musisz przeprowadzić migrację lub wesprzeć więcej niż jednej technologii przechowywania).
Jako programista przy projekcie mogę podzielić się spostrzeżeniami na temat niektórych wyborów, których dokonaliśmy.
Po pierwsze stwierdziliśmy (nawet z unikalnymi identyfikatorami UUID / GUID zamiast liczb całkowitych) z wielu powodów identyfikatory sekwencyjne występują ze względów strategicznych, dlatego samo posiadanie identyfikatora nie było wystarczająco unikalne dla klucza, więc połączyliśmy naszą główną kolumnę klucza identyfikatora z danymi / typ obiektu, aby stworzyć coś, co powinno być naprawdę (w sensie aplikacji) unikalnym kluczem. Wiem, że niektórzy ludzie mówią, że nie musisz go przechowywać, ale będzie to zależeć od tego, czy jesteś od podstaw, czy też musisz współistnieć z istniejącymi systemami.
Utknęliśmy z jednym kontenerem / tabelą / kolekcją ze względu na łatwość konserwacji, ale bawiliśmy się z oddzielną tabelą na jednostkę / obiekt. W praktyce stwierdziliśmy, że oznacza to, że aplikacja potrzebuje uprawnień „TWÓRZ” (co ogólnie nie jest dobrym pomysłem ... generalnie zawsze są wyjątki / wykluczenia) lub za każdym razem, gdy nowy obiekt / obiekt powstawał lub był wdrażany, nowy potrzebne do wykonania pojemniki / stoły / kolekcje magazynowe. Okazało się, że było to boleśnie powolne w przypadku lokalnego rozwoju i problematyczne w przypadku wdrożeń produkcyjnych. Możesz nie, ale takie było nasze doświadczenie w świecie rzeczywistym.
Inną rzeczą do zapamiętania jest to, że żądanie wykonania akcji X może spowodować wystąpienie wielu różnych zdarzeń, a zatem poznanie wszystkich zdarzeń wygenerowanych przez polecenie / zdarzenie / cokolwiek jest przydatne. Mogą również dotyczyć różnych typów obiektów, np. Naciśnięcie przycisku „kup” w koszyku może wywołać uruchomienie zdarzeń dotyczących konta i magazynu. Konsumująca aplikacja może chcieć wiedzieć to wszystko, dlatego dodaliśmy CorrelationId. Oznaczało to, że konsument mógł poprosić o wszystkie zdarzenia wywołane w wyniku jego żądania. Zobaczysz to w schemacie .
W szczególności w przypadku SQL odkryliśmy, że wydajność naprawdę stała się wąskim gardłem, jeśli indeksy i partycje nie były odpowiednio używane. Pamiętaj, że jeśli używasz migawek, wydarzenia będą musiały być przesyłane strumieniowo w odwrotnej kolejności. Wypróbowaliśmy kilka różnych indeksów i stwierdziliśmy, że w praktyce do debugowania rzeczywistych aplikacji w środowisku produkcyjnym potrzebne są pewne dodatkowe indeksy. Ponownie zobaczysz to w schemacie .
Inne metadane w trakcie produkcji były przydatne podczas dochodzeń opartych na produkcji, a sygnatury czasowe dały nam wgląd w kolejność utrwalania i zgłaszania zdarzeń. To dało nam pewną pomoc przy szczególnie silnie sterowanym zdarzeniami systemie, który generował ogromną liczbę zdarzeń, dając nam informacje o wydajności takich rzeczy, jak sieci i dystrybucja systemów w sieci.
źródło
Cóż, możesz rzucić okiem na Datomic.
Datomic to baza elastycznych, opartych na czasie faktów , obsługująca zapytania i łączenia, z elastyczną skalowalnością i transakcjami ACID.
Tutaj napisałem szczegółową odpowiedź
Możesz obejrzeć wykład Stuarta Hallowaya wyjaśniający konstrukcję Datomic tutaj
Ponieważ Datomic przechowuje fakty w czasie, możesz go używać do pozyskiwania informacji o zdarzeniach i nie tylko.
źródło
Myślę, że rozwiązanie (1 i 2) może bardzo szybko stać się problemem w miarę rozwoju modelu domeny. Tworzone są nowe pola, niektóre zmieniają znaczenie, a niektóre mogą przestać być używane. Ostatecznie twoja tabela będzie miała dziesiątki pól dopuszczalnych wartości null, a ładowanie zdarzeń będzie bałagan.
Pamiętaj również, że magazyn zdarzeń powinien być używany tylko do zapisu, a wysyłasz do niego zapytania tylko w celu załadowania zdarzeń, a nie właściwości agregatu. Są to odrębne rzeczy (to jest istota CQRS).
Rozwiązanie 3 To, co ludzie zwykle robią, jest na to wiele sposobów.
Na przykład EventFlow CQRS używany z SQL Server tworzy tabelę o następującym schemacie:
gdzie:
Jeśli jednak tworzysz od zera, zalecałbym przestrzeganie zasady YAGNI i tworzenie z minimalną wymaganą liczbą pól dla twojego przypadku użycia.
źródło
Możliwa wskazówka to projekt, po którym następuje „Wolno zmieniający się wymiar” (typ = 2), który powinien pomóc w uwzględnieniu:
Zaimplementowanie funkcji składania w lewo również powinno być w porządku, ale trzeba pomyśleć o przyszłej złożoności zapytań.
źródło
Myślę, że byłaby to późna odpowiedź, ale chciałbym zwrócić uwagę, że użycie RDBMS jako magazynu do pozyskiwania zdarzeń jest całkowicie możliwe, jeśli wymagania dotyczące przepustowości nie są wysokie. Chciałbym tylko pokazać przykłady księgi źródłowej zdarzeń, którą zbudowałem, aby zilustrować.
https://github.com/andrewkkchan/client-ledger-service Powyżej znajduje się usługa internetowa księgi źródeł zdarzeń. https://github.com/andrewkkchan/client-ledger-core-db A powyższe używam RDBMS do obliczania stanów, więc możesz cieszyć się wszystkimi zaletami oferowanymi przez RDBMS, takie jak obsługa transakcji. https://github.com/andrewkkchan/client-ledger-core-memory I mam innego konsumenta, który przetwarza w pamięci, aby obsłużyć serie.
Można by argumentować, że rzeczywisty magazyn zdarzeń powyżej nadal istnieje w Kafce - ponieważ RDBMS jest powolny do wstawiania, zwłaszcza gdy wstawianie jest zawsze dołączane.
Mam nadzieję, że kod pomoże ci zilustrować, oprócz bardzo dobrych teoretycznych odpowiedzi już udzielonych na to pytanie.
źródło