Doctrine 2:stavy entit a transakce
V rámci seriálu o Doctrine 2 dnes budeme pokračovat v tématech nakousnutých posledně. Podíváme se podrobněji na stavy entit v průběhu jejich života. Ukážeme si nejdůležitější fungování UnitOfWork i práci s transakcemi a zamykáním.
Seriál Doctrine 2
- Doctrine 2: pokročilá definice entit
- Doctrine 2: načítání, ukládání a mazání
- Doctrine 2:stavy entit a transakce
- Doctrine 2: asociace
- Doctrine 2: práce s asociacemi
Stavy entit
Entity mohou být z pohledu EntityManageru v různých stavech, podle toho, jestli jsou nové, persistované nebo třeba smazané. Informace o stavu se uchovává mimo entitu, drží si ho sám EntityManager, přesněji řečeno UnitOfWork. Podle stavu každé entity se EntityManager rozhoduje, jak bude s entitou nakládat a co je s ní potřeba dělat.
Doctrine 2 rozlišuje celkem čtyři různé stavy entit – nová, spravovaná, odpojená a smazaná:
- Nová (NEW)
- Nová je taková entita, kterou jsme zrovna vytvořili operátorem
newa dosud jsme na ni nezavolali$em->persist($entity), takže není pod kontrolou EntityManageru. - Spravovaná (MANAGED)
- Když persistujeme novou entitu nebo z databáze načteme entitu již existující, je ve stavu Spravovaná. Jakékoliv změny, které v ní provedeme, se při nejbližším zavolání
$em->flush()promítnou do databáze. - Odpojená (DETACHED)
- Spravovanou entitu můžeme od EntityManageru odpojit zavoláním
$em->detach($entity). Daná instance entity sice v aplikaci nadále existuje až do konce požadavku, ale jakékoliv změny v ní provedené se nikam nepromítnou a po skončení běhu skriptu se ztratí. V databázi ale záznam existuje i nadále a lze jej kdykoliv načíst dalším zavoláním$em->find(). Odpojenou entitu dostanete zpátky pod kontrolu EntityManageru zavoláním$em->merge($entity). - Smazaná (REMOVED)
- Pokud nad spravovanou entitou zavoláme
$em->remove($entity), přepne se do stavu Smazaná a při nejbližším zavolání$em->flush()se vymaže i z databáze. V paměti ale její instance zůstane i nadále až do konce požadavku.
Aktuální stav libovolné entity ověříte zavoláním metody $em->getUnitOfWork()->getEntityState($entity). Vrací se hodnota jedné z následujících symbolických konstant:
UnitOfWork::STATE_NEWUnitOfWork::STATE_MANAGEDUnitOfWork::STATE_DETACHEDUnitOfWork::STATE_REMOVED
Následuje krátká ukázka životního cyklu instance článku pro demonstraci jednotlivých stavů entity:
$uow = $em->getUnitOfWork(); $article = new Article; echo $uow->getEntityState($article); // STATE_NEW $em->persist($article); echo $uow->getEntityState($article); // STATE_MANAGED $em->detach($article); echo $uow->getEntityState($article); // STATE_DETACHED $article = $em->merge($article); echo $uow->getEntityState($article); // STATE_MANAGED $em->remove($article); echo $uow->getEntityState($article); // STATE_REMOVED
Ukázali jsme si jen nejčastější změny stavů entit. Pokud vás zajímají podrobně všechny povolené přechody, doporučuji si prostudovat příslušnou kapitolu oficiální dokumentace.
UnitOfWork a ukládání změn
Několikrát už padla řeč na UnitOfWork. Ten leží na pozadí EntityManageru a zjednodušeně si jej můžete představit jako frontu všech změn, které chceme uložit do databáze.
Pokud tedy persistujete novou entitu pomocí $em->persist($entity), uděláte nějakou změnu v již persistované entitě nebo třeba smažete entitu přes $em->remove($entity), neprovádí se hned v databázi příslušný SQL dotaz, ale vše se jen kupí v UnitOfWork. Do databáze se pak odešle vše najednou, když zavoláte $em->flush().
Takový přístup přináší několik výhod. První je v tom, že se všechny databázové operace provádějí v relativně krátkém časovém úseku, takže případné zamykání je použito jen na nezbytně nutnou dobu.
Doctrine 2 si při zavolání $em->flush() umí lépe rozvrhnout a výkonnostně zoptimalizovat, co, jak a v jakém pořadí vlastně do databáze pošle. Což se systému daří někdy lépe, jindy hůře, občas to ale bohužel vede k vyloženě nešťastnému a nečekanému chování, se kterým je potřeba počítat.
Můžete také rozšiřovat Doctrine 2 o další dodatečnou funkčnost, na volání $em->flush() můžete navěsit vlastní kód a ještě před samotným promítnutím naplánovaných změn do databáze něco udělat, něco změnit. O událostech se ale budeme bavit někdy jindy.
Další výhodou fronty v UnitOfWork je, že pokud si to v průběhu skriptu rozmyslíte, můžete vzít snadno všechny změny zpět. Buďto prostě vůbec nezavoláte $em->flush(), anebo o něco čistěji zavoláte $em->clear().
Vyčištění a uzavření EntityManageru
Zmiňovaná metoda $em->clear() způsobí, že se odpojí všechny entity pod kontrolou EntityManageru, vyčistí se IdentityMap i UnitOfWork a EntityManager je prázdný stejně jako na začátku běhu skriptu.
Kromě vyčištění existuje ještě i možnost EntityManager úplně zavřít metodou $em->close(). Od toho okamžiku už nelze nad uzavřeným EntityManagerem provádět žádná další volání a pokud chcete ještě něco s daty dělat, musíte si instancovat nový EntityManager.
EntityManager se občas uzavře i automaticky, a to zejména v případech, kdy uvnitř něj dojde k vyhození jakékoliv výjimky. Typicky pokud pošlete do databáze nějaký chybný dotaz, porušíte třeba nějaký databázový constraint. Tohle Doctrine 2 sama nijak nekontroluje, databáze vyhodí PDOException, uzavře se EntityManager a jakákoliv další volání EntityManageru vedou k chybě.
To je poměrně nepříjemné chování, se kterým musíte při návrhu své aplikace počítat. Primárně tedy psát entity tak, aby si všechna omezení kontrolovaly samy a do databáze posílaly pouze bezproblémové SQL dotazy. Sekundárně pak počítat s tím, že při vyhozené PDOException se vám současný EntityManager uzavře, nejspíš budete muset instancovat nový a s ním dovést aplikaci do bezpečného fallbacku.
Implicitní transakce
Doctrine 2 automaticky využívá transakce ve všech databázích, které je podporují. Ve výchozí situaci se o transakci nemusíte vůbec starat. Doctrine 2 ji po zavolání $em->flush() na začátku zpracování všech změn naplánovaných v UnitOfWork sama otevře a po úspěšném provedení commitne. V následujícím kódu se tak nový článek uloží do databáze v transakci:
$article = new Article;
$article->setTitle('Lorem ipsum');
$em->persist($article);
$em->flush();
Když během zpracování dat dojde k jakékoliv chybě, provede se nad databází rollback, uzavře se EntityManager a vyhodí se výjimka.
Explicitní řízení transakcí
Pokud ale chcete mít nad transakcemi kontrolu, můžete si je řídit sami. K dispozici pro to jsou následující metody:
$em->getConnection()->beginTransaction()$em->getConnection()->commit()$em->getConnection()->rollback()
Jakmile explicitně otevřete transakci zavoláním $em->getConnection()->beginTransaction(), od toho okamžiku se už neprovádí implicitní commit ani rollback uvnitř metody $em->flush() a musíte si vše řídit sami:
$em->getConnection()->beginTransaction();
try {
$article = new Article;
$article->setTitle('Lorem ipsum');
$em->persist($article);
$em->flush();
$em->getConnection()->commit();
} catch (Exception $e) {
$em->getConnection()->rollback();
$em->close();
throw $e;
}
Výhodou je, že můžete v rámci jedné transakce postupně odeslat i více různých sad změn postupným voláním více různých $em->flush(). Bez explicitního řízení transakcí se neobejdete třeba v případě, kdy chcete v rámci prováděných změn spouštět přímo nad databází nějaké vlastní DBAL operace.
Nikdy ale nesmíte zapomenout při vyhozené výjimce vedle rollbacku i uzavřít EntityManager zavoláním $em->close(). Pokud to neuděláte, zůstane vám EntityManager otevřený v nekonzistentním „bordelózním“ stavu, kde není předvídatelné jeho další chování.
Alternativní možností, jak nezapomenout na závěrečný commit či rollback, je využití metody $em->transactional():
$em->transactional(function($em) {
$article = new Article;
$article->setTitle('Lorem ipsum');
$em->persist($article);
});
Pesimistické zamykání
Doctrine 2 sama o sobě pesimistické zamykání nijak zvlášť neřeší a nechává to na konkrétní databázi a jejích mechanizmech. Nabízí pouze několik volání, jak si zamknutí od databáze vyžádat.
Zamykat lze samozřejmě jen uvnitř otevřené transakce. Tedy pouze pokud jste někdy předtím zavolali $em->getConnection()->beginTransaction(). Pokud nemáte transakci otevřenou, vyhodí se výjimka.
Pro zamykání lze zvolit jeden ze dvou módů:
Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE- Znemožní ostatním konkurenčním procesům číst nebo aktualizovat dotčené záznamy.
Doctrine\DBAL\LockMode::PESSIMISTIC_READ- Znemožní ostatním konkurenčním procesům aktualizovat dotčené záznamy nebo je zamknout v
PESSIMISTIC_WRITEmódu.
Samotné zamknutí pak lze vyžádat na některém z následujících míst:
- Při načítání entity z databáze:
$em->find('Article', 123, LockMode::PESSIMISTIC_WRITE);- Zamčením nad již načtenou entitou:
$em->lock($article, LockMode::PESSIMISTIC_WRITE);- Nastavením módu nad vytvářeným DQL dotazem:
$query->setLockMode(LockMode::PESSIMISTIC_WRITE);
Optimistické zamykání
Optimistické zamykání využijete v případě, že potřebujete zachovat konzistenci dat napříč více různými požadavky uživatele. Pro lepší pochopení použiji konkrétní příklad, kdy nechcete, aby danému uživateli někdo změnil data v databázi, zatímco je například bude sám upravovat ve formuláři.
Princip je takový, že entita má u sebe jakýsi counter, který se při každém uložení záznamu inkrementuje. Pokud našemu uživateli vykresluji formulář, zapamatuju si u něj i poslední hodnotu counteru, která byla v okamžiku vykreslení formuláře. Když pak uživatel formulář po nějaké chvíli odešle, zkontroluji, zda má entita v databázi stále stejnou hodnotu daného counteru. Pokud ano, data z formuláře klidně uložím, v opačném případě vyhodím výjimku a uživateli třeba zobrazím chybovou hlášku, že během jeho editace už záznam upravil někdo jiný.
V první fázi je tedy nutné v entitě definovat sloupec se zmíněným counterem. Pro ten se používá anotace @version. Nenechte se zmást názvem této anotace, s versionable behaviour, jaké možná znáte z předchozí verze Doctrine, to nemá vůbec nic společného.
class Article
{
// ...
/** @version @column(type="integer") */
private $version;
/** Returns last counter number */
public function getVersion()
{
return $this->version;
}
// ...
}
Místo typu integer můžete teoreticky použít také datetime, tam ale hlavně u hodně navštěvovaných aplikací riskujete, že se dva uživatelé trefí do stejného času a kontrola u nich selže.
Při vykreslování formuláře se na daný článek dotážete stejně jako kdykoliv jindy. Důležité ovšem je, abyste si uchovali hodnotu jeho version, ať už v sessions nebo ve skrytém formulářovém poli:
$article = $em->find('Article', 123);
echo '';
echo '';
To nejdůležitější pak přichází po odeslání formuláře. Ve skriptu si musíme samozřejmě nejprve načíst instanci daného článku pomocí metody find(). A tady máme k dispozici další dva parametry. V prvním z nich určíme, že nám jde o optimistické zamykání, v druhém potom verzi záznamu, kterou očekáváme:
$id = (int)$_POST['id'];
$version = (int)$_POST['version'];
$article = $em->find('Article', $id, \Doctrine\DBAL\LockMode::OPTIMISTIC, $version);
// tady jsou nějaké další změny v článku
$em->flush();
Pokud je v tomto okamžiku aktuální hodnota version v databázi jiná, než jakou měl doteď uživatel ve svém formuláři, vyhodí se výjimka OptimisticLockException, kterou bychom následně měli nějak zpracovat.
Školení Google Analytics pro pokročilé

- Jak využít nové funkce Google Analytics
- Vyhodnocování kampaní díky používání Multichannel funnels
- Kde návštěvníci vašeho webu utíkají z objednávacího procesu.
- Nebudete opakovat časté chyby při vyhodnocování dat o návštěvnosti.
Detailní informace o školení Google Analytics pro pokročilé »
Seriál Doctrine 2
- Doctrine 2: pokročilá definice entit
- Doctrine 2: načítání, ukládání a mazání
- Doctrine 2:stavy entit a transakce
- Doctrine 2: asociace
- Doctrine 2: práce s asociacemi
Přehled názorů
Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.