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

Zdroják » Databáze » Doctrine 2: pokročilá definice entit

Doctrine 2: pokročilá definice entit

Články Databáze, PHP

V minulém díle seriálu jsme nakousli téma entit v Doctrine 2. Dnes budeme s entitami pokračovat a podíváme se na některé pokročilejší možnosti jejich definice a práce s nimi.

Identifikátor entity

Každá entita musí mít určený svůj identifikátor. Což je obdoba primárního klíče u databázové tabulky. Bez identifikátoru nelze daný záznam jednoznačně adresovat. K určení identifikátoru slouží anotace  @id.

/** @entity */
class Article
{
    /** @id @column(type="integer") */
    private $id;
    // ...
}

V tomto případě je nutné každé nové entitě přiřadit hodnotu jejího id ručně. Obvykle se ale chceme spolehnout na automaticky generované identifikátory, v databázi nejčastěji řešené jako auto_increment či sekvence. K tomu slouží anotace  @generatedvalue.

/** @entity */
class Article
{
    /**
     * @id @column(type="integer")
     * @generatedvalue
     */
    private $id;
    // ...
}

Takto definovaná entita se pokusí automaticky zvolit nejlepší způsob generování podle právě používané databáze. Pro MySQL se tedy použije AUTO_INCREMENT sloupec, zatímco u PostgreSQL se nové hodnoty získávají ze sekvence. Pokud chcete mít věc více pod kontrolou, můžete upřesnit požadovanou strategii:

/** @entity */
class Article
{
    /**
     * @id @column(type="integer")
     * @generatedvalue(strategy="IDENTITY")
     */
    private $id;
    // ...
}

Přičemž můžete použít jednu z následujících hodnot:

AUTO
Výchozí hodnota, kdy se nejlepší strategie volí automaticky. Tedy totéž, jako když se parametr strategy vůbec neuvede.
SEQUENCE
Vynutí si použití sekvencí. Funguje tedy pouze v systémech, které sekvence podporují, tedy v PostgreSQL a Oracle.
IDENTITY
Vynutí si použití speciálního sloupce, který si umí sám přiřazovat nové hodnoty. Což je klasický AUTO_INCREMENT u MySQL a SQLite, IDENTITY u MSSQL a SERIAL u PostgreSQL. Z důvodu některých budů v současné vývojové verzi Doctrine 2 je lepší například i u PostgreSQL explicitně používat právě tento typ.
TABLE
Tato možnost zatím není implementována, ale bude umožňovat pro generování identifikátoru využívat jinou speciální tabulku. Cílem je, aby to fungovalo podobně jako sekvence, ale zároveň bylo přenositelné napříč všemi databázovými systémy.
NONE
Identifikátor není automaticky generován, ale musí se do entity ručně přiřazovat. Je to tedy stejné, jako když anotaci @generatedvalue vůbec neuvedete.

Pamatujte na to, že volbou konkrétní strategie si můžete zásadně omezit přenositelnost aplikace mezi různými databázovými systémy. Na druhou stranu pak můžete využívat speciální možnosti zvolené strategie, například podrobněji specifikovat vlastnosti dané sekvence:

/** @entity */
class Article
{
    /**
     * @id @column(type="integer")
     * @generatedvalue(strategy="SEQUENCE")
     * @sequencegenerator(
     *     name="article_id_seq",
     *     initialvalue=1,
     *     allocationsize=1
     * )
     */
    private $id;
    // ...
}

Složený identifikátor

Doctrine 2 teoreticky podporuje i kompozitní identifikátor složený z více sloupců. Ten se pak definuje tak, že anotaci @id uvedete u každého z dotčených sloupců:

/** @entity */
class Permission
{
    /** @id @column */
    private $role;
    /** @id @column */
    private $resource;
    /** @column(type="boolean") */
    private $permission;
}

V praxi je ale aktuální vývojová verze Doctrine 2 se složenými identifikátory na štíru, obzvlášť pokud se ještě i kombinují s asociacemi na jiné entity. Polovina věcí tak nefunguje a zbytek se chová podivně.

Než se tedy Doctrine 2 dostane do první stabilní verze, raději se prozatím složeným identifikátorům vyhněte a dočasně je obejděte umělým klíčem:

/**
 * @entity
 * @table(
 *     name="permission",
 *     uniqueconstraints={
 *         @uniqueconstraint(
 *             name="permission_unique",
 *             columns={"role", "resource"}
 *         )
 *     }
 * )
 */
class Permission
{
    /**
     * @id @column
     * @generatedvalue
     */
    private $id;
    /** @column */
    private $role;
    /** @column */
    private $resource;
    /** @column(type="boolean") */
    private $permission;
}

Unikátní sloupce a indexy

Kontrolu na unikátnost nad jedním sloupcem je nejlepší definovat přímo u konkrétního sloupce:

/** @entity */
class Article
{
    /** @column(length=50, unique=true) */
    private $slug;
    // ...
}

Unikátní klíč přes více sloupců je nutné definovat v záhlaví celé entity pomocí anotace @table a v ní zanořené anotace  @uniqueconstraint:

/**
 * @entity
 * @table(
 *     name="article",
 *     uniqueconstraints={
 *         @uniqueconstraint(name="url_unique", columns={"category", "slug"})
 *     }
 * )
 */
class Article
// ...

Obdobným způsobem se nad tabulkou definují i databázové indexy, jenom se pak používá vnořená anotace  @index:

/**
 * @entity
 * @table(
 *     name="article",
 *     indexes={
 *         @index(name="title_idx", columns={"title"})
 *     }
 * )
 */
class Article
// ...

Je potřeba upozornit na to, že takto definovaná omezení využívá Doctrine 2 pouze pro to, aby uměla správně vygenerovat základní strukturu databáze. Při další průběžné práci s entitami k nim již sama nijak nepřihlíží, nijak je nekontroluje a nechává vše až na databázové vrstvě, případně na vlastních ručně psaných kontrolách.

Vlastní datové typy

V předchozím dílu seriálu jsme si představili základní datové typy, které nám Doctrine 2 standardně nabízí. Pokud vám nestačí, můžete si sami vytvořit vlastní typ. To spočívá v implementaci jednoduchého rozhraní, kde definujete metody pro převod hodnoty z PHP do databáze a naopak.

Bohužel lze zatím mapovat jednu členskou proměnnou právě na jeden databázový atribut. Není tedy možné pomocí vlastních typů rozkládat například nějaký složitější objekt do více databázových sloupců. Tato možnost se ale plánuje zahrnout do některých budoucích verzí Doctrine 2.

Dědění a konstruktor

Doctrine 2 nevyžaduje, abyste entitní třídy dědili od nějakého společného předka, jak je tomu u řady jiných ORM. Můžete tedy entity psát bez jakéhokoliv dědění či je odvodit od libovolného předka dle vlastní vůle a potřeby. Klidně i od Nette Object:

/** @entity */
class Article extends NetteObject
// ...

Stejná volnost platí i pro konstruktor, který si můžete definovat zcela podle své potřeby. Ovšem s jedním důležitým upozorněním – konstruktor se provádí jen a pouze při vytváření nové prázdné instance operátorem new. Pokud systém načítá již uloženou entitu z databáze, vytváří a plní její instanci, tak se již konstruktor nespouští.

Entita ani žádná její metoda nesmí být označena jako final, a to ani v kterémkoliv jejím předkovi. V opačném případě nebude moci Doctrine 2 vytvářet proxy třídy. Co jsou proxy třídy a k čemu jsou dobré, si řekneme někdy příště.

Gettery a settery

Doctrine 2 nevyžaduje pro svou funkčnost mít definované vůbec žádné gettery ani settery. Entita se bude správně ukládat do databáze a načítat z databáze i bez nich. Doctrine 2 totiž umožňuje data entit plnit a číst „zadem“, a to i pokud dané členské proměnné definujete jako  private.

Dokonce právě naopak, zapamatujte si, že v entitách nikdy nesmíte žádné členské proměnné definovat jako public a umožnit tak přistupovat k nim zvenku napřímo. Pokud to porušíte, nebudou vám entity v některých specifických případech fungovat podle očekávání.

Takže gettery a settery samozřejmě potřebujeme sami pro sebe, abychom mohli k datům entity přistupovat zvenku a entitu běžně používat.

V tomto nám Doctrine 2 vůbec nepomáhá. Dodržuje totiž pravidlo nulové magie a vše nechává na vývojáři. Sama neposkytuje žádné magické funkce, které by fungovaly tak nějak automaticky samy od sebe. Všechny gettery a settery, které chceme používat, je tedy nutné definovat explicitně.

Doctrine 2 tím pádem dokonce ani nezajišťuje vůbec žádné validace či konverze přijímaných a vracených dat, ty si musíme také zajistit sami. Jednou z možností je provádět je právě v setterech:

/** @entity */
class Article
{
    // ...
    public function setTitle($title)
    {
        $title = trim($title);
        if (!$title) {
            throw new Exception('Title is required field.');
        }
        if (strlen($title) > 100) {
            throw new Exception('Title should be up to 100 characters long.');
        }
        $this->title = $title;
    }
    // ...
}

Ač je to trochu víc psaní, máme zase veřejné rozhraní své entity naprosto pevně pod kontrolou a můžeme si je definovat přesně tak, jak chceme a jak potřebujeme.

Na druhou stranu, pokud si chcete ulehčit práci, můžete si bez problémů definovat nějakého předka, kam vyčleníte veškerý opakující se kód nebo klidně i nějakou tu magii. A od něj si pak můžete všechny své entity bez problémů podědit.

Klonování a serializace

Klonování a serializace Doctrine 2 entit je problematické. A to opravdu velice problematické. Pokud to tedy jen trošku jde, tak se snažte entity vůbec neklonovat a neserializovat. V opačném případě riskujete rozličné divné a nečekané chování.

Špatná zpráva navíc je, že s tím asi nepůjde ani v budoucnu nic udělat, protože se tu naráží na limity a principielní omezení samotného jazyka PHP.

Pokud byste ale klonování nebo serializaci entit opravdu někdy potřebovali, důkladně si předtím pročtěte návod, co a jak je nutné pohlídat a ošetřit.

Co dál?

Prošli jsme si základní možnosti definice entit. V nejbližších dílech se podíváme, jak pracovat s asociacemi mezi různými entitami nebo jak zajistit ukládání entit do databáze a jejich načítání z databáze.

Krátce před vydáním tohoto článku byla do vývojové verze Doctrine 2 zahrnuta funkční podpora kompozitních klíčů. Podrobnosti jsou popsány v článku Experimental Doctrine 2 Feature: Associated Entities as Id Fields na Doctrine blogu.

Komentáře

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

Prosím opravte si větu „Doctrine 2 totiž data entit plnit a číst „zadem“…“ (2. věta v prvním odstavci části Gettery a settery).

Ale dost bylo hnidopišství: jaký je tedy vlastně rozdíl mezi SEQUENCE a IDENTITY, jestliže pro IDENTITY se v PostgreSQL použije datový typ SERIAL (který implicitně tvoří sekvenci)? Jediný důvod, který mě napadá je možnost parametrizace SEQUENCE (a nemožnost parametrizace IDENTITY :).

Složené identifikátory mi připadají jako velká slabina Doctrine 2, protože speciálně spojovací tabulky jsou na nich bezpodmínečně závislé (nebo existuje pro práci M:N vazbami nějaký jiný mechanismus?), a nejsou to jen spojovací tabulky, kde to může být problém. Budou tohle autoři Doctrine 2 někdy v budoucnu řešit (při běžném prohledávání webu Doctrine 2 jsem bohužel nenašel žádny roadmap)?

Omezení v serializaci a klonování se podle manuálu na první pohled nezdají být až tak drastická, uvidím jak se s tím poperu až na to opravdu narazím.

Díky za seriál,
jen tak dál! :)

drevolution

jaký je tedy vlastně rozdíl mezi SEQUENCE a IDENTITY, jestliže pro IDENTITY se v PostgreSQL použije datový typ SERIAL (který implicitně tvoří sekvenci)? Jediný důvod, který mě napadá je možnost parametrizace SEQUENCE (a nemožnost parametrizace IDENTITY :).

Když použijeme strategii SEQUENCE, tak si entity manager zjišťuje jedním databázovým dotazem hodnotu identifikátoru během persistování entity.
Pokud použijeme IDENTITY, entity manager se o hodnotu identifikátoru nestará a nechává to na výchozí hodnotě sloupce, což u PostgreSQL při použití pseudotypu SERIAL znamená volání next_val(název_sek­vence). Navíc zde známe hodnotu identifikátoru až po flushnutí, což může ovlivnit naší práci s entitami v asociaci.

Osobně preferuji strategii SEQUENCE, protože mi zajistí, že znám hodnotu identifikátoru hned po persistování. Bohužel, autoři Doctrine2 stále nezměnili hodnotu AllocationSize z 10 na 1, takže si Doctrine alokuje hodnoty po 10, což ústí k tomu, že máme v hodnotách identifikátorů díry. Už dlouho slibují, že to změní na 1, ale ve zdrojovém kódu stále vidím 10. Stále si to tedy člověk musí u sebe přepsat. Je to v třídě ClassMetadata­Factory na řádku 396.

jos


Než se tedy Doctrine 2 dostane do první stabilní verze, raději se prozatím složeným identifikátorům vyhněte

to bych asi upravil na:
Než se tedy Doctrine 2 dostane do první stabilní verze, raději se prozatím Doctrine 2 vyhněte

Radek Tuc

Co se vicenasobnych identifikatoru tyce, predpokladam ze do finalni verze budou urcite doladeny. Jsou zaneseny i v XML schematu pro XML mapping, takze si myslim ze s nimi pocitaji (a jsou podporovany i v ORM Designer editoru).
Dal bych chtel podekovat za vynikajici serial. Kdyz jsem cetl prvni dil, bal jsem se onoho obvykleho uvodu, pak plytke shrnuti a hotovo. Zatim jsem mile prekvapen a jiz ted se tesim na dalsi dily.

koubel

“ .. důvodu některých budů v současné .. „

Jimilo

Možná předbíhám, ale zajímalo by mě, zda se v Doctrine 2 ukládají vždy všechny vlastnosti entit (označené @column) najednou nebo se nějak řeší ukládání jen změněné podmnožiny?
Pokud totiž má daný uživatel např. možnost pracovat jen s omezenou množinou vlastností, tak by při uložení všech vlastností najednou mohl přepsat data se kterými paralelně pracoval jeho kolega, který má oprávnění např. na úplně jinou podmnožinu. (příklad: ekonom nastavuje částku, vedoucí termíny, řešitel řešení apod.)

drevolution

Možná předbíhám, ale zajímalo by mě, zda se v Doctrine 2 ukládají vždy všechny vlastnosti entit (označené @column) najednou nebo se nějak řeší ukládání jen změněné podmnožiny?

Ukládají se pouze změněné hodnoty. Pro každou entitu se napřed spočítají tzv. change sety a ty se pak ukládají.

Pokud totiž má daný uživatel např. možnost pracovat jen s omezenou množinou vlastností, tak by při uložení všech vlastností najednou mohl přepsat data se kterými paralelně pracoval jeho kolega, který má oprávnění např. na úplně jinou podmnožinu. (příklad: ekonom nastavuje částku, vedoucí termíny, řešitel řešení apod.)

Tady už to zavání řešením atomicity ukládání a nějakým zamykáním entit. To Doctrine nijak neřeší. Jinak kontrolu, zda nějaký uživatel může upravit nějakou hodnotu, to si člověk musí napsat sám.

Matko

Kdy se dockame pokracovani serialu?

Martin Malý

Podle autorových slov již pozítří…

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.