Warstwa Abstrakcji - stanowi izolację pomiędzy kodem programisty a zewnętrznymi modułami, pozwala odseparować system od jego zależności przez co czyni go podatnym na zmiany i aktualizacje. Przywiązując system do innej usługi czynimy go słabym.

CQRS - identyfikacja zasobów, UUID post

Autor: Norbert Orzechowicz
CQRS - UUID PHP

W celu wygenerowania duplikatu UUID w wersji 4 należy wygenerować 2,71 trylionów* razy, mimo to prawdopodobieństwo jednego duplikatu wynosić będzie zaledwie 50%.

trylion - 10 do potęgi osiemnastej (w krajach stosujących krótką skalę nazywany kwintylionem)

Jeżeli zastanawiałeś się kiedykolwiek czy w jest szansa aby w Twoim systemie UUID mogło się powtórzyć, ten krótki wstęp powinien rozwiać Twoje wątpliwości.

Dlaczego jednak UUID jest najlepszym wyborem dla CQRS?

Ponieważ UUID ma określony format i stosunkowo łatwo można wygenerować go w praktycznie każdym języku, co sprawia że identyfikator zasobu nie musi być nadawany przez bazę danych. Może być bezpiecznie przekazany z "góry" praktycznie bez ryzyka duplikacji.

Bardzo często osoby wdrażające CQRS po raz pierwszy popełniają bardzo podstawowy błąd jakim jest zwracania wartości bezpośrednio po przeprocesowaniu komendy ponieważ wydaje się im, że nie istnieje lepszy sposób na rozwiązanie ich problemu. Nie twierdzę, że takie podejście zawsze jest złe, być może istnieją miejsca, w których wykonanie komendy powinno skutkować zwróceniem jakiejś wartości. Ja jednak na taki przypadek jeszcze nigdy nie trafiłem.

Tworzenie zasobu

Załóżmy, że posiadamy w systemie komendę odpowiedzialną za tworzenie jakiegoś zasobu, niech to będzie Resource. Powiedzmy, że interfejsem naszego systemu jest API Restowe, udostępniające następujący endpoint:

api_resource_create:
    url: /resource
    methods: ["POST"]
    action: ResourceController:createAction

Na potrzeby tego wpisu załóżmy, również że API procesuje requesty w sposób synchroniczny, otrzymuje request, przetwarza go i natychmiast zwraca response.

<?php

declare (strict_types=1);

namespace Example\UserInterface\Http\Symfony\AppBundle\Controller\Rest;

use Ramsey\Uuid\Uuid;
use Example\Application\Command\CreateResource;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class ResourceController extends Controller
{
    public function createAction(Request $request) : Response
    {
        $id = (string) Uuid::uuid4();

        $this->get('command_bus')->handle(new CreateResource($id));

        return new JsonResponse(['id' => $id]);
    }
}

Identyfikator zasobu został przekazany do komendy CreateResource, przez co system nie musiał się już "zastanawiać" jak go utworzyć a co ważniejsze nie musiał polegać na infrastrukturze. Tak byłoby w klasycznym podejściu, w którym zasób otrzymałby identyfikator wygenerowany przez bazę danych, wtedy bez zwracania wartości od razu po wykonaniu komendy implementacja tego endpointu API byłaby niemożliwa.

Edycja zasobu

UUID przydaje się nie tylko podczas tworzenia nowych zasobów, o wiele lepiej sprawdza się w trakcie edycji.

<?php

declare (strict_types=1);

namespace Example\UserInterface\Http\Symfony\AppBundle\Controller\Rest;

use Ramsey\Uuid\Uuid;
use Example\Application\Command\UpdateResource;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class ResourceController extends Controller
{
    public function updateAction(Request $request) : Response
    {
        $id = $request->query->get('id');

        $this->get('command_bus')->handle(new UpdateResource($id, 'some value'));

        $resourceViewObject = $this->get('system.query.resource')->get($id);

        return new JsonResponse([
            'id' => $resourceViewObject->id(),
            'field' => $resourceViewObject->field()
        ]);
    }
}

Jak widać w powyższym przykładzie kodu najpierw wykonywany jest Command, który nic nie zwraca. Następnie system robi Query i zwraca wartość. Proste prawda?

A co jeżeli komenda nie może zostać przetworzona przez system? Powiedzmy, że zasób o konkretnym identyfikatorze nie istnieje?

W takim przypadku najlepiej najpierw wykonać Query i sprawdzić czy zasób istnieje, jeżeli nie zostanie znaleziony rzucamy exception z poziomu User Interface. Jest to wstępna walidacja. Gdyby jednak Query z jakiegoś powodu zwróciło informację, że zasób istnieje (powiedzmy read model się nie wygenerował jak należy) mechanizm obsługujący komendę powinien rzucić wyjątek.

Czy warto?

Część osób może teraz zastanawiać się po co tak kombinować, skoro Command może po prostu zwrócić wartość?

Oczywiście, to jak zaprojektowany będzie system zależy tylko od jego architekta. Jeżeli istnieje dobry powód aby wykonanie komendy skutkowało natychmiastowym zwróceniem wartości nie będę próbował nikogo przekonywać na siłę, że powinien robić inaczej. Nikt nie powinien, nie istnieje w końcu jedna słuszna architektura.

Podejście opisane wyżej ma jednak jedną bardzo istotną zaletę. Pozwala bardzo łatwo skalować system wraz z jego rozwojem. W pewnym momencie przychodzi ten moment kiedy nasz startup zaczyna odwiedzać więcej osób niż CEO i jego rodzina. Jeżeli mamy trochę szczęścia a nasz biznes dobry pomysł ta chwila przyjdzie szybciej niż nam się wydaje. Trzymając się sztywno założenia, że Command zmienia stan systemu a Query go odczytuje będziemy mogli bardzo sprawnie wprowadzić asynchroniczną obsługę Command opartą o jakiś system kolejek. W systemie którego Command ma zwrócić natychmiastowo wartość nie będzie to takie łatwe, nie możemy po prostu zserializować Command, wrzucić na kolejkę i pozwolić workerowi go przeprocesować, trzeba w końcu jakoś zwrócić tą wartość. Co więcej asynchroniczne procesowanie komend dotyczących zasobów operujących na UUID pozwala również zignorować kolejność wykonywania się komend. Przykładowo można wgrać do filesystemu plik dla encji o konkretnym UUID nawet kiedy ta encja jeszcze nie istnieje ponieważ komenda tworzenia encji wykona się później niż komenda wgrania pliku. Autoinkrementacja by na coś takiego nie pozwoliła.

Wydajność bazy danych

Nie jestem specjalistą od wydajności baz danych. Sam stanąłem kiedyś przed pytaniem czy UUID będzie wydajne. Moje pierwotne obawy okazały się bezpodstawne. W systemie który aktualnie projektuje i rozwijam dosłownie wszystko identyfikowane jest za pomocą UUID. Agregaty, eventy, encje, pliki w filesystemie, wszędzie korzystamy z UUID. PostgreSQL udostępnia typ UUID, wykorzystujemy go zarówno jako Primary Key jak również Foreign Key. Ostatnio procesowaliśmy w tle ponad 210 tysięcy komend, nie natrafiliśmy na żadne problemy wydajnościowe. Jedna z naszych tabel z PK o typie UUID posiada kilka milionów rekordów, tutaj również nie natrafiliśmy na żadne problemy wydajnościowe. Najwięcej problemów wydajnościowych sprawiają integrację z zewnętrznymi usługami, moim zdaniem takich problemów należy się obawiać. Bardzo dużą rolę odgrywa tu jednak odpowiedni Read Model. Kiedy dane stają się zbyt skomplikowane aby je odpowiednio przeszukiwać tworzymy zoptymalizowany read model, problemy wydajnościowe pomaga nam rozwiązywać architektura, nie narzędzia.

Value Object

Czy warto pod każdy zasób tworzyć nowy Value Object dla UUID? Raczej nie, próbowałem tego podejścia, każdy agregat w systemie posiadał własny typ UUID. Finalnie takie rozwiązanie niczego nie dało, spowodowało tylko że pod każdy typ trzeba było tworzyć osobny Doctrine Dbal Type. Finalnie ujednoliciliśmy wszystko, posiadamy teraz jeden generyczny VO UUID i korzystamy z niego w całym modelu domeny, niezależnie od kontekstu. Tak zostanie przynajmniej do momentu w którym PHP do SPL (Standard PHP Library) nie wprowadzi obiektu UUID. Ponieważ się na to jednak nie zapowiada polecam ramsey/uuid oraz ramsey/uuid-doctrine.

Więcej na temat UUID:

Masz pytanie?
Napisz: kontakt@zawarstwaabstrakcji.pl

Feedback

Podobał Ci się ten wpis? Bardzo mnie to cieszy. Nie ma większej motywacji niż zadowolony odbiorca. Gdybyś uznał, że treść którą tworzę jest wartościowa byłbym wdzięczny za zostawienie recenzji na Facebooku. Taka recenzja nie tylko zmotywuje mnie do dalszej pracy ale pozwoli również zyskać większy zasięg. Zasięg, który pozwoli mi dotrzeć do większej grupy odbiorców.
Możesz też polubić fanpage, na którym staram się w miarę regularnie publikować krótsze ale nie mniej wartościowe treści.

Akceptuję

Ten serwis używa plików cookies. Więcej o plikach cookies.