Devel.cz Lupa Měšec Podnikatel Root Zdroják.cz DigiZone Slunečnice Vitalia TopDrive KupDnes Navrcholu NovýTarif Dobrý web Weblogy Woko Jagg Computer.cz SK: MojeLinky

Hlavní navigace

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.

Tweetni to Twitter Jaggni to! Jagg Del.icio.us Delicious

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ů:

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_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, \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.

Jan Tichý

Jan Tichý

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ů.

Školení Google Analytics pro pokročilé

DW - Školení Google Analytics
  • 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é »

Přehled názorů

Re: Doctrine 2:stavy entit a transakce
Josef Čech 9. 9. 2010 11:18
Nový
└ 
Re: Doctrine 2:stavy entit a transakce
Jan Tichý 9. 9. 2010 14:32
Nový
 
└ 
Re: Doctrine 2:stavy entit a transakce
Josef Čech 9. 9. 2010 15:17
Nový
 
 
└ 
Re: Doctrine 2:stavy entit a transakce
Jan Tichý 11. 9. 2010 21:56
Nový
Generovanie entit z databaze
Tomas 11. 9. 2010 16:08
Nový
├ 
Re: Generovanie entit z databaze
Vít Šesták (v6ak) 11. 9. 2010 16:13
Nový
│
└ 
Re: Generovanie entit z databaze
Tomas 11. 9. 2010 16:42
Nový
└ 
Re: Generovanie entit z databaze
Jan Tichý 11. 9. 2010 21:53
Nový
 
└ 
Re: Generovanie entit z databaze
Tomáš Bajzecer 12. 9. 2010 19:53
Nový
 
 
└ 
Re: Generovanie entit z databaze
Jan Tichý 12. 9. 2010 20:00
Nový
Uzamykanie riadkov tabuľky
stefano 18. 9. 2010 21:19
Nový
└ 
Re: Uzamykanie riadkov tabuľky
Václav Novotný 20. 9. 2010 10:41
Nový
       

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.

Zasílat nově přidané příspěvky e-mailem