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

Zdroják » Databáze » Doctrine 2: asociace

Doctrine 2: asociace

Články Databáze, PHP

Asociace jsou v terminologii ORM analogií ke vztahům mezi tabulkami u relačních databází. Je to jednoduše způsob, jak namapovat vazby mezi entitami na cizí klíče v databázových tabulkách. V článku si ukážeme, jak s těmito asociacemi pracovat v ORM Doctrine 2.

Nálepky:

Zatímco v relačních databázích se pro vyjádření vztahů uměle šroubují cizí klíče nebo vazební tabulky, které často nemají v reálném světě svůj přirozený protějšek, na úrovni objektů je práce s asociacemi mezi entitami naprosto přirozená a logická:

$category = $em->find('Category', 123);
$article = new Article;
// první možnost - přiřadíme kategorii do článku
$article->setCategory($category);
// druhá možnost - přiřadíme článek do kategorie
$category->addArticle($article);
// získáme seznam všech článků z kategorie
$articles = $category->getArticles();

Stejně jako je tomu u vztahů mezi relačními tabulkami, i u asociací mezi entitami se rozlišují typy 1:1, 1:N a M:N, ačkoliv v následné práci s entitami se rozdíly mezi nimi zásadně stírají. Nejprve je ale potřeba si vyjasnit pojem vlastnící a inverzní strany.

Vlastnící a inverzí strana

Pokud je mezi dvěma entitami vztah, ať už kteréhokoliv typu, vždy je jedna z nich vlastnící (owning side) a druhá inverzní (inverse side). Pro další práci s asociacemi je velice důležité vlastnící a inverzní stranu vztahu navzájem rozlišovat, a to z několika důvodů:

  • Na každé straně se asociace definuje jinými anotacemi. Je třeba vědět, na kterou stranu jaké anotace patří.
  • Navíc na vlastnící straně je definice asociace vždy povinná, zatímco na inverzní straně volitelná. Podle toho se pak rozlišují jednosměrné (unidirectional) a obousměrné (bidirectional) asociace.
  • Rozdíl se pak projeví zejména v samotném používání entit. Do databáze se po zavolání $em->flush() promítnou jen změny asociací provedené na vlastnící straně, zatímco změny provedené na inverzní straně se ignorují!

Zpočátku budete mít možná trochu problém určit, která strana je která. Pomůže vám jednoduchá pomůcka, opět z oblasti relačních databází. U vazby 1:1 respektive 1:N je vlastnící vždy ta strana, u které je v databázi vazební sloupec s cizím klíčem. Předpokládejme pro příklad následující strukturu:

CREATE TABLE category (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE article (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    category_id INT NOT NULL REFERENCES category (id)
);

Entita Article je zde vždy vlastnící stranou, protože u její tabulky je cizí klíč category_id, zatímco Category je stranou inverzní.

U vazeb M:N s vazební tabulkou je to pak čistě na vaší volbě, jenom si ale musíte vždy jednu ze stran určit jako vlastnící a druhou jako inverzní. Pro toto rozhodování nejste prakticky ničím omezeni. Je ale vhodné zvážit, na které straně budete s asociací pracovat častěji a tuto stranu prohlásit za vlastnící.

Jednosměrné asociace

Jako příklad si teď vezměme 1:N asociaci – přiřazení článků do kategorií, pro které máme už výše strukturu databáze. Nejprve se podívejme na samotné vymezení daných sloupců pomocí příslušných anotací.

/** @Entity */
class Article
{
    /**
     * @id @column(type="integer")
     * @generatedValue
     */
    private $id;
    /**
     * @manyToOne(targetEntity="Category")
     * @joinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;
}
/** @Entity */
class Category
{
    /**
     * @id @column(type="integer")
     * @generatedValue
     */
    private $id;
}

Příklad ukazuje definici jednosměrné 1:N vazby, kde se článek odkazuje na příslušnou kategorii. Definice vazby pomocí anotace @manyToOne je na straně článku, protože článek je vlastnící stranou.

Názvy vazebního sloupce a sloupce s primárním klíčem kategorie uvedené v anotaci @joinColumn jsou implicitní a pokud vám takto vyhovují, můžete celou anotaci @joinColumn vynechat:

class Article
{
    // ...
    /**
     * @manyToOne(targetEntity="Category")
     */
    private $category;
}

Vše uvedené platí i pro 1:1 vazbu, pouze se místo @manyToOne použije anotace  @oneToOne.

Pro vazbu M:N je věc poněkud složitější, protože mezi dvěma základními entitami v takovém případě existuje ještě vazební tabulka:

CREATE TABLE article (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE tag (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE article_tag (
    article_id INT NOT NULL REFERENCES article (id),
    tag_id INT NOT NULL REFERENCES tag (id),
    PRIMARY KEY (article_id, tag_id)
);

Pokud dodržíte takovouto pojmenovávací konvenci pro názvy tabulek a atributů, kterou vyznává i Doctrine 2, je definice M:N asociace jednoduchá:

class Article
{
    // ...
    /**
     * @manyToMany(targetEntity="Tag")
     */
    private $tags;
}

Při vlastním pojmenování spojovací tabulky či jednotlivých sloupců to musíte systému sdělit, a to s pomocí anotace  joinTable:

class Article
{
    //...
    /**
     * @manyToMany(targetEntity="Tag")
     * @joinTable(
     *     name="article_tag",
     *     joinColumns={
     *         @joinColumn(name="article_id", referencedColumnName="id")
     *     },
     *     inverseJoinColumns={
     *         @joinColumn(name="tag_id", referencedColumnName="id")
     *     }
     * )
     */
    private $tags;
}

Obousměrné asociace

Ve výše uvedených příkladech můžeme na vlastnící straně, tedy u konkrétního článku zjistit, do jaké kategorie a jakých štítků je článek zařazený. Prostě se jednoduše podíváme do jeho členských proměnných $category$tags.

Velice často se nám to ale hodí i na inverzní straně – pro konkrétní kategorii či štítek si chceme z nějaké její proměnné $articles vypsat seznam všech článků, které do nich patří. V takovém případě musíme definovat asociaci jako obousměrnou.

V praxi to znamená, že musíme do kategorie respektive tagu definovat správně anotovaný inverzní sloupec $articles. Navíc musíme pomocí parametru inversedBy přidat informaci o inverzním sloupci i do entity článku. Celá definice oboustranné 1:N (a analogicky tomu i oboustranné 1:1) asociace mezi článkem a kategorií by pak vypadala takto:

/** @Entity */
class Article
{
    // ...
    /**
     * @manyToOne(targetEntity="Category", inversedBy="articles")
     * @joinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;
}
/** @Entity */
class Category
{
    // ...
    /**
     * @oneToMany(targetEntity="Article", mappedBy="category")
     */
    private $articles;
}

Obdobně pro oboustrannou M:N vazbu mezi články a tagy by úplný zápis vypadal nějak takto:

class Article
{
    //...
    /**
     * @manyToMany(targetEntity="Tag", inversedBy="articles")
     * @joinTable(
     *     name="article_tag",
     *     joinColumns={
     *         @joinColumn(name="article_id", referencedColumnName="id")
     *     },
     *     inverseJoinColumns={
     *         @joinColumn(name="tag_id", referencedColumnName="id")
     *     }
     * )
     */
    private $tags;
}
/** @Entity */
class Tag
{
    // ...
    /**
     * @manyToMany(targetEntity="Article", mappedBy="tags")
     */
    private $articles;
}

Obousměrné asociace jsou zpravidla výkonově náročnější, než asociace jednosměrné. Kde to tedy jde, omezte se na jednosměrnou definici na vlastnící straně. Oboustranné asociace pak vytvářejte jen v opravdu odůvodněných případech.

Všechny typy a kombinace asociací

Výše jsou uvedeny jen základní nejčastější případy asociací. Různých variant a nuancí při definici vztahů mezi entitami je celá řada. Stojí za to si v dokumentaci Doctrine 2 pozorně pročíst popis všech typů asociací.

Zároveň určitě doporučuji si tento odkaz uložit do záložek nebo někam k ruce. Bude vám sloužit jako rychlý referenční přehled, do kterého budete ještě dlouho potřebovat nahlížet pokaždé, kdy budete nějakou asociaci definovat.

Pozdní inicializace

Chceme-li vložit článek do některé kategorie, jednoduše v instanci článku jakožto vlastnící strany do proměnné $category přiřadíme instanci dané kategorie (samozřejmě skrze setter, který teď pro přehlednost v našich ukázkách chybí):

$article->setCategory($category);
$em->flush();

Zavoláním $em->flush() se pak daná vazba uloží do databáze. Při jakémkoliv dalším načtení článku přes $em->find() se společně s článkem načte i instance jeho kategorie. Nemusíme ji tedy už nijak ručně donačítat, máme ji hned automaticky v proměnné  $category:

$article = $em->find('Article', 123);
$category = $article->getCategory();

Přesněji řečeno Doctrine 2 zde důsledně využívá pozdní inicializaci. Takže kategorii si automaticky vyžádá z databáze teprve v okamžiku, kdy ji skutečně potřebujete. Do té doby je v proměnné uložena pouze takzvaná proxy entita, která zajišťuje právě ono pozdní načtení. Vše je ale zcela transparentní, takže se o to vůbec nemusíte starat.

Úplně stejně se chovají i asociační kolekce, v našem případě například proměnné $articles v kategorii:

$category = $em->find('Category', 123);
$articles = $category->getArticles();

I zde se příslušné články do kolekce $articles automaticky načítají z databáze teprve ve chvíli, kdy je opravdu nutně potřebujete.

Doctrine 2 je tedy z tohoto pohledu poměrně efektivní, snaží se do databáze sahat jen v nezbytných případech.

Příští pokračování

Příště budeme v tématu asociací pokračovat. 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. Ukážeme si správné postupy při definicích getterů, setterů a dalších obslužných funkcí pro manipulaci s asociacemi.

Komentáře

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

Načítá Doctrine entity při přístupu jednotlivě, resp. jedinou entitu z vazby při přístupu? To by pak mohlo skončit bombardováním databáze dotazy, kdy při klasickém přístupu bychom měli jeden nebo hrstku dotazů…zkoušel jsem hodit ‚, fetch=“EAGER“‚, ale nepřišlo mi, že by to mělo vliv (zkoumal jsem přes logger, je možné že špatně).
I když Doctrine samo asi nemůže umět určit co vše nahrát, tak… leda nějak manuálně (možná při eventu)

Jinak potěší: http://www.doctrine-project.org/blog/dc2-experimental-associations-id-fields

v6ak

NotORM umí načítání ze závislých tabulek řešit elegantně. U Doctrine by to mohl být trošku problém, protože používá jednu instanci entity na instanci aplikace, ale nějaký 20/80 přístup by zde mohl fungovat.

Nox

Děkuji za obsáhlou odpoveď … DQL na vhodném místě zní dobře

Jenom doplním příklad k mému dotazu, např. při:
foreach($item->attributes as $attribute){ ... }
je jasné, že budeme potřebovat vše a je zbytečné posílat 10 dotazů navíc. Zdá se mi, že to není až tak výjimečná situace…

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.