Přejít k navigační liště

Zdroják » Různé » Doctrine 2:stavy entit a transakce

Doctrine 2:stavy entit a transakce

Články Různé

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.

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.

Komentáře

Subscribe
Upozornit na
guest
13 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Cechjos

Dobrý den, měl bych „dotaz“ k následující části:

„…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.“

Nebyl by nějaký konkrétní příklad neočekávaného chování, na který jste při používání Doctrine2 UnitOfWork narazil?

Jinak díky za pěkný seriál. .)

Cechjos

Díky za odpověď; myslel jsem, že mj. právě o správné pořadí dotazů by se měl Unit of Work starat.

Tomas

Dobry den.
Je mozne vygenerovat entity ak uz mam databazu? Aby som nemusel definovat entity pre tu spustu tabuliek :-)

v6ak

Asi to půjde, stačilo krátce pohledat: http://www.doctrine-project.org/documentation/manual/2_0/en/introduction
Ale může se samozřejmě stát, že vygenerované entity nebudou úplně ono. Záleží to na konkrétním příkladě.

Tomas

tam som hladal, ale ak myslis ORM:generate-entities, tak tam pisu, ze generuje entity z mapovacich informacii, takze predpokladam, ze to nebude priamo z DB, takze si musim nadefinovat xml alebo yaml, ak to teda dobre chapem

sebastiano19

Super, dakujem. Mam este jeden problem. Mam tabulku „users“ (PK:id_user), na ktoru robim JOIN tabulky „auths“, ktora ma kompozitny PK (PK:id_driver,lo­ginname FK:id_user FK:id_driver), co je v pohode. Ale v tom istom dotaze chcem spravit JOIN aj na „auths“ a viazana tabulka je authdrivers (PK:id_driver), kde mi doctrine zahlasi: Exception: Single id is not allowed on composite primary key in entity UsersModuleEn­titiesAuthdri­ver.
Neviem, kde moze byt chyba, asi to zle chapem…
blizsi popis problemu: http://forum.nette.org/cs/5291-doctrine-2-join-na-tabulku-s-kompozitnym-pk

Lenka

Dobrý den, jde ječte to načtení entit z databáze již vytvořené?jsem začátečník s Doctrine, takže jsem ráda za každou radu

stefano

Používa entity manager pri spracovaní transakcie zámky na načítané záznamy????
Ide o to či sa viacero súčasných procesov nad jedným záznamom nepobije.

drevolution

Tohle je věc, kterou si musí každý databázový systém řešit nejlépe sám. PostgreSQL to řeší přes MVCC (http://wiki.postgresql.org/wiki/MVCC), MySQL to na InnoDB řeší AFAIK pomocí zamykání tabulek. Jak je to implementované nad MyISAM netuším. Oracle IMHO transakce podporuje stejně jako PostgreSQL. Záleží tedy, jaký databázi používáte.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.