Nette\Database a hezčí podpora pro M:N vazby

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

Problém

Nelíbí se mi, když mám v šabloně něco takového:

{foreach $books as $book}
    <h2>{$book->title}</h2>

    {foreach $book->related('book_tag') as $book_tag}  // kodér tohle nechce psát :-)
        {$book_tag->tag->name}{sep}, {/sep}            // a tohle taky ne
    {/foreach}
{/foreach}

Řešení

Zavést metodu addShortcut($name, $path):

Použití v modelu:

$database->table('book')
	->addShortcut('tags', 'book_tag:tag')  // Database teď už ví, že `$book->tags` má hledat přes `->related('book_tag')->fetch()->ref('tag')`

A v šabloně pak už (pro kodéra srozumitelně):

{foreach $books as $book}
    <h2>{$book->title}</h2>

    {foreach $book->tags as $tag}
        {$tag->name}{sep}, {/sep}
    {/foreach}
{/foreach}

Editoval nanuqcz (23. 2. 2012 2:22)

Honza Marek
Člen | 1664
+
0
-

Já bych se spokojil i s

$book->vyberMn('tag')->přesTabulku('book_tag') // názvy metod jsou ilustrativní

A první věc, kterou jsou v tomto zkoušel bylo

$book->related('book_tag')->ref('tag')

ale tyhle metody jsou očividně dostupné jen na ActiveRow.

nanuqcz
Člen | 822
+
0
-

Já bych se spokojil i s

$book->vyberMn('tag')->přesTabulku('book_tag') // názvy metod jsou ilustrativní

Troufám si říct, že můj návrh je hezčí :-) V ideálním světě (tedy v „Nette světě“ :-) ) by kodér neměl muset znát strukturu databáze. Můj návrh řeší také to, že by kodér ani nemusel používat v šablonách metodu related(), pokud programátor správně napíše model:

$authors = $database->table('author')
		->addShortcut('books', 'book')  // Database teď už ví, že `$author->books` má hledat přes `->related('book')->fetch()`

v šabloně

{foreach $author->books as $book}         // místo `$author->related('book')` napíše kodér jen `$author->books`
	{$book->title}: {$book->slogan} <br>
{/foreach}

A první věc, kterou jsou v tomto zkoušel bylo

$book->related('book_tag')->ref('tag')

ale tyhle metody jsou očividně dostupné jen na ActiveRow.

Tady nevím, jestli přesně chápu, co tím chceš říct. Nette\Database by si ony zkratky (shortcuty) ukládala do nějaké proměnné $shortcuts, kterou by pak měla k dispozici i ActiveRow.

paranoiq
Člen | 390
+
0
-

„Zavést metodu addShortcut($name, $path)“ – to je jen obezlička na věc, kterou by za rozumných konvencí měla umět řešit reflexe sama

nanuqcz
Člen | 822
+
0
-

paranoiq: Konvence ale nikdy nedokáže vyřešit problémy typu „sloupečky author_id a moderator_id ve stejné tabulce, oba odkazující na tabulku user apod.

Jinak s tebou ale souhlasím. Každopádně i za „rozumných“ konvencí by metoda addShortcut() měla smysl.

hrach
Člen | 1819
+
0
-

Pro daný problém mám hezčí řešení, které budu později publikovat. A pozor, nebude to nativně v Nette\DB.

nanuqcz
Člen | 822
+
0
-

hrach: škoda.. Ale díky, těším se :-)

hrach
Člen | 1819
+
0
-

Tak, plnim sliby a tu je ukazka meho reseni, ktere je funkcni, ackoliv plne v plenkach.
https://github.com/…okEntity.php#L24

nanuqcz
Člen | 822
+
0
-

hrach: Jestli to chápu dobře, tak: V aplikaci nějak nastavím Nette\Database, aby místo ActiveRow vracela moji třídu (potomka Ndab\Entity). V tomto potomkovi si můžu překrýt metody pro vracení různých related záznamů.

To je určitě, co se týče objektového návrhu, skvěle vymyšlené řešení, ale nebude to zbytečně moc upsané? Pokud budu mít v DB dvacet tabulek, musím v projektu vytvořit dvacet nových tříd (potomků Ndab\Entity), abych v šabloně mohl používat hezký zápis?

hrach
Člen | 1819
+
0
-

@nanuqcz:
V podstatě ano. Na signály.cz máme něco podobného, akorát to není na nette db, takže se tam řeší uplně jiné věci. Tabulek mají 70, takových entit je tam samozřejmě méně. A je to věc, který je opravdu hodně hodně skvělá. Tento dvouvrstvý model je imo nejlepší praktická implementace modelu. Jeden typ tříd data vybírá(řídí) – manager/repository/…, druhý reprezentuje dat – entity/…

To, že nemůžu Nette\Database říct, jaký objekt má vracet, je bohužel Davidův názor. V tuto chvíli je tedy ndab obchází celkem hnusně a je třeba data selektovat přes Ndab\Selection.

Honza Marek
Člen | 1664
+
0
-

hrach napsal(a):

To, že nemůžu Nette\Database říct, jaký objekt má vracet, je bohužel Davidův názor.

Jo, to mi naštvalo, když jsem myslel, že rozšířím ActiveRow o podporu m:n. Všude v kódu natvrdo new ActiveRow, nikde žádná IRowFactory :)

Jinak pokud to dobře chápu, tak ta tvoje nadstavba řeší prakticky jen něco jako setRowClass a podporu m:n relací?

hrach
Člen | 1819
+
0
-

Ano, tedy prozatim. Predlokladam ze se najdou dalsi pekne veci. Jak sem psal v tweetu, je to zatim porad jen koncept. Prijim pully s vylepsenim :)

David Grudl
Nette Core | 7569
+
0
-

Měnit třídu ActiveRow je balancování na hraně antipatternů. Každopádně v dev verzi je továrna function createRow().

hrach
Člen | 1819
+
0
-

Čekám na ukázkovou implementace mazání komentářů pod článkem, které smí mazat admin, nebo autor daného komentáře. A čekám, že budu mít čistou šablonu a bude využito nettí acl.

Nic takového aktuálně nejde. Jde to krásně s ndab, ačkoliv, tak je uvnitř plná antipattern. (Budiž). Upřímě je mi to uplně jedno, pro mě je důležítý výsledek a uživatelské(=programátorské) rozhraní, které musí být čisté a samo o sobě vést k čistému kodu.

Antipattern se tu zacina stavat urazkou na cokoliv, co klade prednost na pouzitelnost a ne na teoretickou cistost. Cemu proboha vadi takoveto upravovani tridy ActiveRow? Cemu?

nanuqcz
Člen | 822
+
0
-

Nepovažuji se za nějak zkušeného programátora, ale dovoluji si souhlasit s Hrachem.

Věřím, že tohle by ničemu v praxi nevadilo. O kterém antipatternu konkrétně tu mluvíme?

jtousek
Člen | 951
+
0
-

Pohnulo se tohle nějak? Jakmile jsem potřeboval M:N vazbu tak tahle feature mi v Nette\Database citelně chyběla.

nanuqcz
Člen | 822
+
0
-

Znovu bych rád otevřel tohle téma. Zrovna jsem dokončil jeden projekt, kde jsem používal NetteDB a chvílemi mi práci spíš přidělávala, než ulehčovala. Přitom je ale v NetteDB geniální nápad a rád bych ji pomohl dostat do stavu, kdy se bude krásně používat za všech okolností. Zároveň jsem zjistil, že je potřeba daleko silnější API, než původní nápad s addShortcut().

Příklad: Chtějme vypsat seznam článků, a k nim seznam tagů. Pokud v modelu použijeme return $db->table('article'), tak na kodéra čeká škaredá práce s ->related():

{foreach $articles as $article}
  <h2>{$article->title}</h2>
  Tagy: {foreach $article->related('article_tag') as $article_tag}{$article_tag->tag->name}, {/foreach}
{/foreach}

Co když budeme chtít zobrazit jen aktivní tagy (v tabulce tag mějme sloupeček active s hodnotami 0, nebo 1):

{foreach $articles as $article}
  <h2>{$article->title}</h2>
  Tagy: {foreach $article->related('article_tag')->where('tag.active = ?', 1) as $article_tag}{$article_tag->tag->name}, {/foreach}
{/foreach}

A teď si představte, že tagů bude u každého článku moc, a tak budeme chtít zobrazit jen ty, které zpětně obsahují nejvíce článků. Zkuste si to napsat. Tohle už je programování v šabloně…

Hrachovo řešení

Taky bych se rád vyjádřil k Hrachovu řešení. Na nedávné přednášce David vyslovil myšlenku, že NetteDB má sloužit pro případy, kdy nechceme použít ORM. Ta myšlenka se mi opravdu líbí. Tím, že se ale Hrach snaží pro řádky každé z tabulek v DB psát vlastní třídu, z toho ORM vlastně dělá. Jeho nápad je objektový (tzn člověk se šíleně moc upíše) a podle výše zmíněné Davidovy (i mojí) myšlenky si myslím, že je to non-nette-db-way :-)

Jak bych si tedy představoval hezké řešení já?

Celá myšlenka stojí na tom, že každý Table\Selection by měl instanci svých vlastních konvencí (což je logické, protože každá tabulka má jiné cizí klíče). Tyto „nádstavbové“ konvence by jen rozšiřovaly klasické DiscoveredReflection a ConventionalReflection (které by v NetteDB samozřejmé zůstaly), a programátor by měl možnost tyto nádstavbové konvence upravovat.

Celé by to mohlo být zabalené např do takovéhohle API:

# V modelu

// Vybereme všechny články
$articles = $db->table('article');

// Hrátky s vlastními konvencemi
$aConv = $articles->getConventions();
$aConv->addColumn('tags', 'article_tag:tag');  // řekneme databázi, kde má hledat tagy

$aConv['tags']->where('active', 1);  // v šabloně chceme zobrazovat jen aktivní tagy
# V šabloně
{foreach $articles as $article}
  <h2>{$article->title}</h2>
  Tagy: {foreach $article->tags as $tag}{$tag->name}, {/foreach}
{/foreach}

Rád bych se o tom pobavil taky zítra na Poslední sobotě.

hrach
Člen | 1819
+
0
-

Uf, trochu se v tom motáš.

  1. já se nesnažím z nette\database udělat orm. Naopak se snažím ho od toho odstřihnout (odstranit metody find, atp.)
  2. „Jeho nápad je objektový (tzn člověk se šíleně moc upíše)“ achjo, skoro bych rekl „zase nekdo bez praxe“. Pracuji na dvou/trech opravdu velkych aplikacich. Jedna v tomto pristupu, druha castecne v tomto pristupu, treti (nemam za ni zodpovednost) bez tohoto pristupu. Hadej, kde se nejvic upisu…
  3. „Jeho nápad je objektový“ no nevim, co si pak mam predstavit pod tvojim $aConv = $articles->getConventions(); $aConv->addColumn('tags', 'article_tag:tag');

Nebranim se impelmentaci m:n zlehcovatka, ale zatim sem nevidel zadne pekne api / fungovalo stejne jako muj pristup.

Take je treba zminit, ze nemam zadne privilegium tu ridit vyvoj Nette\Database. Tedy, klidne udelejte pully a David to treba prijme :) Ale toto me zatim opravdu nepresvedcilo.

nanuqcz
Člen | 822
+
0
-

Sry, asi jsem to napsal fakt špatně. Prostě se mi nechce pro každou tabulku v DB vytvářet další vlastní třídu, pokud můžu čistotu kódu udržet i bez toho. Samozřejmě jsem pro, ať je řešení tohohle problému správně objektově navrhnuté – ale ať je to řešené uvnitř frameworku, a ne ať je programátor nucen vytvářet pořád další třídy a soubory.

Moje API (které jsem si ale vycucal z prstu dneska během asi desíti minut), by se určitě dalo napsat líp. Přesto se mi ale líbí a vlastně se stejný přístup v Nette už používá.

Pro srovnání:

$form->addText('name')
	->getControlPrototype()->addClass('foo');

$db->table('article')
	->getConventions()->addColumn('tags', 'article_tag:tag');
hrach
Člen | 1819
+
0
-

Proboha proc by Selection melo byt nejake conventions?

foreach ($article->related('article_tags:tag')->where('active', 1) as $tag) {
	$tag->name;
}
nanuqcz
Člen | 822
+
0
-

Protože pro každý Selection, který si vytáhnu z DB, můžou platit jiné konvence (nebo si to nazveme jinak, třeba „pohled na data“, pokud chceš).

Příklad: Pokud si vytáhnu články, bude platit konvence, že tagy se získávají přes tabulku article_tag:tag. Naproti tomu zboží z e-shopu (např. tabulka item) může mít taky tagy, ale dostaneš se k nim přes item_shop_tag:shop_tag. Navíc můžu chtít uživatelům zobrazit jen aktivní tagy, ale v administraci chci zobrazit (z nějakého důvodu) všechny tagy. Takže jednoduše řeknu, že pro uživatele platí konvence ->where('active', 1) a pro admina ne.

# V ArticleModelu
public function getArticles() {
	$articles = $this->db->table('article');

	$articles->getConventions()
		->addColumn('tags', 'article_tag:tag')->where('tag.active', 1);

	return $articles;
}

# V AdminModule\ArticleModel
public function getArticles() {
	$articles = $this->db->table('article');

	$articles->getConventions()
		->addColumn('tags', 'article_tag:tag');

	return $articles;
}
nanuqcz
Člen | 822
+
0
-

hrach napsal(a):

foreach ($article->related('article_tags:tag')->where('active', 1) as $tag) {
	$tag->name;
}

Tak jsem si to zkoušel, a $article->related('article_tags:tag') nefunguje (proto taky v Ndab existuje metoda getSubRelation() – do které musíš předávat callbacky, abys mohl výsledek např. seřadit – ne?)

Tak mě napadá, kdybych se chtěl pokoušet o implementaci toho mojeho nápadu, nebyl bych nakonec taky nucen sklouznout k metodám jako „getSubRelation“ apod.? Pak by to moc nemělo smysl, protože tohle by se do oficiálního Nette asi nedostalo…

hrach
Člen | 1819
+
0
-
  • article->related('article_tags:tag') byl návrh api…
  • neni to o nic vic zavisle na konvencich nez normalni related.
nanuqcz
Člen | 822
+
0
-

Znovu jsem se díval na Ndab a začíná se mi líbit ;-) Pokud by v Nette fungoval ten vylepšený related(), začal bych ji používat (a postavil si na něm vrstvu, která by uměla i getConventions() :-))

jtousek
Člen | 951
+
0
-

Připravil jsem experimentální implementaci této funkce. Jako základ jsem použil implementaci od @juzna, doplnil implementaci insert a delete, připsal testy a provedl rebase na aktuální master.

Nevím si moc rady s podporou agregací takže to hází NotImplementedException. Rád bych kdyby se na to podíval někdo další.

hrach
Člen | 1819
+
0
-

No, juznuv pristup je spatny, protoze filtrovaci api nabizi az nad tabulkou tags, misto aby filtroval uz book_tags. To muze vest k teoretickymu obrovskemu resultsetu tagId, ktery bude nasledne vyfiltrovan. Proto je treba filtrovani provadet uz na spojovaci tabulce a do iteratoru nejakeho toho MNGroupedSelection poustet opravdu chteny resultset.

jtousek
Člen | 951
+
0
-

Jestli to dobře chápu, tak se ti nelíbí tohle. Raději tedy vycházet rovnou z tabulky tags s backjoinem na tabulku book_tags?

jtousek
Člen | 951
+
0
-

Tak jsem to přepsal. Co myslíš teď?

Editoval jtousek (5. 9. 2012 14:19)

Climber007
Člen | 105
+
0
-

Jak to vypadá s vývojem hezčí podpory M:N vazeb? Nemůžu nikde najít, jak to nakonec dopadlo.

Editoval Climber007 (3. 2. 2013 0:23)

Šaman
Člen | 2574
+
0
-

Taky by mě zajímalo, jak to vypadá? Potřeboval bych možnost získat Selection tagů (nikoliv pole, které vrací getSubRelation()), jinak mi je k prdu, že vazby 1..m vrací Selection. Když změním násobnost vazby (např. původně mohla mít kniha jednoho autora a pak se rozhodnu že jich může mít víc a vložím mezitabulku), tak se mi dost brutálně změní práce s výsledkem metody getBooks() v entitě author.

Je mi jedno, jestli to bude hezký, nebo dlouhý zápis, ale potřebuji dostat stejný výsledek pro oba druhy násobných vazeb a k dispozici mám jen $row na jedné straně (jsem v Entitě, takže načítat třeba tagy přes connection nelze, nebo jen za cenu extraprasárny.)