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

Zdroják » Databáze » Doctrine 2: načítání, ukládání a mazání

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

Články Databáze, PHP

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.

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

Komentáře

Subscribe
Upozornit na
guest
17 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Marek Šudák

„Entita se sama neukládá, někdo ji vezme a někam uloží.“
To je správně. Zároveň ale entita v anotaci obsahuje informaci, k jaké databázové tabulce se váže, což by IMHO neměla – máslo taky neví, jestli patří do ledničky.

drevolution

A když nepoužijeme anotace, ale zapíšeme mapování do XML? Anotace se jménem tabulky mi přijde jako informace pro toho, kdo chce s máslem něco dělat, ne pro samotné máslo.

Tharos

Máslo samo to sice neví, ale má to na sobě napsané, v případě mého exempláře doma: „Skladujte při teplotě od 4°C do 8°C“ :). Takže i máslo má v podstatě anotaci :).

v6ak

Ale už třeba neví, jestli patří do mé ledničky, nebo do ledničky souseda :)

pavel

[cite]Jinými slovy pokud uvnitř jakékoliv entity potřebujete sáhnout na Entity Manager, děláte něco špatně.[/cite]
jak potom docílím něčeho jako:
$articleEntiti­es=$categoryEn­tity->getArticleEn­tities();
?
Musím pokaždé někde explicitně volat Entity Manager a vyžádat si $articleEntities pro tuhle $categoryEntity?
Omlouvám se, pokud jsem tohle přehlédl v předchozích článcích. Ale ani teď to tam nevidím.

Pavel

Díky za odpověď i seriál. Dost mi rozšiřuje obzory.

Tharos

Děkuji autorovi za moc příjemné počtení, Doctrine 2 (oproti první verzi) se pro mě též začíná stávat velmi inspirativní. Tak už aby byla na světě stabilní verze :).

ameeck

K poslednímu odstavci by bylo vhodné přidat hlavně důvod – a to výkon.
Doctrine vygeneruje potřebné dotazy k provedení flush() tak, abych jich bylo co nejméně. Např. rozšířené INSERTy, DELETE z jedné tabulky v jednom dotazu, etc.

drevolution

Tak on není důvodem jenom výkon (i když je velice důležitý). Stejně tak je třeba zajistit, aby se dotazy provedly ve správném pořadí či abychom nemuseli mít dlouho otevřené transakce.

v6ak

Pokud se Doctrine stará, aby měl jednu instanci na entitu, pak si vše drží až do ukončení? Jinak si to představit dost dobře nedovedu, v PHP neznám WeakReference apod.

drevolution

Ano, všechny načtené instance entit jsou v Entity manageru po celou dobu běhu skriptu. Osobně v tom nevidím žádný problém.

v6ak

V případě typického webového použití to problém obvykle opravdu není. Ale třeba u dávkového skriptu to problém být může.

drevolution

Ano, to pak opravdu může být problém. Pokud bys chtěl mermomocí Doctrinu používat, tak pak můžeš clearovat entity manager a unsetovat nepotřebné instance. Nicméně z vlastní zkušenosti mohu doporučit v takovýhle případech se na Doctrine vykašlat a používat čisté SQL (třeba přes Doctrine DBAL). Za tu ztrátu výkonu práce s entitami IMHO ani trochu nestojí.

tomp

Existuje v Doctrine 2 nějaká obdoba funkce RANDOM() z Doctrine 1?

Z Doctrine teprve začínám a narazil na problém náhodného výběru kolekce objektů. Zatím jsem nenašel elegantní způsob jak toho docílit. Zjistil jsem, že v Doctrine 1 existovala funkce RANDOM, která ale v Doctrine 2 nelze využít.

Bublafus

Super články!
Měl bych ale jeden dotaz – Jde pomocí entityManageru a metody findBy hledat sloupečky, které mají hodnotu NULL?

Díky;)

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.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.