Nette\Database ORM Challenge

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
nanuqcz
Člen | 822
+
+2
-

Ahoj,
rozhoduju se právě pro použití některého ORM pro Nette. A myslím, že by bylo super mít ukázky implementace stejného úkolu v různých ORM knihovnách. Nějaké jednoduché, ale netriviální zadání, kde se ukáže, jak konkrétní ORM řeší praktické problémy. Takže:

Zadání

  • Máme následující strukturu databáze: sql dump, schema.
  • Vypište názvy všech článků (seřazeny od nejnovějšího). U každého článku také vypište název kategorie, obsah článku a seznam jeho tagů, které v DB nejsou označeny jako deleted=1.
  • Šablona bude vypadat takto, případně takto.

Pravidla

  1. Musí jít o spustitelnou implementaci postavenou na Nette sandboxu.
  2. Verze Nette 2.1-stable.

Budu rád za jakékoli implementace (v YetORM, Nextras/ORM, NDab, Fabik/Database, Lean Mapper, Doctrine 2, …). Já si zkusím co nejdřív najít čas na implementaci v YetORM, který mě zaujal na první pohled nejvíc.

Editoval nanuqcz (2. 2. 2014 19:52)

nanuqcz
Člen | 822
+
0
-

Tak přikládám slibovanou implementaci v YetORM: https://bitbucket.org/…rm-test/src/.

U YetORM jsem byl trochu zklamaný z toho, že všechny instance entit jsem si nakonec stejně musel vytvářet sám. Doufal jsem, že to bude YetORM dělat tak nějak automaticky. Na druhou stranu je tam docela hezká berlička pro M:N vazby, které jsou v čisté NDB jak známo trochu problém.

Tharos
Člen | 1030
+
0
-

Ahoj,

tak tady máš jedno možné řešení využívající Lean Mapper.

Omlouvám se, že jsem nedodržel pravidla a nepostavil to nad Nette sandboxem, ale odradilo mě, že čistý sandbox obsahuje nějaký formulář (?), nějaký UserManager (?), SignPresenter, BasePresenter… zkrátka balast a IMHO zbytečně nedodržuje PSR-0. Nicméně věřím, že můj kód pro Tebe bude dobře pochopitelný.

Lean Mapper je dost flexibilní a třeba ty soft deleted tagy by šly vyřešit hned několika způsoby. Vybral jsem jeden vcelku stručný a snad elegantní.

Kdybys měl chuť, klidně mi k tomu poskytni nějakou zpětnou vazbu, budu jenom rád. :)

Editoval Tharos (2. 2. 2014 23:04)

Tharos
Člen | 1030
+
0
-

Mimochodem, spustil jsem si Tvé řešení v YetORM a mám k tomu dvě věci:

1) Jsi si jist, že dodržuješ své vlastní zadání ;)? Mám podezření, že ty články nemáš seřazené od nejnovějších (tj. sestupně podle id).

2) Lean Mapper položí následující čtyři dotazy:

SELECT `article`.*
FROM `article`
ORDER BY `id` DESC

SELECT `article_category`.*
FROM `article_category`
WHERE `article_category`.`id` IN (1, 3, 2)

SELECT `article_2_tag`.*
FROM `article_2_tag`
WHERE `article_2_tag`.`article_id` IN (31, 30, 29, 28, 27, 26, 25, 24, 23)

SELECT `tag`.*
FROM `tag`
WHERE `tag`.`id` IN (2, 3, 4, 1, 5) AND `deleted` != 1

Zatímco YetORM (Nette\Database?) jich položí celkem osm a jsou trochu divné:

SELECT `id`, `title`, `article_category_id`, `content_html`
FROM `article`

SELECT `id`, `name`
FROM `article_category`
WHERE (`id` IN (1, 2, 3))

SELECT `article_2_tag`.`id`, `article_2_tag`.`article_id`, `article_2_tag`.`tag_id`
FROM `article_2_tag`
LEFT JOIN `tag` ON `article_2_tag`.`tag_id` = `tag`.`id`
WHERE (`article_2_tag`.`article_id` IN (23, 24, 25, 26, 27, 28, 29, 30, 31)) AND (`tag`.`deleted` =
0)

SELECT COUNT(*), `article_2_tag`.`article_id`
FROM `article_2_tag`
LEFT JOIN `tag` ON `article_2_tag`.`tag_id` = `tag`.`id`
WHERE (`article_2_tag`.`article_id` IN (23, 24, 25, 26, 27, 28, 29, 30, 31)) AND (`tag`.`deleted` =
0)
GROUP BY `article_2_tag`.`article_id`

SELECT `id`, `name`
FROM `tag`
WHERE (`id` IN (1, 2, 5))

SELECT `id`, `name`
FROM `article_category`
WHERE (`id` IN (1, 2, 3))

SELECT `id`, `name`
FROM `article_category`
WHERE (`id` IN (1, 2, 3))


SELECT `id`, `name`
FROM `article_category`
WHERE (`id` IN (1, 2, 3))

Proč tolik dotazů? A proč se druhý dotaz v pořadí pak ještě třikrát opakuje?

Neměly být podobné věci ve stable Nette 2.1 již vyřešené? Anebo je to problém YetORM?

nanuqcz
Člen | 822
+
0
-

Tharos: Jj díky, na to řazení jsem zapomněl :-D Hned pushnu opravu.

Těch dotazů jsem si všiml, ale zatím jsem to moc do hloubky neřešil, protože zkouším ještě Fabik/Database.

nanuqcz
Člen | 822
+
0
-

Implementace ve Fabik/Database: https://bitbucket.org/…ase-test/src.

Fabik/Database se mi ve výsledku líbí. Automatické mapování výsledků na entity funguje perfektně, dokonce i při takovýchto voláních. Na druhou stranu nevím, co vlastně mají představovat potomci Fabik\Database\Table a jak bych měl s nima pracovat (IMHO to budou vždy prázdné třídy, dědící z Fabik\Database\Table). Taky bohužel Fabik/Database není updatovaný na aktuální Nette, a tak jsem mu musel šáhnout do zdrojáků a zakomentovat tam 4 řádky.

EDIT: Zapomněl jsem na řazení :-D řazení přidáno

Editoval nanuqcz (2. 2. 2014 23:54)

Tharos
Člen | 1030
+
0
-

nanuqcz napsal(a):
EDIT: Zapomněl jsem na řazení :-D

Přemýšlel jsem, jestli Tě mám prudit. :D

nanuqcz
Člen | 822
+
0
-

Tak, řazení v obou projektech opraveno :-)

uestla
Backer | 799
+
0
-

Ahoj, díky za zpracování ukázky v YetORMu:

ad počet dotazů:

Dotaz pro COUNT(*) se pokládá proto, že se v šabloně volá vícekrát $article->getTags(), který pokaždé vytvoří novou instanci EntityCollection, čili data načtená v první vytvořené instanci se do druhé nedostanou a proto nový dotaz. Řešením je buď použít v Latte „obrácenou“ podmínku {/if $iterations}, nebo uložit instanci kolekce do proměnné.

Další zbytečný dotaz je v Article::getCategory(), kde doporučuji kontrolovat nejdřív hodnotu sloupce s klíčem a podle té až pak přistupovat ke spřaženému záznamu.

Pullík ;-) (po úpravách už jsou dotazy také jen 4)

castamir
Člen | 629
+
0
-

@uestla obrácená podmínka?!? Co to je? :D

Možná to ve výsledku dělá to, co by to dělat mělo, ale ta konstrukce je pro mě nečitelná a už vůbec ne intuitivní. Neexistuje jiné řešení?

Majkl578
Moderator | 1364
+
0
-

Přidávám řešení s Doctrine 2 ORM, zde na mém GitHubu: https://github.com/…ge-doctrine2

S modelovou vrstovu jsem se zrovna moc nepáral, proto ten dotaz přímo v presenteru. :)
Použité kdyby/doctrine je jen kvůli snazší integraci, nechtělo se mi patlat s konfigurací Doctrine manuálně. Na dotazy apod. nemá vliv.
Řešené je to nad ořezaným nette sandboxem (fuj, tam je ale zbytečného smetí!).

TL;DR: Doctrine 2 = 1 dotaz s funkčním OR mapováním.

Tharos
Člen | 1030
+
0
-

@uestla: Díky za vysvětlení. Jsem upřímně rád, že to nebyl bug v Nette\Database.

@Majkl578: Hezký!

Pavel Macháň
Člen | 282
+
0
-

Majkl578 napsal(a):

Přidávám řešení s Doctrine 2 ORM, zde na mém GitHubu: https://github.com/…ge-doctrine2

S modelovou vrstovu jsem se zrovna moc nepáral, proto ten dotaz přímo v presenteru. :)
Použité kdyby/doctrine je jen kvůli snazší integraci, nechtělo se mi patlat s konfigurací Doctrine manuálně. Na dotazy apod. nemá vliv.
Řešené je to nad ořezaným nette sandboxem (fuj, tam je ale zbytečného smetí!).

TL;DR: Doctrine 2 = 1 dotaz s funkčním OR mapováním.

Huh ta Doctrine me prijde teda dost ukecana

Majkl578
Moderator | 1364
+
0
-

EIFEL napsal(a):

Huh ta Doctrine me prijde teda dost ukecana

No tak samozřejmě můžeš použít jen findAll()/findBy(), ale to pak nebudou ty dotazy přes vazby optimalizované. Ten filtr na tagy bych osobně udělal globálně, ale nechtěl jsem příklad komplikovat. Další „úspora“ v ukecanosti je nepoužívat query builder, ale ten je mi pohodlnější.

nanuqcz
Člen | 822
+
0
-

Majkl: Mě ta Dotrine přijde takhle na první pohled super. Ukecanost vidím jen trochu v anotacích entit (možná tohle myslel EIFEL).

Majkl578
Moderator | 1364
+
0
-

Souhlasím, taky mi to přijde spíš jako výhoda než nevýhoda. Nemám rád zkratky, zkrácené zápisy a moc „generované magie“. :) Ty anotace by se daly asi taky zjednodušit, akorát protože používáš nestandardní názvy sloupců, není pro to moc prostor. Případně je můžeš mít někde bokem v neonu/yamlu.
Plus ještě ukecanost v getterech/setterech, ale ta je spekulativní, např. v Kdyby/Doctrine je BaseEntity, která tohle automatizuje (ač na můj vkus je to už moc magické) a celá entita pak může vypadat např. takto stručně (samozřejmě vlastnosti protected, nikoliv public).

nanuqcz
Člen | 822
+
0
-

Majkl: Pokud to není už moc off-topic, jaké názvy sloupců jsou tedy pro Doctrine „standardní“? Předpokládám, že mluvíš o cizích klíčích (article_category_id) a spojovacích tabulkách (article_2_tag), nebo i o jiných věcech?

Majkl578
Moderator | 1364
+
0
-

Standardní ve smyslu defaultní je název entity/vlastnosti/asociace == název tabulky/sloupce (např. „contentHtml“ == „contentHtml“). To je defaultní naming strategie. Pak ještě disponuje underscore steategií kdy „contentHtml“ == „content_html“. Případně si můžeš implementovat vlastní (viz odkaz).

uestla
Backer | 799
+
0
-

OT – @castamir: Obrácenou podmínku Davídek popisoval např. na Poslední sobotě, v tomhle případě je to spíš moje lenost zjišťovat počet záznamů, ale má to i ten hezčí efekt menšího počtu dotazů. Jde o to, že EntityCollection je lazy (což drobně souvisí i s tím, že bych v příkladu viděl raději volání $articles->orderBy('id', YetORM\EntityCollection::DESC), ale je to detail).

Pavel Macháň
Člen | 282
+
0
-

nanuqcz napsal(a):

Majkl: Mě ta Dotrine přijde takhle na první pohled super. Ukecanost vidím jen trochu v anotacích entit (možná tohle myslel EIFEL).

Jj šlo mě spíš o anotační ukecanost. Každopádně se na Doctrine jednou chystám, ale zatím si vystačím s LeanMapperem a není čas laborovat s Doctrine

Editoval EIFEL (3. 2. 2014 16:33)

hrach
Člen | 1844
+
0
-

Pro demo na posobotu jsem implementoval dosti podobne zadani, akorat misto kategorii jsou komentare. Stitky tam jsou, ale deleted=1 strategie se naopak vyuziva u tech komentaru, kde to imo dava vetsi smysl. Nicmene pro tagy by se to delalo uplne stejne. Kazdopadne, reseni splnuje nastinene aspekty.

https://github.com/hrach/orm-demo