Doctrine 2 – Dědičnost entit
Tomáš PilařEntity v ORM (objektově relační mapování) zachycují objekty z reálného světa a transformují je do tříd v programovacím jazyku. V reálném světě má vždy skupina objektů stejné vlastnosti a liší v některých detailech. Všichni lidé mají jméno, datum narození, pohlaví, ale liší se v barvě očí, pleti, dovednostech apod.
Při čistém návrhu entit by se nám tedy hodila dědičnost. Umí ji Doctrine zpracovat, a jak s takovou entitou pracuje?
Entita a její rodič
Ideální bude, když si problematiku rozebereme na konkrétním příkladu. Představme si, že jsme programátor, který programuje blog. Na blogu budou vycházet článku, ale i krátké novinky. Pro jednoduchost ukázek budeme chtít u článku ukládat pouze název, úvodní text (perex), obsah a autora. U novinky budeme chtít ukládat její název, obsah, štítek a autora. Jak budou vypadat Doctrine entity?
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="article")
*/
class Article
{
/**
* @ORM\Id()
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private int $id;
/**
* @ORM\Column(type="string")
*/
private string $header;
/**
* @ORM\Column(type="text")
*/
private string $perex;
/**
* @ORM\Column(type="text")
*/
private string $content;
/**
* @ORM\Column(type="string")
* @var string
*/
private string $author;
// getters + setters
}
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="news")
*/
class News
{
/**
* @ORM\Id()
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private int $id;
/**
* @ORM\Column(type="string")
*/
private string $header;
/**
* @ORM\Column(type="text")
*/
private string $content;
/**
* @ORM\Column(type="string")
*/
private string $tag;
/**
* @ORM\Column(type="string")
*/
private string $author;
// getters + setters
}
Jak je vidět na první pohled entita News
obsahuje stejné vlastnosti jako Article
a liší se pouze ve štítku. V tento moment se nám může zdát jako zbytečné přemýšlet o nějaké dědičnosti. Možná…
Blogger, pro kterého blog programuje, se rozhodne, že by chtěl u článků a novinek přidat možnost ukládat datum publikace. Takže si jako programátor otevřeme entitu Article
a přidáme novou vlastnost $publishedAt
a implementujeme getter + setter. V tento moment bude následovat copy&paste nových věci i do entity News
a jakožto zkušení programátoři, kteří píší čistý kód, prostě ten Ctrl+C nezmáčkneme. Rozhodneme se to implementovat pořádně tak, jak se sluší od zkušeného programátora.
Počáteční refaktoring
Zprvu musíme vydefinovat společné vlastnosti, které přeneseme do společného předka a od něj pak budou entity Article
a News
dědit.
Když se na entity podíváme, tak je jasné, že se bude jednat o vlastnosti název, obsah a autor. Entita Article
má oproti entitě News
navíc perex a News
zase štítek. Nyní začíná veškerá sranda. :)
Vytvoříme tedy abstraktní třídu AbstractText
, do které přesuneme vydefinované proměnné.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use DateTime;
/**
* @ORM\Entity
* @ORM\Table(name="text")
*/
abstract class AbstractText
{
/**
* @ORM\Id()
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
private $id;
/**
* @ORM\Column(type="string")
* @var string
*/
private $header;
/**
* @ORM\Column(type="text")
* @var string
*/
private $content;
/**
* @ORM\Column(type="string")
* @var string
*/
private $author;
/**
* @ORM\Column(type="datetime")
* @var DateTime
*/
private $publishedAt;
// getters + setters
}
Následně entitu Article
podědíme od AbstractText
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="article")
*/
class Article extends AbstractText
{
/**
* @ORM\Column(type="text")
* @var string
*/
private $perex;
// getter + setter
}
a nakonec podědíme od AbstractText
i entitu News
.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="news")
*/
class News extends AbstractText
{
/**
* @ORM\Column(type="string")
* @var string
*/
private $tag;
// getter + setter
}
Propojení přes komentářové anotace
V tento moment, ještě Doctrine nedokáže pochopit, že chceme použít dědičnost u entit a je potřeba jí to oznámit přes speciální komentářovou anotaci. Otevřeme si tedy AbstractText
a přidáme anotaci InheritanceType
, DiscriminatorColumn
a DiscriminatorMap
.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="text")
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="discriminator", type="text")
* @ORM\DiscriminatorMap({
* "article" = "Article",
* "news" = "News"
* })
*/
abstract class AbstractText
{
// zůstává stejné
}
V tento moment již Doctrine zná vše potřebné a můžeme si nechat vygenerovat nové schéma pro databázi.
Databázové schéma
Pro implementaci dědičnosti v relační databázi využívá Doctrine propojení přes primární klíč. Když si dáme vypsat SQL aktuálního databázového schématu, tak bude vypadat následovně.
// generováno pro PostgreSQL 9.6
CREATE TABLE text (id SERIAL NOT NULL, header VARCHAR(255) NOT NULL, content TEXT NOT NULL, author VARCHAR(255) NOT NULL, published_At TIMESTAMP NOT NULL, discriminator TEXT NOT NULL, PRIMARY KEY(id));
CREATE TABLE article (id INT NOT NULL, perex TEXT NOT NULL, PRIMARY KEY(id));
CREATE TABLE news (id INT NOT NULL, tag VARCHAR(255) NOT NULL, PRIMARY KEY(id));
ALTER TABLE article ADD CONSTRAINT FK_23A0E66BF396750 FOREIGN KEY (id) REFERENCES text (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE;
ALTER TABLE news ADD CONSTRAINT FK_1DD39950BF396750 FOREIGN KEY (id) REFERENCES text (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE;
Jak je vidět, tak tabulky text
, article
a news
sdílí hodnotu primárního klíče. V tabulce text je navíc sloupec discriminator
, do kterého si Doctrine ukládá informaci o tom, která tabulka se je potomkem rodiče. Doctrine zde dbá na maximální výkon a tak při získávání dat z databáze rovnou propojí potřebné tabulky a načte všechna data v jediném SQL dotazu. Takto bude vypadat dotaz pro načtení všech článků.
// generováno pro PostgreSQL 9.6
SELECT t1.id AS id_2, t1.header AS header_3, t1.content AS content_4, t1.author AS author_5, t1.publishedAt as published_at_6, t0.perex AS perex_7, t1.discriminator
FROM article t0
INNER JOIN text t1 ON t0.id = t1.id;
Závěr
Doctrine nám umožňuje používat dědičnost i na úrovni databáze bez zbytečných hacků a magie. Díky této vlastnosti můžeme navrhovat čisté entity a nemusíme duplikovat stejné vlastnosti v entitách.
Chci se dozvědět více!
Zajímá tě dědičnost entit více do hloubky nebo se chceš přiučit jiným cool věcem? Přihlaš se na má školení Doctrine 2 právě teď! První školení s názvem "Doctrine 2 - Začínáme používat ORM" proběhne již příští středu a máme ještě pár míst! :)
Zde jsou materiály, které ti pomohou pochopit, jak to v Doctrine funguje hlouběji.
- http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html
- http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annref-discriminatormap