DÍL 07b KAPITOLA 07 · SEKCE 07.08–07.13

Agregát v Doctrine ORM 3: verze, mapování, zátěž

Jak agregát namapovat v Doctrine ORM 3, aby hranice zůstala hranicí: vlastní typy místo asociací mezi agregáty, verze na kořeni a proč sama nestačí, velké kolekce, hot aggregate, volba identifikátoru a postup návrhu.

Přehrávač YouTube se načte až po kliknutí · otevřít na YouTube (nové okno) ↗

Přepis dílu komentář tak, jak zazní ve videu

Dva požadavky, žádná výjimka

Modelová situace. Objednávka smí mít nejvýš tři položky a teď má dvě. Dva požadavky ji načtou ve stejnou chvíli a oba vidí dvě položky.

Každý jednu přidá a oba projdou. V databázi jsou čtyři položky a výjimku nedostal nikdo, přestože na kořeni je optimistický zámek.

Mapování v Doctrine ORM 3

Doctrine je v Symfony výchozí ORM. Jeho mapování často rozhodne, jestli agregát zůstane čistý, nebo se přizpůsobí databázi. Nejčastější chyba přitom vypadá nevinně.

Objednávka dostane asociaci ManyToOne na zákazníka. Přes ni jde zákazníka načíst i změnit. Nejbližší flush ho pak uloží spolu s objednávkou, a to bez jediného varování.

Mezi agregáty proto vede jen identita. Objednávka drží CustomerId a v databázi je z něj obyčejný sloupec s vlastním typem Doctrine.

Ten typ je malá třída. Hodnotový objekt převede na řetězec pro databázi a při načtení zase zpátky.

Pozor na poslední řádek. Když místo OrderId přijde obyčejný řetězec, typ hodí výjimku. Bez ní by z řetězce byl NULL a dotaz by tiše nenašel nic.

Metodu getName třída nemá, DBAL čtyři ji zrušil. Jméno typu je tedy jen klíč v konfiguraci Doctrine. Pod tímhle jménem na typ odkazuje atribut Column.

Uvnitř hranice je to naopak. Kolekce položek je asociace OneToMany a kaskáda tu nevadí, protože hranici agregátu nepřekročí.

Hned pod ní sedí sloupec verze. Na kořeni drží optimistický zámek, ale jak uvidíte za chvíli, všechno nehlídá.

Položka sama má náhradní celočíselnou identitu. Zvenčí se na ni nikdo neodkazuje, takže UUID by tu jen zabíralo místo. Kdyby se odkazoval, patřila by položka do vlastního agregátu.

Cena položky je Money, hodnotový objekt se dvěma poli. Mapuje se jako embeddable, tedy do sloupců téže tabulky. Zpátky ke kořeni vede ManyToOne a ta je v pořádku, protože zůstává uvnitř hranice.

Všimněte si ještě slova final. Od Doctrine tři celá čtyři na PHP osm celá čtyři se líné načítání obejde bez proxy podtřídy, takže final projde. V DoctrineBundle tři pro Symfony osm jsou nativní líné objekty zapnuté vždy.

Z toho plyne šest pravidel. Asociace jen uvnitř agregátu, jeden repozitář na agregát a verze na kořeni. K tomu události ven přes outbox, kaskáda jen uvnitř hranice a hodnotové objekty s více poli jako embeddable.

Krátký odhad. Doručovací adresa jako embeddable, které smí chybět. V databázi opravdu chybí. Co Doctrine při načtení vrátí?

Null to není. Doctrine vrátí napůl sestavenou instanci a první čtení její vlastnosti skončí chybou. Hodnota, která nemusí existovat, proto patří do vlastní entity, nebo do obyčejných sloupců, které smějí být prázdné.

Co Doctrine nevymůže: verze a flush()

Šest pravidel vypadá jako konfigurace. Jde ale o konvence a nikdo je nekontroluje. Dvě z nich v provozu potichu selžou.

První je verzování. Doctrine zvedne verzi jen tehdy, když se změní pole na kořeni. Nová nebo upravená položka se do verze objednávky nepromítne.

Tady je metoda z úvodu, v ilustrační variantě s limitem. Kontrola počtu proběhne v paměti a oba požadavky ji splní. Pak každý vloží svou položku, jenže řádek objednávky se nezmění. Verze zůstane stejná.

V JPA na to existuje režim zámku, který verzi zvedne vynuceně. Doctrine ho nemá a požadavek na něj je otevřený od roku 2013.

Obrana má dvě podoby. Doménová metoda se při změně potomka dotkne vlastního pole kořene, třeba času poslední změny. Taková změna mívá i doménový význam a verzi zvedne.

Teď oba požadavky znovu. Rychlejší zapíše a verze skočí na čtyři. Druhý zapisuje nad verzí tři a dostane OptimisticLockException. Aplikace pak požadavek zopakuje, nebo uživateli ohlásí konflikt.

Druhou obranou je pesimistický zámek, za cenu propustnosti. Lock s verzí od klienta naopak nestačí. Odhalí zastaralý formulář, ale verzi nezvedne.

Kanonický Order z knihy se ale kořene v metodě addItem nedotýká. Proč je to v pořádku? Zkuste odhadnout.

Položky se mění jen ve stavu Draft a s rozpracovanou objednávkou pracuje jediný zákazník. Vernon tomu říká user-aggregate affinity. Kde položky upravuje víc lidí současně, musí se metoda kořene dotknout.

Druhá trhlina je hranice transakce. Flush zapíše všechny změněné entity v Unit of Work, nejen agregát, který handler načetl.

Když handler cestou změní vlastnost cizího agregátu, Doctrine ho uloží v téže transakci. Pravidlo jeden agregát na transakci tak hlídá jen kázeň v týmu a architektonický test.

Large collection a hot aggregate

Modelový příklad. Agregát Project drží kolekci úkolů. S desítkami to funguje, s tisíci ne.

Každé načtení projektu hydratuje celou kolekci. Flush pak prochází změny všech načtených úkolů, i když se mění jediný.

Kniha nabízí tři východiska, seřazená od nejčistšího. První je agregát rozdělit. Úkol dostane vlastní agregát a na projekt odkazuje přes ProjectId. Že projekt existuje, ověří handler specifikací, o které bude příští díl.

Druhá možnost je kolekce EXTRA_LAZY. Počet úkolů pak zjistíte bez načtení celé kolekce. Hodí se tam, kde ji agregát opravdu potřebuje pro invariant, třeba pro limit úkolů na projekt.

Třetí je služba, která invariant ověří dotazem v repozitáři. Funguje, jenže naznačuje špatně vedenou hranici.

Příbuzný problém je hot aggregate. Na jednu instanci sahá souběžně mnoho lidí, třeba na nákupní košík během Black Friday nebo na sportovní výsledek. Optimistický zámek tu většinu transakcí shodí.

Krátký odhad. Deset zápisů za sekundu do jedné instance. Je to hot aggregate?

Záleží na tom, jak dlouho trvá transakce. Pevná hranice v transakcích za sekundu neexistuje. Rozhoduje součin frekvence zápisů a doby, po kterou transakce drží stav.

Při jedné milisekundě se zápisy skoro nepotkají a agregát je klidný. Při dvou stech milisekundách jsou v rovnoměrném provozu otevřené pořád dvě transakce a konflikty přicházejí. Proto se měří obojí.

Kniha popisuje čtyři přístupy. Agregát jde rozdělit, třeba stadion s tisícem sedadel na sekce po desítkách. Nebo přejít na Event Sourcing, kde souběh hlídá očekávaná verze streamu.

Třetí je single-writer, tedy jediný proces, který do instance zapisuje. V Symfony se k tomu blíží Messenger, který zprávy téhož agregátu posílá jedinému konzumentovi. A čítači lajků stačí přijmout zpoždění i uvnitř agregátu.

Poznámka na okraj. Při multi-tenancy patří tenantId do identity agregátu, filtr přilepený k dotazu nestačí.

Identifikátory, postup návrhu a checklist

Zbývá vybrat typ identifikátoru. Náhodné UUID verze čtyři vygeneruje kdokoli, jen se do indexu nevkládá v pořadí. U clustered indexu v MySQL to zhoršuje zápis.

Sekvenční číslo je krátké, ale potřebuje centrální generátor a prozradí, kolik záznamů máte. Přirozený klíč, třeba IČO nebo e-mail, kniha nedoporučuje. Domény své přirozené klíče mění častěji, než se zdá.

Výchozí volbou je UUID verze sedm. Řadí se podle času vzniku, generuje se bez koordinace a v Symfony ho vytvoří komponenta symfony uid. ULID je alternativa s kratším zápisem, dvacet šest znaků místo třiceti šesti.

Návrh agregátu má v knize sedm kroků. Nejdřív sepíšete invarianty, seskupíte je a pro každou skupinu najdete kořen.

Čtvrtý krok je odhad velikosti tužkou na papíře. Vernon ho ukazuje na položce backlogu. Dvanáct úkolů, u každého dvanáct záznamů o přeodhadu, to je sto čtyřicet čtyři objektů.

Při líném načítání ale jeden požadavek drží v paměti nejvýš dvacet pět. Položku backlogu, dvanáct úkolů a záznamy jediného z nich. Takový agregát Vernon považuje za malý.

Pátý krok odhadne souběh, tedy ten součin z hot aggregate. Pak přijdou commandy a události a nakonec kontrola proti checklistu.

Na workshop existuje hotový formulář, Aggregate Design Canvas od komunity ddd-crew. Má devět polí a otevřenou licenci, takže si ho tým může upravit.

Dvě kolonky v klasických knihách nenajdete. Corrective Policies popisují, co se stane, když se hranice záměrně uvolní. Throughput a Size jsou kroky čtyři a pět, tedy odhad souběhu a růstu.

Checklist na konci kapitoly má dvanáct bodů. Jeden už znáte z druhé části. Na kořeni je verze a každá metoda, která mění vnitřní entitu, se kořene dotkne.

Kanonický addItem tenhle bod vědomě porušuje. Kniha to říká nahlas a zdůvodňuje jediným zákazníkem nad rozpracovanou objednávkou.

Otázky a shrnutí

Otázka na rozmyšlenou. Proč změna položky sama nevyvolá OptimisticLockException?

Verze na kořeni hlídá jen pole kořene. Když se ho doménová metoda nedotkne, konflikt projde potichu.

A jedna z dílu o Team Topologies. Podle čeho poznáte, že Bounded Context je jen fikce?

Neumíte pojmenovat tým, který ho nasazuje a drží u něj pohotovost. A v repozitáři pro jeho adresář chybí pravidlo v CODEOWNERS.

Takže v kostce. Mezi agregáty vede v Doctrine sloupec s vlastním typem, asociace zůstávají uvnitř.

Verzi kořene ani hranici flush Doctrine sama neuhlídá. Chrání je doménová metoda, která se dotkne kořene, a architektonický test.

Velikost a souběh se odhadují předem, tužkou na papíře, a potom se měří.

Příští díl otevírá doplňující taktické vzory a začne u Specification Pattern. Celou kapitolu o návrhu agregátu najdete v knize na ddd-v-symfony.katuscak.cz.

← Všechny díly videokurzu