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
new
a 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_NEW
UnitOfWork::STATE_MANAGED
UnitOfWork::STATE_DETACHED
UnitOfWork::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ů:
DoctrineDBALLockMode::PESSIMISTIC_WRITE
- Znemožní ostatním konkurenčním procesům číst nebo aktualizovat dotčené záznamy.
DoctrineDBALLockMode::PESSIMISTIC_READ
- Znemožní ostatním konkurenčním procesům aktualizovat dotčené záznamy nebo je zamknout v
PESSIMISTIC_WRITE
mó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, DoctrineDBALLockMode::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.
Přehled komentářů