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

Zdroják » Databáze » Doctrine 2: základní definice entit

Doctrine 2: základní definice entit

Články Databáze

V úvodním článku seriálu jsme si systém Doctrine 2 obecně představili, ukázali si, kde jej stáhnout a jak jej nainstalovat. Dneska se pustíme do skutečné práce, řekneme si, co jsou vlastně entity a jak se s nimi v Doctrine 2 pracuje. Téma nám vydrží až do příště.

Nálepky:

Zapomeňte na databázové tabulky

Entity jsou základní kameny v Doctrine 2 a vůbec celé vaší aplikace. Každá entita reprezentuje nějaký objekt reálného světa, takzvaný doménový objekt. Jednu entitu tak budu mít definovanou pro článek, jinou pro kategorii, další pro uživatele.

Pokud jste dosud nepoužívali vůbec žádné ORM, případně jste zvyklí na některé ORM založené na návrhovém vzoru Active Record, byla pro vás takovou základní jednotkou práce databázová tabulka. Měla nějaké atributy, nějaká pravidla, co lze a co nelze, přes cizí klíče mohla odkazovat na další tabulky.

Jestliže je to i váš případ, je teď potřeba udělat malý myšlenkový posun. Při používání Doctrine 2 zkuste na databázové tabulky zapomenout a brát je alespoň zpočátku jako něco, co je prostě někde na pozadí a o co se zatím nemusíte starat. Oprostěte se od všech pomocných mechanizmů, které jsou účelově specifické pro relační databáze, ale v reálném světě vůbec neexistují.

Typickým příkladem jsou cizí klíče. Ve skutečném světě nic takového jako cizí klíč není. Článek je prostě zařazen do nějaké kategorie. To, že se to v relačních databázích řeší nějakým cizím klíčem category_id v tabulce article, je jenom vedlejší efekt toho, jak relační databáze fungují.

Obdobně se někdy jeden doménový objekt, který je z logiky věci jedním celkem, v relačních databázích roztrhá do více různých tabulek. Například zboží v e-shopu může mít různé vlastnosti, které bývají z ryze pragmatických důvodů dekomponovány do samostatné key/value tabulky. Nebo můžete mít konkrétní typ produktu poskládaný z jedné tabulky s obecnými atributy společnými pro všechny produkty a z druhé se specifickými atributy jen pro ten konkrétní typ.

V Doctrine 2 pracujete o vrstvu výš, pro modelování doménových objektů se používají entity. Ty nás dokonale odstiňují od konkrétní databáze i se všemi jejími podivnostmi, jako jsou třeba cizí klíče nebo nutnost tříštit doménový objekt do více míst. Práce s ORM se tak stává systémově a logicky čistou.

Rychlá ukázka používání entit

Entity jsou běžné PHP objekty, jak je normálně znáte a používáte. Jenom v nich do komentářů musíme přidat zavináčové anotace, podle kterých Doctrine 2 říkáme, jak s nimi má pracovat.

Pro příklad si vezměme jednoduchý článek s titulkem a textem, který v databázi vytvoříme takto:

CREATE TABLE article (
    id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    text TEXT NULL
);

Příslušnou entitu v Doctrine 2 pak definujeme takto:

/** @entity */
class Article
{
    /**
     * @id @column(type="integer")
     * @generatedValue
     */
    private $id;
    /** @column(length=100) */
    private $title;
    /** @column(type="text", nullable=true) */
    private $text;
    public function getId()
    {
        return $this->id;
    }
    public function getTitle()
    {
        return $this->title;
    }
    public function setTitle($title)
    {
        $this->title = $title;
    }
    public function getText()
    {
        return $this->text;
    }
    public function setText($text)
    {
        $this->text = $text;
    }
}

Pomocí anotací jsme Doctrine 2 řekli, že má do databáze ukládat hodnoty z vlastností $id, $title a $text. Navíc že $id je primární klíč s automaticky generovanou hodnotou.

S entitou se pak pracuje stejně jako s jakýmkoliv jiným objektem:

$article = new Article;
$article->setTitle('Lorem ipsum');
$article->setText('Lorem ipsum dolor sit amet.')
echo $article->getTitle();
echo $article->getText();

Pro uložení do databáze či načtení z databáze musíte využít Entity Manager, který jsme si připravili v předchozím dílu. Entity Manageru se budeme věnovat v samostatném článku, takže teď jenom pro základní představu:

// vytvoříme nový článek
$article = new Article;
$article->setTitle('Lorem ipsum');
$article->setText('Lorem ipsum dolor sit amet.')
// článek uložíme do databáze
$entityManager->persist($article);
$entityManager->flush();
// načteme z databáze článek s ID = 123
$anotherArticle = $entityManager->find('Article', 123);
echo $anotherArticle->getTitle();
echo $anotherArticle->getText();
// něco v něm změníme a zase uložíme
$anotherArticle->setTitle('Foo bar');
$entityManager->flush();

Seznam všech anotací, které lze při definici entity použít, naleznete v dokumentaci projektu. My si je budeme představovat postupně, když na ně přijde čas.

Vedle komentářových anotací nabízí Doctrine 2 možnost definice entity pomocí XML nebo YML formátu či prostřednictvím speciálních PHP volání. Stejně tak není potřeba vytvářet strukturu databáze ručně a můžeme nechat Doctrine 2, aby nám ji sama vygenerovala z entity. K tomu se ale dostaneme podrobněji v některém z pozdějších dílů seriálu.

Mapování entity na tabulku

Doctrine 2 se snaží z názvu třídy sama odvodit název příslušné databázové tabulky, do které data ukládá. Entitu z našeho příkladu tedy mapuje na tabulku  article.

Pokud chcete entitu explicitně namapovat na jinou tabulku, lze to udělat speciální anotací @table v záhlaví třídy:

/**
 * @entity
 * @table(name="posts")
 */
class Article
// ...

Pokud jste v názvu tabulky použili rezervované slovo z SQL, musíte je obalit zpětnými uvozovkami:

/**
 * @entity
 * @table(name="`order`")
 */
class Order
// ...

Tohle je aktuálně jedno ze slabých míst Doctrine 2. Ideální by totiž bylo, kdyby veškeré escapování zajišťovala sama, jako to dělá například dibi. Dříve či později navíc narazíte na případy, kdy vám ani obalování zpětnými uvozovkami nebude v Doctrine 2 fungovat. Vřele tedy doporučuji se používání rezervovaných slov v názvech tabulek a atributů oklikou vyhnout.

U databázových systémů podporujících schémata je samozřejmě možné určit i schéma:

/**
 * @entity
 * @table(schema="public", name="posts")
 */
class Article
// ...

Doctrine 2 bohužel nemá nativní podporu pro automatické prefixování všech tabulek aplikace. Lze to zajistit s využitím mechanizmu událostí, což je ale vyšší dívčí a k podrobnějšímu osvětlení se dostaneme v pokročilejší fázi seriálu.

Anotace jednotlivých sloupců

Každý sloupec, který má Doctrine 2 ukládat do databáze, anotujeme pomocí @column. V závorce za anotací lze upřesnit podrobnější vlastnosti sloupce:

type
Datový typ daného sloupce. Datové typy podrobněji rozebírá následující podkapitola. Výchozí hodnota je  string.
name
Název atributu v definici databáze. Standardně se použije stejný název, jako má samotná proměnná, zde ho lze ale změnit a proměnnou tak mapovat na jiný databázový atribut.
unique
Udává, zda má být nad sloupcem kontrolována unikátnost. Může nabývat hodnot true nebo false, což je i výchozí hodnota.
nullable
Udává, zda může být do sloupce uložena i NULL  hodnota. Může nabývat hodnot true nebo false. Výchozí hodnota je false, takže u každého NULL sloupce to musíte explicitně povolit, jinak se vám v databázi daný sloupec definuje jako  NOT NULL.
length
Dává smysl jen u řetězcových sloupců a udává jejich maximální délku. Výchozí hodnota je 255.
precision a scale
Dávají smysl jen u desetinných čísel a udávají jejich přesnost. Výchozí hodnota je 0.

Příklad použití:

/** @column(name="username", type="string", length=100, unique=true) */
private $name;
/** @column(type="text", nullable=true) */
private $description;
/** @column(type="decimal", precision=2, scale=1) */
protected $height;

Na tomto místě je nutné upozornit, že takto definované vlastnosti a omezení využívá Doctrine 2 pouze pro prvotní vytvoření databáze. Při následné průběžné práci s entitami k nim ale již sama nijak nepřihlíží, neprovádí podle nich téměř žádné konverze ani kontroly. Vše nechává až na databázové vrstvě. Případně si to musíte dopsat sami ručně do getterů, setterů či speciálních validačních metod.

Datové typy sloupců

Uvnitř anotace @column(type="xxx") můžete použít kterýkoliv z předdefinovaných datových typů.

Musíte si ale uvědomit, že se nejedná o databázové ani PHP typy. Jsou to speciální mapovací typy Doctrine 2, kterými určujete, co a jak se má z PHP mapovat do databáze. Například definicí @column(type="text") vlastně říkáte, že Doctrine 2 má mapovat danou proměnnou, ve které je uložena hodnota v PHP typu string, na databázový sloupec typu  CBLOB.

Jaké všechny typy tedy máme k dispozici a jaké nám zajišťují mapování mezi databází a PHP?

Doctrine 2 typ PHP typ Databázový typ
string string VARCHAR
text string CLOB
integer integer INT
smallint integer SMALLINT
bigint string BIGINT
decimal double DECIMAL
boolean boolean BOOLEAN
date DateTime DATETIME
time DateTime TIME
datetime DateTime DATETIME či TIMESTAMP bez časové zóny
datetimetz DateTime DATETIME či TIMESTAMP s časovou zónou
object object CLOB
array array CLOB

V příštím pokračování seriálu budeme v entitách ještě pokračovat a podíváme se na některé pokročilejší možnosti jejich definice a práce s nimi.

Komentáře

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

Keď už pracujeme s objektami, neni lepšie použiť nejakú objektovú databázu? Ja sám používam ORM (Entity Framework) ale ajtak nerozumiem v čom je použitie RDBMS + ORM frameworku lepšie ako priamo použitie ODBMS?

drevolution

Na tuto otázku není vůbec jednoduchá odpověď. Sám za sebe tvrdím, že pokud máme objektový model, tak určitě není automatické, že ho budeme ukládat do objektové databáze. Já osobně sahám po relační databázi z těchto důvodů:

  1. Větší zkušenost a více znalostí relačních databází
  2. Jednoduchost na správu a dostupnost (i když ta jednoduchost pramení právě z větších znalostí)
  3. Možnost dekompozice objektů do relací. Často je to nevýhoda, ale občas to může být výhoda

Určitě jsem na něco ještě zapomněl, ale jen tyto důvody mě drží u relačních databází. Pokud máme dobré ORM, což Doctrine 2 s přivřeným okem je, je rozhodování ještě jednodušší.

Patrik Votoček

v ukázce ukládání článků se uloží jenom první… Protože ten druhý není přidán do UnitOfWorku schází tam $entityManager->persist($anotherArticle); před flush.

Ondrej Mirtes

Je to takhle spravne, na entite se vola persist jen jednou, pak uz se ulozi jen pomoci flush().

juzna

Jak Doctrine pozna, ktere zaznamy se zmenily a tzn. ktere musi ulozit do DB?
Jde mi o to, ze treba nactu tisic zaznamu pomoci find(), na par z nich udelam nejakou malou zmenu a pak zavolam flush(). Bude Doctrine fungovat efektivne?
A jeste by me zajimalo, je mozne udelat nejaky hook, aby mi Doctrine logovalo veskere provedene zmeny (kdo, kdy, puvodni data, nova data)?
Diky za rady

drevolution

Jak Doctrine pozna, ktere zaznamy se zmenily a tzn. ktere musi ulozit do DB? Jde mi o to, ze treba nactu tisic zaznamu pomoci find(), na par z nich udelam nejakou malou zmenu a pak zavolam flush(). Bude Doctrine fungovat efektivne?

V rámci možností to efektivně funguje. Začíná to již při načítání entit. Pro každou načítanou entitu se pomocí funkce spl_object_hash() spočítá hash, který se uloží do interního pole v Unit of Work. Jakmile zavoláme flush na Entity Manageru, zavolá Entity Manager Unit of Work, aby zařídil uložení dat. Unit of Work projde každou načtenou entitu, spočítá pro ní další hash a porovná ho s hodnotou uloženou při načtení entity. Pokud se hodnoty nerovnají, ví, že se entita změnila. Aby byla práce ještě efektivnější, počítá si ještě pro každou entitu set změn, které pak pošle do databáze (což opět nejakým způsobem opimalizuje).

A jeste by me zajimalo, je mozne udelat nejaky hook, aby mi Doctrine logovalo veskere provedene zmeny (kdo, kdy, puvodni data, nova data)?

Toto udělat lze. V Doctrine2 se to realizuje pomocí listenerů. Honza jim určitě v některém díle bude věnovat pozornost, takže sem dám pouze odkaz do dokumentace: http://www.doctrine-project.org/projects/orm/2.0/docs/reference/events/en#implementing-event-listeners

Některé další implementace listenerů pak ukázal Benjamin Eberlei na Doctrine blogu:

paranoiq

spl_object_hash() není hash! tato funkce nic nepočítá. pouze vrací jednoznačný identifikátor objektu. při změně vlastností objektu se hodnota vrácená spl_object_hash() nemění.
tato hodnota je použita pouze k indexaci polí entit a polí původních hodnot, aby je bylo možné navzájem dohledat a porovnat i pokud dojde ke změně všech údajů entity
_______
porovnávat entity s původní hodnotou musí složitěji (u private a protected nejspíš pomocí odemykání metodou setAccessible() z Reflection nebo konverzí objektu na pole)

drevolution

Aha, už to vidím, díky za opravu.

Patrik Votoček

Ha super já to pro jistotu vždy explicitně persistoval. A navíc jsem díky tomu konečně pochopil smysl  $em->detach($entity);

drevolution

Dobrý článek Honzo. Hlavně úvod se mi líbí, protože lehce přesahuje tématiku ORM a ukazuje přístup, jak stavět doménové objekty, které jsou izolované od persistentní vrstvy aplikace.

jos

Typickým příkladem jsou cizí klíče. Ve skutečném světě nic takového jako cizí klíč není. Článek je prostě zařazen do nějaké kategorie. To, že se to v relačních databázích řeší nějakým cizím klíčem category_id v tabulce article, je jenom vedlejší efekt toho, jak relační databáze fungují.

Asi by se slušelo říct, že je to kvůli referenční integritě a že to programátor dělá hlavně proto, aby mu databáze nelhala (protože tam strčil / nechal strčit nesmysly). To s tim vedlejším efektem sem buď nepochopil, nebo je to úplnej nesmysl.
Nevim o SQL databázi, která by mi zakázala joinovat přes atributy jen proto, že mezi nima není cizí klíč

Jinak k tomu myšlenkovýmu posunu – programátor by se měl v případě cizích klíčů posunout spíš k uvažování o „doméně“ atributu relace. Mějme relaci A s atributem FOO (jehož hodnoty jsou z množiny přirozených čísel) a v ní dva řádky s hodnotama 1 a 2. Dále mějme relaci B s atributem BAR o němž prohlásíme, že je z domény (je datovýho typu) A.FOO a o přirozených číslech se v tomhle případě dále nebavíme

Bohužel současný produkty nenabízejí takhle přímočarý vyjádření … (nebo jo?)

A vůbec všechny SQL databáze dělají z programátora otroka a Doctrine mi nepřipadá jako elegantní způsob jak se z toho vymanit

Patrik Votoček

Tohle chápu a líbí se mi. Ale jak řešíš právě avizovaný případ přiřazení kategorie k článku. Mám selectbox a se seznamem kategorií a jako nejvhodnější mě připadá pro value použít ID kategorie. Jenže pak obcházím právě onu Doménovou logiku (nebo mě to tak alespoň připadá).

juzna

Doctrine cloveka nuti nejdriv ta data vytahnout z databaze, a pak tam zapsat upravu. Takze na takovou jednoduchou cinnost ti driv stacil jeden update, ale kluvi Doctrine se budou muset udelat jeste 2 selecty.
Toto se mi zda takove „divne“ a porad mi to nejde do hlavy. A navic se bojim poklesu vykonu aplikace kvuli takovymto zbytecnym operacim. Odhaduju, ze to ale zase nebude az takovy problem, ze?

drevolution

Výkonový pokles tu samozřejmě je. To není problém jen Doctrine2, ale jakéhokoliv ORM. ORM se na oplátku snaží poskytnou komfort v mapování.

Nicméně je zde možnost, jak nenatahovat všechny entity, ale napsat si UPDATE ručně. Nepoužívá se na to čisté SQL, nýbrž DQL (Doctrine Query Language), které stále pracuje s entitami a jejich atributy. Takováhle aktualizace skrz DQL může být pak vložena do vlastního repozitáře konkrétní entity (např. nějaký ArticleRepository), který bude mít kupříkladu metodu ArticleReposi­tory::acceptA­llArticlesByDa­te(DateTime $date).

Jak se píše UPDATE v DQL je v dokumentaci v sekci UPDATE queries

Pavel

Nešlo by vytvořit neúplný objekt Article, který by se teprve při přístupu k jeho atributům inicializoval? Tuším by to mohl být návrhový vzor Decorator.

Patrik Votoček

Já vím že celkem spamuju ale…
Na to neescapování jsem nadával cca před týdnem. Vůbec to nechápu ale je k tomu tady ISSUE kterou jsem taky nepochopil. Nicméně myslím si že pokud se „zbouří“ dostatek lidí mohlo by to „borce“ od Doctrine donutit to vyřešit lépe…

Patrik Votoček
Srigi

Po precitani clanku musim napisat, ze som z Doctrine 2 sklamany. V snippetoch vidim nenormalne velku zavislost medzi Entitou a (sry za anglictinu) underlaying storage engine. A z tejto debaty medzi autorom clanku a developerom Doctrine 2 som sklamany este viac.
Dufal som, ze Doctrine 2 sa priblizi k 5-lay Modelu, ktory uz pan Tichy popisal v dnes uz pomaly kultovom clanku u seba na blogu. Dufal som, ze Doctrine 2 mi umozni napisat domenove modely (Entity) a iba pomocou vymeny jednej triedy kdesi v 4–5 vrstve zmenit engine z MySQL na MongoDB. Je mi jasne, ze toto naprogramovat je skoro nemozne, ale ze bude Doctrine tak daleko od tohoto sna som necakal.

drevolution

Já z té debaty vůbec zklamaný nejsem. Mně pomohla uvědomit si, že to s quotováním není vůbec tak samozřejmá a jednoduchá záležitost, jak jsem si původně myslel.

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.