Devel.cz Lupa Měšec Podnikatel Root Zdroják.cz DigiZone Slunečnice Vitalia TopDrive KupDnes Navrcholu NovýTarif Dobrý web Weblogy Woko Jagg Computer.cz SK: MojeLinky

Hlavní navigace

Doctrine 2: asociace

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.

Tweetni to Twitter Jaggni to! Jagg Del.icio.us Delicious

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.

Jan Tichý

Jan Tichý

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

Školení SEO (optimalizace pro vyhledávače)

DW - Školení SEO
  • Jak fungují vyhledávače a co od nich můžete očekávat.
  • Analýza klíčových slov - kde hledat, jak slova vybrat, jak optimalizovat.
  • Metody linkbuildingu - jak získat zpětné odkazy aniž byste za ně museli platit.
  • Vyhodnocování SEO - nesledujte jen pozice.

Další informace o školení SEO »

Přehled názorů

Pozdní inicializace
Nox 23. 9. 2010 18:02
Nový
└ 
Re: Pozdní inicializace
Jan Tichý 25. 9. 2010 11:33
Nový
 
├ 
Re: Pozdní inicializace
Vít Šesták (v6ak) 26. 9. 2010 17:21
Nový
 
│
└ 
Re: Pozdní inicializace
Jan Tichý 27. 9. 2010 14:11
Nový
 
└ 
Re: Pozdní inicializace
Nox 28. 9. 2010 15:55
Nový
       

Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.

Zasílat nově přidané příspěvky e-mailem