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 (12 dílů)

  1. Doctrine 2: úvod do systému 21.7.2010
  2. Doctrine 2: základní definice entit 4.8.2010
  3. Doctrine 2: pokročilá definice entit 11.8.2010
  4. Doctrine 2: načítání, ukládání a mazání 26.8.2010
  5. Doctrine 2:stavy entit a transakce 9.9.2010
  6. Doctrine 2: asociace 23.9.2010
  7. Doctrine 2: práce s asociacemi 14.10.2010
  8. Doctrine 2: DQL 3.11.2010
  9. Doctrine 2: Query Builder a nativní SQL 18.11.2010
  10. Doctrine 2: události 9.12.2010
  11. Doctrine 2: událostní handlery 13.1.2011
  12. Architektura aplikace nad Doctrine 2 23.2.2012

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ís­t:

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.

Provozuje vývojářskou firmu Medio Interactive. Vystudoval informační a znalostní inženýrství na VŠE, kde stále příležitostně přednáší o tvorbě webů.

Komentáře: 13

Přehled komentářů

Cechjos Re: Doctrine 2:stavy entit a transakce
Jan Tichý Re: Doctrine 2:stavy entit a transakce
Cechjos Re: Doctrine 2:stavy entit a transakce
Jan Tichý Re: Doctrine 2:stavy entit a transakce
Tomas Generovanie entit z databaze
v6ak Re: Generovanie entit z databaze
Tomas Re: Generovanie entit z databaze
Jan Tichý Re: Generovanie entit z databaze
sebastiano19 Re: Generovanie entit z databaze
Jan Tichý Re: Generovanie entit z databaze
Lenka Re: Generovanie entit z databaze
stefano Uzamykanie riadkov tabuľky
drevolution Re: Uzamykanie riadkov tabuľky
Zdroj: https://www.zdrojak.cz/?p=3322