Doctrine 2: načítání, ukládání a mazání

Seriál o novinkách, které pro vývojáře v PHP přináší databázová knihovna (ORM) Doctrine 2, pokračuje. V ukázkách minulých dílů jsme se letmo dotkli Entity Manageru. Dnes se na něj podíváme podrobněji a ukážeme si základní způsoby, jak své entity načítat, ukládat a mazat.

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

Dáváme sbohem Active Recordu

Pro načítání, ukládání a mazání entit jste možná z jiných ORM zvyklí na přístup zvaný Active Record. V něm si všechny tyto akce zajišťuje sama entita svými metodami:

$article = new Article;
$article->setTitle('Lorem ipsum');
$article->save();
$product = Product::load(123);
$product->setTitle('Foo bar');
$product->save();
$category = Category::load(123);
$category->delete();

Doctrine 2 se od Active Recordu zcela oprošťuje. Nechává entitu samu o sobě nezávislou na jakémkoliv načítání, ukládání či mazání. Nezávislou na konkrétním použitém úložišti nebo obecněji způsobu persistování. Entita si tak řeší jen a pouze nastavování a vracení dat nebo jejich základní konverze a validace – prostě vlastní doménovou logiku.

Načítání, persistování a mazání se pak zajišťuje vně entity, a to s pomocí takzvaného Entity Manageru. Jenom připomenu, že instanci Entity Manageru jsme si připravili už v úvodním díle seriálu.

$article = new Article;
$article->setTitle('Lorem ipsum');
$em->persist($article);
$em->flush()
$product = $em->find('Product', 123);
$product->setTitle('Foo bar');
$em->flush();
$category = $em->find('Category', 123);
$em->remove($category);
$em->flush();

Takový přístup je čistší z hlediska celkového návrhu, protože načítání, persistování nebo mazání dovnitř entity skutečně nepatří. Entita se sama neukládá, někdo ji vezme a někam uloží. Máslo se také samo nedá do ledničky, musí ho tam někdo vložit. Ale úplně stejně může stejné máslo vzít někdo úplně jiný a vložit do úplně jiné ledničky. Anebo vyhodit do koše.

Měli byste si vrýt pod kůži i další železnou zásadu. Nejen, že entita nenačítá, neukládá či nemaže sama sebe, ale neměla by nikdy explicitně načítat, ukládat či mazat jakékoliv jiné entity.

Jinými slovy pokud uvnitř jakékoliv entity potřebujete sáhnout na Entity Manager, děláte něco špatně. Nejspíš jste si špatně navrhli architekturu celé své aplikace a do entity strkáte něco, co tam vůbec nepatří.

Kdyby se vám ale opravdu po starém dobrém Active Recordu stýskalo, můžete to snadno vyřešit jednoduchou nadstavbou nad Doctrine 2. Pro podrobnosti viz sekci „Doctrine2 and ActiveRecord“ v článku Write your own ORM on top of Doctrine2. Mějte ale na paměti, že je to méně čistý návrh, který v důsledku může svádět k dalším navazujícím chybným postupům.

Entity Manager

Entity Manager je ve své podstatě jen fasáda, která umožňuje snadný a rychlý přístup k jednotlivým částem, ze kterých je Doctrine 2 poskládána pod kapotou.

Pokud vás vnitřní architektura frameworku nezajímá a nechcete se pouštět do žádných pokročilých funkcí, vystačíte si hodně dlouho jen s Entity Managerem. Ostatní pro vás může zůstat černou skříňkou někde na pozadí.

Na druhou stranu pro plné využití Doctrine 2 je dobré vidět i trochu hlouběji. Zkusím tedy čas od času, vždy když na to přijde vhodná chvíle, alespoň stručně poodkrýt, co se za Entity Managerem skrývá.

Načítání entit

Pro načtení uložené entity z databáze je k dispozici metoda $em->find(), které se v parametrech předává název entity, kterou chceme načíst, a identifikátor požadovaného záznamu:

// Najde článek s ID 123
$article = $em->find('Article', 123);

Dejte si pozor na to, že pokud není žádný takový záznam nalezen, nevyhazuje se žádná výjimka ani jiná chybová hláška, ale vrací se  NULL.

Metoda je v podstatě jen zkratkou do Repository skryté za Entity Managerem. Úplný zápis, který v důsledku udělá totéž, tedy vypadá takto:

// Najde článek s ID 123
$article = $em->getRepository('Article')->find(123);

Složitější dotazy

Bez explicitního získání Repository už si nevystačíme, pokud se budeme chtít na entity dotazovat nějakým složitějším způsobem, než jen přes identifikátor:

// Najde uživatele s uživatelským jménem "novak"
$user = $em->getRepository('User')->findOneBy(array('username' => 'novak'));

Přitom username je název členské proměnné v entitě User, podle které hledání omezujeme. Místo nepřehledného pole v parametru můžete použít magické názvy metod:

// Najde uživatele s uživatelským jménem "novak"
$user = $em->getRepository('User')->findOneByUsername('novak');

Obdobným způsobem se můžeme dotazovat i na celou skupinu entit, které splňují zadané podmínky:

// Najde všechny dvacetileté uživatele
$users = $em->getRepository('User')->findBy(array('age' => 20));
// Najde všechny dvacetileté Nováky
$users = $em->getRepository('User')->findBy(array('age' => 20, 'surname' => 'Novák'));

Pokud chcete najít úplně všechny uživatele v systému, nabízí se k tomu metoda  findAll():

// Najde úplně všechny uživatele
$users = $em->getRepository('User')->findAll();

Pro ještě komplikovanější dotazy už musíte sáhnout po speciálním dotazovacím jazyce DQL. V opravdu krajním případě pak i po starém dobrém SQL, kde ale už riskujete nepřenositelnost aplikace mezi různými databázemi. O jazyce DQL se budeme podrobně bavit někdy jindy, teď jenom ukázka:

$query = $em->createQuery('SELECT u FROM User u WHERE u.age >= 20 AND u.age <= 30');
$users = $q->getResult();

Identity Map

Ve všech případech Doctrine 2 zajišťuje, že od jedné entity s jedním ID máte v celé aplikaci pouze jednu jedinou instanci, která se všude předává jen pomocí referencí. Nemůže se vám tedy stát, že byste měli dvě různé instance entity se stejným identifikátorem:

$first = $em->find('Article', 123);
$second = $em->find('Article', 123);
// TRUE - je to opravdu stejná instance
echo ($first === $second);

Doctrine takové chování na pozadí řídí pomocí Identity Map. Tu si můžete představit jako asociativní pole, kde je každá již načtená entita uchovávaná pod kombinací názvu své třídy a ID.

Repository se pak pokaždé, kdy je požádána o jakoukoliv entitu, nejprve podívá, jestli už ji náhodou v Identity Map nemá. Pokud ano, vrátí ji. V opačném případě se teprve dotáže do databáze.

Ale i v případě složitějších dotazů do databáze kontroluje před vrácením jejich výsledku existenci jednotlivých nalezených záznamů v Identity Map a pokud tam již jsou, tak je upřednostňuje před těmi načtenými z databáze.

Existence Identity Map není nic, co byste museli jakkoliv řešit, Doctrine 2 ji používá naprosto transparentně. Je ale dobré o ní vědět.

Vlastní repository

V případě potřeby lze standardně používanou Repository překrýt vlastní implementací s nějakou rozšířenou či pozměněnou funkčností. Krátký příklad:

class UserRepository extends DoctrineORMEntityRepository
{
    public function getAllAdminUsers()
    {
        return $this->_em->createQuery('SELECT u FROM User u WHERE u.role = "admin"')->getResult();
    }
}

Všimněte si, že novou repository je nutné pomocí speciální anotace nastavit i v definici dané entity:

/**
 * @Entity(repositoryClass="UserRepository")
 */
class User
{
    // ...
}

Následné použití je pak zřejmé:

$admins = $em->getRepository('User')->getAllAdminUsers();

Persistování entit

Místo ukládání se v Doctrine 2 a jí podobných ORM frameworcích používá výraz persistování. Rozdíl mezi těmito dvěma pojmy je filozofický i praktický.

Ukládání musíte provádět při každé provedené změně v entitě. V Active Record návrhu tak po každé změně voláte  $article->save().

Persistování naopak vychází z toho, že entity persistujete pouze jednou po vytvoření její nové instance:

$article = new Article;
$em->persist($article);
$em->flush();

Od tohoto okamžiku už je tato instance pod kontrolou Entity Manageru. Je vedená, jakože se má persistovat napříč různými požadavky uživatelů. A to tak dlouho, dokud zase explicitně neřeknete jinak. Nemusíte tedy volat $em->persist($article) znovu po každé změně. Stačí to jen při vytváření nové instance.

Odebírání entit

Persistované entity se smažou z databáze pomocí metody  $em->remove($article):

$article = $em->find('Article', 123);
$em->remove($article);
$em->flush();

Samotná třída $article pak sice až do konce běhu daného skriptu existuje a má v sobě stále svůj obsah, není ale už v evidenci Entity Manageru, takže jakékoliv další změny se už do databáze nijak nepromítnou a při příštím požadavku na tuto entitu už nebude vůbec existovat.

Potvrzení a odeslání změn

Jak je patrné i z příkladů výše, pro potvrzení všech provedených změn musíte pokaždé nakonec zavolat metodu  $em->flush().

Typický případ je ten, že během jednoho běhu skriptu provedete více různých změn, které se řadí jakoby do fronty, a pak je všechny najednou potvrdíte a odešlete do databáze jedním  $em->flush():

// Vytvoříme a persistujeme nový článek
$article = new Article;
$em->persist($article);
// Změníme titulek produktu s ID 123
$product = $em->find('Product', 123);
$product->setTitle('Foo bar');
// Odstraníme kategorii s ID 123
$category = $em->find('Category', 123);
$em->remove($category);
// Všechny změny výše potvrdíme a pošleme do databáze
$em->flush();

Pokud byste $em->flush() nezavolali, tak se žádná z těchto změn do databáze neuloží a všechny provedené změny ve všech persistovaných entitách se po dokončení aktuálního dotazu ztratí.

Zmiňovaná fronta změn v Doctrine 2 opravdu existuje a je reprezentovaná návrhovým vzorem UnitOfWork. O něm, stejně jako o transakčním zpracování nebo různých stavech entit, ale zase až příště.

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

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 17

Přehled komentářů

Marek Šudák Čistý návrh entity
drevolution Re: Čistý návrh entity
Jan Tichý Re: Čistý návrh entity
Tharos Re: Čistý návrh entity
v6ak Re: Čistý návrh entity
pavel Re: Doctrine 2: načítání, ukládání a mazání
Jan Tichý Re: Doctrine 2: načítání, ukládání a mazání
Pavel Re: Doctrine 2: načítání, ukládání a mazání
Tharos Díky za seriál
ameeck Dodatek k poslednímu odstavci
drevolution Re: Dodatek k poslednímu odstavci
v6ak Jedna instance na entitu
drevolution Re: Jedna instance na entitu
v6ak Re: Jedna instance na entitu
drevolution Re: Jedna instance na entitu
tomp Random() v Doctrine 2
Bublafus Hledání podle nullové hodnoty
Zdroj: https://www.zdrojak.cz/?p=3312