Slovník pojmů
CQRS
Změna stavu a čtení dat mají jiný účel. CQRS jim proto dává samostatné use cases a dovoluje každou stranu navrhnout podle jejích skutečných potřeb.
Stručná definice
Command rozhoduje a mění stav, query data pouze čte.
Zkratka CQRS znamená Command Query Responsibility Segregation. Command vyjadřuje záměr něco provést, například ChangeOrderStatus, a může být odmítnut kvůli business pravidlům. Query, například GetOrderDetail, nic nemění a sestaví odpověď potřebnou pro detail objednávky.
Oddělení je nejprve logické: obě cesty mohou běžet v jedné PHP aplikaci nad jedinou databází. Samostatný read model, asynchronní projekce nebo odlišné úložiště jsou pokročilé možnosti, nikoli definice CQRS.
Jaký problém řeší
Jeden univerzální model bývá současně příliš složitý pro zápis a nepohodlný pro čtení.
Zápisová strana chrání pravidla a konzistenci, zatímco obrazovky a API často potřebují předpočítaná nebo jinak seskupená data. CQRS jejich potřeby nepředstírá jako jednu totožnou operaci.
- pojmenování businessových změn jako konkrétních úloh místo obecného UpdateOrder
- zabránění tomu, aby čtecí endpointy omylem měnily stav nebo spouštěly vedlejší účinky
- jednodušší DTO a dotazy pro detail, přehled nebo export bez načítání celého doménového modelu
- soustředění validace, autorizace změny a invariantů do command handleru
- možnost optimalizovat hodně vytížené čtení nezávisle na méně častých zápisech
- jasnější testování: command se ověřuje podle výsledné změny, query podle vrácených dat
Praktický příklad
ChangeOrderStatus versus GetOrderDetail
Administrátor e-shopu odešle ChangeOrderStatus s ID objednávky a cílovým stavem. ChangeOrderStatusHandler načte objednávku, ověří povolený přechod, oprávnění a další pravidla, provede změnu a uloží ji v databázové transakci. Command popisuje záměr; nemusí vracet celý aktualizovaný detail objednávky.
GetOrderDetailHandler naopak nic nemění. Jedním dotazem může načíst údaje o objednávce, zákazníkovi, platbě a dopravě přímo do OrderDetail DTO. V jednoduché variantě oba handlery používají tutéž databázi. Až když má čtení jiné nároky, může GetOrderDetail používat samostatnou projekci aktualizovanou asynchronně.
PHP
final readonly class ChangeOrderStatus
{
public function __construct(
public string $orderId,
public OrderStatus $newStatus,
) {}
}
final class ChangeOrderStatusHandler
{
public function __invoke(ChangeOrderStatus $command): void
{
$order = $this->orders->get($command->orderId);
$order->changeStatusTo($command->newStatus);
$this->orders->save($order);
}
}
final readonly class GetOrderDetail
{
public function __construct(public string $orderId) {}
}
final class GetOrderDetailHandler
{
public function __invoke(GetOrderDetail $query): OrderDetail
{
return $this->orderDetails->find($query->orderId);
}
}
Zjednodušený diagram
Dvě cesty se společným cílem, ale odlišnou odpovědností
Textová alternativa diagramu: Command → command handler → write model → změna stavu. Query → query handler → read model → odpověď. Jedna konkrétní implementace může oba modely ukládat do stejné databáze.
- Command ChangeOrderStatus vyjadřuje požadavek změnit objednávku a nese data nezbytná pro toto rozhodnutí.
- Command handler a write model Handler koordinuje use case; write model ověřuje pravidla, provede změnu a uloží ji v konzistentní hranici.
- Query GetOrderDetail žádá o konkrétní pohled na data a nesmí při vyhodnocení měnit businessový stav.
- Query handler a read model Handler sestaví odpověď z tabulek, view, repliky nebo samostatné projekce podle složitosti systému.
- Aktualizace čtecí strany Při jedné databázi může být výsledek dostupný okamžitě. Samostatná projekce se často aktualizuje asynchronně a může být krátce pozadu.
Hlavní části a principy
Oddělení operací neznamená povinné rozdělení infrastruktury.
CQRS lze zavést po use cases a jen v části systému, kde rozdíl mezi rozhodováním a čtením přináší srozumitelnost nebo provozní výhodu.
Command
Pojmenovaný požadavek na změnu stavu, typicky v rozkazovacím způsobu: ConfirmOrder nebo ChangeOrderStatus. Může selhat, pokud vstup, oprávnění nebo aktuální stav pravidlo nesplní.
Query
Požadavek na data bez změny businessového stavu. Vrací DTO nebo jiný kontrakt odpovědi přizpůsobený obrazovce či API, nikoli nutně entity zápisového modelu.
Command a query handler
Každý handler obsluhuje konkrétní use case. Command handler koordinuje pravidla a zápis, query handler sestavuje výsledek; handler není důvod rozdrobit každou triviální operaci do mnoha prázdných vrstev.
Write model
Reprezentuje způsob, jak bezpečně měnit stav. Ve složitější doméně může používat agregáty a invarianty z Domain-Driven Designu; u jednoduchého modulu může jít o přímočarou aplikační službu.
Read model
Je navržen pro čtecí scénáře. Může být SQL dotaz nad stejnými tabulkami, databázové view, read replica nebo předpočítaná projekce; samostatná databáze není podmínkou.
Jednoduchá a pokročilá varianta
Jednoduché CQRS oddělí názvy, vstupní modely a handlery v jednom procesu. Pokročilá varianta přidá vlastní read store, události a asynchronní synchronizaci pouze tehdy, když cenu této infrastruktury ospravedlní provozní požadavky.
Výhody a omezení
Lepší zaměření modelů výměnou za více explicitních cest.
Možné přínosy
- commandy pojmenovávají záměr a drží pravidla změny na jednom dohledatelném místě
- čtecí DTO nemusí kopírovat složitost zápisového modelu ani ORM mapování
- čtení a zápis lze podle potřeby samostatně ladit, zabezpečit a škálovat
- oddělené testy lépe vyjadřují, zda se ověřuje rozhodnutí, nebo pouze tvar dat
Omezení a časté chyby
- dvojice tříd a rozhraní pro každý jednoduchý CRUD endpoint může přidat více ceremonie než hodnoty
- samostatný read store přináší synchronizaci, monitoring, opožděná data a obnovu projekcí
- query, která při čtení skrytě zapisuje nebo odesílá zprávu, porušuje očekávané oddělení
- command pojmenovaný UpdateEntity s desítkami volitelných polí nevyjadřuje businessový záměr
- frameworkový command bus může dispatch usnadnit, ale sám o sobě CQRS ani správné hranice nevytváří
Kdy přístup použít
CQRS patří tam, kde se potřeby čtení a zápisu skutečně rozcházejí.
Přístup dává smysl u objednávek, plateb, rezervací nebo administrací s významnými stavovými přechody a zároveň s mnoha přehledy. Začít lze pouze oddělenými use cases v jednom modulárním monolitu. Samostatné procesy a úložiště jsou další rozhodnutí založená na výkonu, škálování a odolnosti, ne podmínka vzoru.
CQRS nevyžaduje Event Sourcing, dvě databáze, RabbitMQ ani mikroservisy. Event Sourcing lze s CQRS kombinovat, protože projekce přirozeně poskytují read model, ale jde o dva různé koncepty. U jednoduchého CRUD bez složitých pravidel a rozdílných čtecích potřeb CQRS obvykle jen zvětší počet tříd.
Na co myslet
Hranice musí být zřejmá z chování, ne jen z názvu adresáře.
Nejdřív je potřeba popsat use case a konzistenční očekávání, teprve potom vybrat bus, projekci nebo další infrastrukturu.
- pojmenovat command podle businessového záměru a query podle požadovaného výsledku
- udržet query bez změny businessového stavu a překvapivých vedlejších účinků
- provádět pravidla, autorizaci změny a zápis v jasné transakční hranici
- u asynchronního read modelu určit přijatelné zpoždění a chování UI po právě provedené změně
- sledovat lag a chyby projekce a umět read model bezpečně znovu sestavit
- ověřit, zda oddělení skutečně zjednodušilo změnu, nebo pouze znásobilo průchozí vrstvy
Časté otázky
CQRS v praxi
Vyžaduje CQRS Event Sourcing?
Ne. CQRS odděluje command a query operace. Event Sourcing je způsob ukládání stavu jako historie událostí; oba koncepty lze použít nezávisle i společně.
Musí mít CQRS dvě databáze?
Ne. Read i write model mohou mít odlišný kód a přitom používat stejné tabulky v jediné databázi. Druhé úložiště je až pokročilá optimalizace se samostatnými náklady.
Musí command procházet frontou zpráv?
Ne. Command handler lze zavolat synchronně v jednom PHP procesu. Asynchronní zpracování se volí kvůli konkrétní latenci, odolnosti nebo kapacitě, nikoli kvůli definici CQRS.
Může query číst přímo SQL?
Ano. Query handler může použít přímočarý SQL dotaz a vrátit DTO. Nemusí načítat doménové entity, pokud pouze skládá data pro čtení.
Kdy je CQRS zbytečné?
Typicky u jednoduchého CRUD modulu, kde jsou čtecí a zápisové potřeby podobné, pravidel je málo a oddělení by pouze přidalo handlery, mapování a údržbu.
Jak přistupuji k architektuře aplikací
Command a query odděluji jen tam, kde chrání skutečné rozhodnutí nebo provozní potřebu.
U složitějších procesů pojmenovávám use cases a jejich konzistenční hranice. Infrastrukturu přidávám podle konkrétního rizika, nikoli podle nálepky vzoru.