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

Zdroják » Databáze » Doctrine 2: práce s asociacemi

Doctrine 2: práce s asociacemi

Články Databáze

Dnes budeme pokračovat v tématu asociací v Doctrine 2. Představíme si možnosti kaskádového peristování, odpojování a mazání. Podíváme se podrobněji na kolekce a práci s nimi. Nejprve si ale ukážeme správné postupy při definicích getterů, setterů a dalších obslužných funkcí pro manipulaci s asociacemi.

Nálepky:

Základní gettery a settery

Abyste mohli s asociacemi nakládat zvenku entity, musíte jim definovat alespoň základní gettery a settery.

U jednosměrných asociací dává samozřejmě smysl definovat getter pouze na vlastnící straně, u obousměrných asociací pak můžeme gettery definovat na obou stranách:

/** @entity */
class Category
{
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category")
     */
    private $articles;
    /**
     * @return DoctrineCommonCollectionsCollection;
     */
    public function getArticles()
    {
        return $this->articles;
    }
}
/** @entity */
class Article
{
    /**
     * @manyToOne(targetEntity="Category", inversedBy="articles")
     * @joinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;
    /**
     * @return Category
     */
    public function getCategory()
    {
        return $this->category;
    }
}

Použití je pak poměrně průhledné:

// na straně kategorie
$category = $em->find('Category', 123);
foreach ($category->getArticles() as $article) {
    echo $article->getTitle();
}
// na straně článku
$article = $em->find('Article', 123);
echo $article->getCategory()->getName();

U definice setterů je strašně důležité rozlišovat vlastnící a inverzní stranu asociace. Důležitý je totiž vždy pouze setter na vlastnící straně! Toto pravidlo si velice dobře zapamatujte!

/** @Entity */
class Article
{
    // ...
    public function setCategory(Category $category = NULL)
    {
        $this->category = $category;
    }
}

Přiřazení článku do kategorie se pak provede jednoduše takto:

$category = $em->find('Category', 123);
$article = $em->find('Article', 123);
$article->setCategory($category);
$em->flush();

Setter na inverzní straně

Proč je to tak důležité, aby byl setter vždy na vlastnící straně? Důvod je prostý – při ukládání změn v asociacích během zavolání $em->flush() se Doctrine 2 dívá vždy jen a výhradně na vlastnící stranu a asociaci uloží podle toho, co je nastaveno právě na vlastnící straně.

Pokud byste místo toho měli nějaký setter (respektive v případě kolekcí spíše „adder“) na inverzní straně a provedli změnu v asociaci jen na inverzní straně, tak bude Doctrine 2 takovou změnu zcela ignorovat a vůbec ji při zavolání $em->flush() do databáze nepromítne. Následující kód tedy nebude fungovat podle očekávání a provedená změna se do databáze vůbec neuloží:

/** @Entity */
class Category
{
    // ...
    public function addArticle(Article $article)
    {
        $this->articles->add($article);
    }
}
// a samotné provedení změny, které nebude fungovat:
$category = $em->find('Category', 123);
$article = $em->find('Article', 123);
$category->addArticle($article);
$em->flush();

Co s tím? Máte vcelku dvě možnosti. První je udělat vše průhledné, setter definovat opravdu jen na vlastnící straně, zatímco inverzní stranu necháte bez jakéhokoliv setteru.

Pokud ale na inverzní straně z jakéhokoliv důvodu opravdu setter chcete (například abyste si v rámci aktuálního požadavku udrželi konzistenci v entitách na obou stranách asociace), musíte uvnitř daného setteru explicitně zajistit, aby se aktualizovala i asociace na vlastnící straně. Pak bude fungovat vše bez problémů:

/** @Entity */
class Category
{
    // ...
    public function addArticle(Article $article)
    {
        $this->articles->add($article);
        $article->setCategory($this);
    }
}

Kolekce

V příkladu výše stojí za povšimnutí, že kolekce v entitách, například $articles v entitě Category, jsou uloženy jako implementace rozhraní DoctrineCommonCollectionsCollection, ve výchozím chování zpravidla jako instance třídy DoctrineCommonCollectionsArrayCollection nebo  DoctrineORMPersistentCollection.

Tak je tomu samozřejmě ale jen v případě, kdy nám Doctrine 2 načte a vrátí nějakou již uloženou entitu. Naproti tomu pokud si sami vytvoříme úplně novou instanci přes operátor new, je na začátku daná proměnná $articles logicky úplně prázdná.

Silně proto doporučuji všechny kolekce v entitách důsledně explicitně inicializovat následujícím voláním v konstruktoru:

/** @Entity */
class Category
{
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category")
     */
    private $articles;
    /**
     * V konstruktoru inicializujeme všechny kolekce.
     */
    public function __construct()
    {
        $this->articles = new DoctrineCommonCollectionsArrayCollection;
    }
}

Doctrine 2 provádí konstruktor pouze při vytváření úplně nové prázdné instance, nikdy pak už například při načítání uložené entity z databáze, proto je toto volání zcela v pořádku.

Zvykněte si hned automaticky psát takovou inicializaci do konstruktoru vždy, kdy v entitě definujete jakoukoliv asociační kolekci.

Metody pro práci s kolekcemi

Třída DoctrineCommonCollectionsArrayCollection nabízí několik užitečných funkcí pro práci s danou kolekcí. Kromě běžných metod vyžadovaných od implementovaných rozhraní Countable, IteratorAggregate a ArrayAccess jsou to zejména funkce pro přidávání, získávání a mazání jednotlivých prvků z kolekce.

Pokud jsem výše psal, že pro práci s kolekcí bych měl mít v dané entitě definovaný nějaký setter ve smyslu $category->addArticle($article), není to tak úplně pravda, protože teoreticky mi bohatě stačí mít pouze getter  $category->getArticles():

$category = $em->find('Category', 123);
$article = $em->find('Article', 123);
// měníme kolekci skrz getter
$category->getArticles()->add($article);
$category->getArticles()->removeElement($article);

Tohle ale nikdy nedělejte, protože tím trochu narušujete pomyslné zapouzdření celé entity kategorie a můžete si tím potenciálně udělat v aplikaci čurbes. Navíc tím můžete obcházet některé další operace, které byste rádi ve svých setterech měli, jako je například výše ukázané promítnutí změny v asociaci i do vlastnící strany.

Takže si namísto toho pro těch pár nejčastějších operací radši definujte metody přímo v rámci entity $category a používejte důsledně jen a pouze je:

/** @Entity */
class Category
{
    // ...
    public function addArticle(Article $article)
    {
        $this->articles->add($article);
        $article->setCategory($this);
    }
    public function removeArticle(Article $article)
    {
        $this->articles->removeElement($article);
        $article->setCategory(NULL);
    }
}

Pozdní inicializace

Na konci minulého dílu jsme si ukázali, že Doctrine 2 používá pro načítání odkazovaných entit pozdní inicializaci. Nejinak je tomu i u celých odkazovaných kolekcí.

Pokud si tedy v našem příkladu načteme nějakou kategorii přes $category = $em->find('Category', 123), neprovádí Doctrine 2 hned automaticky další SELECT na všechny články patřící do dané kategorie.

Namísto toho si do proměnné $category->articles uloží instanci takové trochu zvláštní kolekce DoctrineORMPersistentCollection. Ta nabízí všechny metody, jako jakákoliv jiná kolekce v Doctrine 2, ale vyčkává až do poslední chvíle, co to jde, a pak teprve sama na svém pozadí pošle do databáze potřebný SELECT:

// tady se provede jen SELECT na danou kategorii
$category = $em->find('Category', 123);
// dokonce ani tady se ještě nic dalšího nenačítá
$articles = $category->getArticles();
// a teprve tady se načte seznam článků v kategorii
foreach ($articles as $article) {
    echo $article->title;
}

Řazení kolekcí

Odkazované entity se do asociačních kolekcí načítají obecně v náhodném řazení. Pokud chcete explicitně určit, podle čeho se mají entity seřadit, můžete pro to využít anotaci  @orderBy.

Pokud tedy například budu chtít řadit články v kategoriích podle času publikování sestupně a pak podle titulku vzestupně, definuji danou asociaci takto:

/** @Entity */
class Category
{
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category")
     * @orderBy({'published' = 'DESC', 'title' = 'ASC'})
     */
    private $articles;
}

Pokud nějaké vlastní řazení definujete, je zpravidla dobrý nápad doprovodit to hned vytvořením odpovídajícího indexu v databázové struktuře.

Kaskádové persistování

Jak už bylo zmíněno v některém z dřívějších dílů, pokud chcete jakoukoliv nově vytvořenou entitu uložit do databáze, musíte na ni vždy zavolat  $em->persist($entity):

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

Nutnost persistovat každou jednu instanci zvlášť může být občas nepohodlná, hlavně při ukládání složitějších asociací a komplikovanějších struktur. Proto Doctrine 2 nabízí možnost kaskádového persistování.

Kaskádové persistování se nastavuje na inverzní straně asociace s pomocí atributu cascade. Zdůrazňuji, že toto nastavení se dělá vždy na inverzní straně asociace, i když to na první pohled nemusí vypadat moc logicky. Mimo jiné to znamená, že pokud chcete použít kaskádování, musíte vždy definovat obousměrnou asociaci.

/** @Entity */
class Category
{
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category",
     *   cascade={"persist"})
     */
    private $articles;
}

Pak stačí persistovat jen hlavní entitu a persistování pak „probublá“ i do všech dalších navazujících entit:

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

Kromě persistování je možné kaskádovat i další základní operace s entitami, tedy mazání, odpojování a slučování:

/** @Entity */
class Category
{
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category",
     *   cascade={"persist", "remove", "detach", "merge"})
     */
    private $articles;
}

Pokud chcete kaskádovat všechny čtyři operace, můžete místo jejich vyjmenovávání použít hodnotu  "all":

/** @Entity */
class Category
{
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category",
     *   cascade={"all"})
     */
    private $articles;
}

Kaskádování velice zpříjemňuje práci s rozsáhlejšími asociacemi, na druhou stranu může mít zásadní dopad na celkovou výkonnost Doctrine 2. Pokaždé tedy zvažte, zda v daném případě kaskádování opravdu potřebujete a využijete. Určitě ale nezapínejte kaskádování mechanicky u všech asociací.

Komentáře

Odebírat
Upozornit na
guest
5 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
Zobrazit všechny komentáře
Adam

Zdravím,

díky za zajímavý seriál! Měl bych na autora nebo čtenáře dotaz k asociacím… U několika projektů používáme many-to-many asociace, kde bychom potřebovali k té asociaci přidat ještě nějakou poznámku – pokud použiju entity z příkladu, děláme many-to-many asociaci mezi Article a Category a do této asociace bychom potřebovali přidat např. výchozí kategorii článku, popř. pořadí jednotlivých kategorií. Jde tohle nějak pomocí Doctrine 2 zařídit nebo je potřeba udělat i entitu Article_Category a do ní tyhle doplňující informace dát?

Díky, Adam

Adam

Díky, toho jsem se obával. :)

Lenka

Dobrý den, když mám vazbu many-to.many mezi filmem a osobou, mam tam udelanou spojovaci entitu film_osoba a film_reziser a chtela bych se zeptat zda by mi stacilo tam jen zadavani id nebo mam udelat i promenou treba film nebo osoba v propojovaci entite? dekuji za radu zacinam s doctrine

Přístupnost není jen o splnění norem: nový pohled na inkluzivní design

Přístupnost a inkluze možná nepatří mezi nejžhavější témata digitálního světa – dokud o nich nezačne mluvit Vitaly Friedman. Na WebExpo 2024 předvedl, že inkluzivní design není jen o splněných checkboxech, ale hlavně o lidech. S energií sobě vlastní obrátil zažité přístupy naruby a ukázal, že skutečně přístupný web je nejen možný, ale i nezbytný.

Efektivnější vývoj UI nebo API: Co si odnést z WebExpo 2025?

Různé
Komentáře: 0
Jak snadno implementovat moderní uživatelské rozhraní? Které funkce brzdí rychlost vašeho webu? A kdy raději sami přibrzdit, abychom využitím AI nepřekročili etické principy? Debatu aktuálních dev témat rozdmýchá sedmnáctý ročník technologické konference WebExpo, která proběhne v Praze od 28. do 30. května. Který talk či workshop si rozhodně nenechat ujít? Toto je náš redakční výběr z vývojářských hroznů.

Zapřáhněte AI jako nikdy předtím. Květnová konference WebExpo přivítá hvězdy technologického světa

Od 28. do 30. května 2025 promění pražský Palác Lucerna na tři dny technologická konference WebExpo. Na programu je více než 80 přednášek a workshopů od expertů z celého světa. WebExpo tradičně propojuje vývojáře, designéry, marketéry i byznysové lídry a nabízí praktické dovednosti, strategické myšlení a přináší nejnovější trendy nejen v oblasti AI.