Článek v rubrikách:

»PHP
»MySQL
»Dibi

ORM, Row Data Gateway, Table Data Gateway, Active Record, Data Mapper

Co to vlastně ORM je a jaké jsou návrhové vzory objektově relačního mapování. Jak efektivně pracovat s databází?


ORM je objektově relační mapování, je to jakýsi most mezi relačními databázemi (RDBMS) a objekty (OOP). Snažíme se o mapování objektů na data v databázi. Úkolem ORM by mělo být automatické skládání SQL dotazu a zabezpeční základních operací CRUD (create, read, update, delete). Chceme pracovat s objekty, aniž bychom se museli starat o to jakým způsobem budou v pozadí ukládány a získávány.

Tím snad obecný výklad končí, v některých detailech už se různé zdroje rozchází. Podle některých by mělo ORM zabezpečit i dědičnost (tu databáze neznají), mapování datových typů (např. bool mysql vůbec nemá - nahrazuje ho za tinyint(1)), validaci některých omezení jako je např. povinná hodnota (v mysql jedině jako NOT NULL, ovšem když se sql dotazu vůbec neúčastní pak je záznam bez problémů vložen), mapování asociací a sdružených objektů a další.

Nejvýznamější v této oblasti je asi autor a softwarový architekt Martin Fowler, zejména jeho kniha Patterns of Enterprise Application Architecture (ke stažení).

V této knize popisuje nejvýznamější návrhové vzory, které se týkají ORM. Dělí je na návrhové vzory architektury a návrhové vzory chování.

Návrhové vzory architektury (Architecture patterns)

Návrhový vzor architektury popisuje vazby mezi relačním databázovým úložištěm a objektovým modelem.

Domain Model (Doménový model) je souhrn tříd naplňující logiku vazeb, specifikuje atributy tříd, primární klíče, cizí klíče a další potřebné vlastnosti, které přesně odpovídají tabulkám v databázi. Jeden objekt odpovídá jedné tabulce v databázi.

Jednotlivé objekty, bychom pak měli vhodně rozšířit o business logiku, kterou databáze nemohou zajistit. Např. vydat zboží ze skladu lze až po jeho zaskladnění, nebo objednat zboží lze pokud je volná skladová dostupnost.

Table Data Gateway

Je přístup k databázové tabulce, komunikace s databází je obalena do jedné třídy, která data získává i čte, mapuje a vrácí kolekci dat. Najdeme v takové třídě metody jako insert, update, delete, ale i vyhledávací funkce find, findByUrl apod.

Insert

  1. $customer=new Customer();
  2. $customer->name="František";
  3. CustomerTable::insert($customer);

Update

  1. $customer=CustomerTable::find(1);
  2. $customer->name="František";
  3. CustomerTable::update($customer);

Row Data Gateway

Jedna se o model samotného řádku v tabulce databáze. Mapuje přesně vlastnosti objektu, které odpovídají struktuře databázové tabulky, tedy jednotlivým sloupcům. Sám umí provést běžné operace jako insert, update, delete. Pro samotné získání tohoto objektu se využívá externí třídy Finderu.

Insert

  1. $customer=new Customer();
  2. $customer->name="František";
  3. $customer->insert();

Update

  1. $customer=CustomerFinder::find(1);
  2. $customer->name="František";
  3. $customer->update();

Active Record

Kombinuje Row Data Gateway a funkce pro získání objektu z databáze (již existujícího modelu).

Insert

  1. $customer=new Customer();
  2. $customer->name="František";
  3. $customer->insert();

Update

  1. $customer=Customer::find(1);
  2. $customer->name="František";
  3. $customer->update();

Data Mapper

Tento návrh je vhodné použít pokud nelze zajistit zcela ekvivalentní nastavení objektu vůči databázi, například pro účely dědění, nebo jiných OOP vztahů, které relační databáze neumí zachytit. Data Mapper je prostředník mezi relačním schématem databáze a objektovým schématem, využívá se hlavně pro účely, kdy tyto pohledy nejsou totožné.

Problémy nastávají s určováním a povědomím mapperu o stavu objektu, o stavu v databázi a v aplikační vrstvě, jak předcházet duplicitám apod.

Vhodným příkladem je přihlašování uživatelů. Např. systém, který má vlastnosti redakčního webu a zároveň eshopu. Začnete stavět redakční systém a potřebujete kvůli diskusím logovat uživatele (userid, user, pass), později potřebujete zahrnout PR soutěže apod. a potřebujete znát o soutěžícím jeho adresu a jméno (userid, user, pass, name, adress), později přidáte možnost prodávat nějaké suvenýry a uděláte z toho eshop, potřebujete logovat zákazníka (userid, user, pass, name, adress, ICO, DIC, phone, email). A teď co s tím? V OOP budete mít základní objekt User a podědí ho Reader/Competitor a také Customer, ovšem relační databáze Vám tohle nedovolí a nezbyde nic jiného než udělat tři tabulky ve kterých udržujete všechny údaje duplikátně a nastává problém ze které tabulky budete ověřovat přihlašovací údaje. Přesné mapování tabulek v tomto případě může být problém a k vyřešení by měl pomoci Data Mapper.

Data Mapper je objekt , který požádáme o získání zákazníka a je už na něm jak se postará o namapování tohoto objektu a ze kterých tabulek, s objektem zákazníka už pracujeme jako v OOP a příliš nás nezajímá, jak je uložen v databázi. Ten samý objekt se stará o ukládání, mazání a další operace v databázi.

Insert

  1. $customer=array('name'=>"František");
  2. Mapper::Customers()->insert($customer);

Update

  1. $customer=Mapper::Customers()->findById(1);
  2. $customer->name="František";
  3. Mapper::Customers()->update($customer);

Návrhové vzory chování (Behavioral Patterns)

Tyto návrhové vzory se zabývají způsobem jak efektivně data načítat a ukládat. Jak je jednoznačně identifikovat v paměti, jak zajistit konkurenční přístup (jak ukládat data, které nám někdo uložil pod nosem) a další. Nejvýznamnější návrhové vzory jsou např. Unit of Work, Identity Map, Join fetching, Lazy Load atd.

Článek vznikl jako součást seriálu, kde se snažím vytvořit vlastní relační mapování postavené nad Dibi, zde je pro ujasnění pár pojmů ohledně ORM, které můžete prostudovat, abychom věděli o čem se bavíme.

Zajímavé zdroje (další vzory a názory odborníků na přístup)

Pro tvorbu vlastního ORM se asi budu soustředit na návrhový vzor Active Record, případně jeho nějakou kombinaci s Data Mapperem nebo jiným vzorem.

Repository pattern

Určitě stojí za zmínku, krátké porovnání Active Record a Repository pattern najdete v článku od Aleše Roubíčka.

 

podobné články

15.12.2009DbDibiOrm - DibiConnection - staticky, instančně, přistupovat přímo k dibi factory?(17%)
09.12.2009DbDibiOrm(17%)
07.12.2009ORM, dibi, MySQL defaultní hodnoty (pokračování článku)(17%)
03.12.2009MySQL povinné hodnoty, NULL hodnoty, defaultní hodnoty, prázdné hodnoty a jak by se mělo chovat ORM(17%)
22.07.2009ORM, Zend_Db, Doctrine, DibiTableX a co bych navrhl pro dibi(17%)

komentáře

RSS Komentáře k článku RSS Komentáře   Add to Google
15.12.2009 17:41 | Anonym (v6ak) | Dva detaily
  1. Databáze není databázový systém (rozdíl asi jako fieldy třídy a metody pro práci s nimi).
  2. Proč je tu ten magický přístup (ve smyslu statických metod apod.) a ne instance tabulky?
15.12.2009 19:18 | Administrátor | Re: Dva detaily
  1. ? která věta konkrétně? přijde mi slovo databáze všude v kontextu použitelné a pochopitelné?
  2. opět, která část konkrétně?
<?php
$customer=new Customer();
$customer->name="František";
CustomerTable::insert($customer);
?>

navrhuješ snad

<?php
$customer=new Customer();
$customer->name="František";

$customerTable=new CustomerTable();
$customerTable->insert($customer);
?>

?

jendak mi to přijde o řádek navíc a jednak mi přijde, že instance se vytváří úplně zbytečně, pokud to využiji v jedné metodě tak dobrá, ale to mám složitější kód, tak budu pokaždé vytvářet instanci? má to výhodu?

15.12.2009 21:07 | Anonym (v6ak) | Re: Re: Dva detaily
  1. „relačními databázemi (RDBMS)“
  2. Ano, přesně takto, ale vytvářet stačí jen jednou.

Jinak použití instancí může mít výhodu použití omezené tabulky – například mám tabulku aukcí. Typickým příkladem omezené tabulky je zde fixace sloupce s vlastníkem tabulky, takže výběr bude filtrován a při vkládání to bude automaticky doplněno. Při update taky není potřeba myslet na přidání další podmínky. Dělat to bez instancí by byl teprve hnus. Jinak můj názor je částečně ovlivněn i tím, že prostě nemám rád tento přístup a preferuji nějakou formu Dependency Injection. (Neříkám, že jsem jej nikdy nepoužil.) Pokud mám Singleton nebo obecně třídu se statickýma metodama, neměly by IMHO nijak umožnit měnit logický stav. (Úprava cache nemusí být změna logického stavu.)

15.12.2009 21:32 | Administrátor | Re: Re: Re: Dva detaily

Doufám, že se někdo přidá do diskuse k tvému názoru, protože si nejsem jist části:

„Jinak použití instancí může mít výhodu použití omezené tabulky – například mám tabulku aukcí. Typickým příkladem omezené tabulky je zde fixace sloupce s vlastníkem tabulky, takže výběr bude filtrován a při vkládání to bude automaticky doplněno. Při update taky není potřeba myslet na přidání další podmínky. Dělat to bez instancí by byl teprve hnus.“ prosím příklad, nebo bližší vysvětlení

ad. názor: už pár let používám to co ty vyzdvihuješ a opravdu mi vadilo pokud sem chtěl například vytvořit form s několika selecty a nemohl jsem použít

$form->addSelect(„I­Dexpedition“, ExpeditionTable::fet­chPairs());

$form->addSelect(„ID­guaranty“, GuarantyTable::fet­chPairs());

atd.

příjemnější mi bylo i

$form->addSelect(„ID­guaranty“, Guaranty::table()->fetchPairs());

než

$guarantyTable=new GuarantyTable(); $form->addSelect(„ID­guaranty“, $guarantyTable->fetchPairs());

dále proběhli nějaké další funkce a opět potřebovali vrátit něco od tabulky guaranty a opět sem musel vytvářet novou instanci, takže během jedné funkce jsem třeba 3× musel vytvořit instanci tabulky, jen proto, abych vypsal nějaké páry

ale to jen názor, protože funkce na 30–40řádků se stávali hodně nepřehlednými, nicméně stále je to přístup, který upřednostňuje X orm frameworků, řekněme si tedy proč je to špatné nebo je to jen otázka názoru

16.12.2009 15:49 | Anonym (v6ak) | Instanční přístup

Tak si představ toto: máš Presenter s aukcema jen aktuálního uživatele. Použiješ tam tedy něco jako: $this->auctions = $auctions->getSubtableFo­rOwner($userId);

Pak se nemusíš starat o user id při výběrech, vkládání ani úpravě, kde zapomenutí není na první pohled vidět, ale je to dost důležité.

16.12.2009 16:36 | Administrátor | Re: Instanční přístup

jestli tomu správně rozumím, kde získáš $userId

tuším že to bude vlastnost presenteru, nebo si ho získal o pár řádků výše z funkce která ho vrátila

tedy, spíš <? $this->auctions = $auctions->getSubtableFo­rOwner($this->user->userId);

každopádně nevidím problém v použití

$this->auctions = Auctions::get­SubtableForOw­ner($this->user->userId);

případně snad může být

$this->user->getSubtableFo­rOwner(); ?> tady je snad jasné že to patří userovi, a snad i vidím chybu v trvání na Dependency Injection protože tohle je proti logice

opravdu ale přesuňme debatu sem

http://www.webfaq.cz/…dibi-factory

vždyť přece staticky se snažím metodu volat jen v případě, kdy právě nemá co dělat se svým okolím, kdy nechci aby byl výstup nebo funkce ovlivněna jejím stav, což podle mne Customers::find nebo Customers::fin­dByName je! mě přece nezajímá jaký je stav objektu? a nechci aby to do toho vztahu nějak vstupovalo, chci vrátit kolekci zákazníků, kteří odpovídají nějakému jménu, proč do toho plést v jakém stavu je Customers / což více méně spíše chápu jako knihovnu funkcí než, aby zaznamenávala nějaký stav

16.12.2009 17:57 | Anonym (v6ak) | Re: Re: Instanční přístup

„$this->auctions = Auctions::get­SubtableForOw­ner($this->user->userId);“

To možné teoreticky je, ale pak k podtabulce přistupuješ poněkud jinak než tabulce. Kompromisem by bylo to implementovat instančně a udělat statickou obálku.

„opravdu ale přesuňme debatu sem“ No toto se týká spíše samotných ORM modelů než připojení.

„proč do toho plést v jakém stavu je Customers / což více méně spíše chápu jako knihovnu funkcí než, aby zaznamenávala nějaký stav“

Zaznamenává stav tabulky (uložené řádky). Takových tabulek můžu mít i víc. To je důvod, proč mám principiálně radši instance.

16.12.2009 19:56 | Administrátor | Re: Re: Re: Instanční přístup

Myslím, že tohle je otázka celého programování, ani tak ne ORM. Tabulek právě více mít nemohu, pokud přistoupím k pravidlu, že co tabulka to model.

„To možné teoreticky je, ale pak k podtabulce přistupuješ poněkud jinak než tabulce. Kompromisem by bylo to implementovat instančně a udělat statickou obálku.“

Já chápu dělení ORM na abstraktní model řádku a abstraktní model tabulky (který chápu spíše jako knihovnu funkcí, knihovnu chování, bez závislosti na okolí / vlastním stavu).

Tedy podle mne u Row by měla samozřejmě existovat instance (zajímá mne stav, vlastnosti atd.), u Table snad ani moc důvod k instanci nevidím, resp. proč ne, ale chápu to spíš jako knihovnu která pracuje konrétně s databází, manipuluje s jednotlivými Row objekty, ale sama si nic pamatovat nemusí

17.12.2009 10:52 | Anonym (v6ak) | Re: Re: Re: Re: Instanční přístup

To použití instancí je do jisté míry spíše filozofický postoj: Ano, v rámci databáze máš tu tabulku jen jednu. Ale můžeš mít více databází. Byl by to asi trošku extrém je používat v jedné aplikaci během jednoho požadavku zároveň. Ale má to i praktické dopady, minimálně:

  • Tabulka může implementovat nějaké rozhraní a lze potom pracovat s tabulkou obecně.
  • Výše zmíněné omezené tabulky.
15.12.2009 21:52 | Administrátor | Re: Re: Re: Dva detaily

Chápu, že Dependency Injection má dobré myšlenky, ale dá se to v tomto příkladě nějak v budoucnu použít? Tedy říkáš, že statické metody ne, že Customer::find(1) není správně, protože chceme umožnit využívat instanční metody a vlastnosti instance, tedy tato metoda může v nějakém případě mít potřebu přistupovat k něčemu co je instanční a zaměnitelné, krom přístupu k databázi, tedy getDbConnection, což by mne prakticky také zajímalo jak je realizovatelné?

<? $c1=new Customer(); $c1->save();

$c2=new Customer();

chceš říct, že tady je správné provést něco jako

$c2->setDbConnecti­on(dibi::getCon­nection(‚db2‘));

$c2->save();

mě totiž děsí, že pod nohama objektu Customer odpálím připojení k databázi se kterým třeba počítá

kdy člověk pracuje v rámci jednoho objektu, jednoho modelu s více databázemi zároveň? napadá mne jedině nějaká forma synchronizace (musí se tedy jednat o naprosto totožné databáze, co když před metodou save ověřujeme jestli má zákazník nějaké produkty objednané a pokud ano zapíšeme i rezervace s jeho registrací? pak tedy voláme objekt Product, který nepočítá s tím, že Customer je už někde jinde, že zákazníka ukládá jinam a produkty by měl ověřovat také v databázi jiného systému? no nevím)

16.12.2009 15:52 | Anonym (v6ak) | Re: Re: Re: Re: Dva detaily

Takto určitě ne, instanci připojení bych předával v konstruktoru.

16.12.2009 16:28 | Administrátor | Re: Re: Re: Re: Re: Dva detaily

Dělám to tak a dlouho, ale příjde mi to jako neustálý boj. Myslím, že tahle debata patří spíše do tohoto článku.

http://www.webfaq.cz/…dibi-factory

16.12.2009 15:22 | Anonym (Petr Jirásek) | Pěkné

I když nejsem na tak vysoké úrovni, abych zde byl schopen debatovat nad koncepcí ORM vrstvy, tak i tak bych chtěl pochválit autora, protože jsem si konečně udělal bližší představu, co to vlastně ORM znamená.

Jméno
Název
Text
Lze používat Texy! syntax. Příklad syntaxe: "text odkazu":odkaz, **tučně**, *kurzíva*, `code`. PHP kód uzavírejte do <?php ... ?> a JavaScript do <script> ... </script>