Návrh modelu produktu, který načítá z databáze cenu dle vnějšího kontextu
- tobice
- Člen | 30
Mám eshop, ve kterém se cena produktů může lišit podle aktuálně přihlášeného uživatele (např. „věrný zákazník“ má přístup k cenovým akcím). Tuto cenu potřebuji používat napříč celou aplikací, ať už při vypisování v šablonách (přehled kategorie, vyhledávání, detail produktu) či ve vnitřní logice aplikace (vytváření objednávek). Určení konkrétní ceny není zcela triviální záležitost, je nutné zJOINovat několik tabulek a zároveň zohlednit aktuálně přihlášeného uživatele. Navíc se může způsob výpočtu v budoucnu změnit. Z tohoto důvodu je nutné mít tento výpočet nadefinovaný na jednom jediném místě.
Prošel jsem spoustu Nette tutorialů i část tohoto fóra, ale žádné hezké elegantní řešení se mi najít nepodařilo. Většinou je model jakýsi repozitář, či přímo nadstavba nad tabulkou, která definuje několik základních operací a data vrací obvykle jako ActiveRow. Nicméně pokud vyžaduji něco složitějšího, jako je popsáno výše, nevím, jak to do toho způsobu zapojit.
Tento problém mi nepřijde nijak zvlášť exotický, jak se toto v praxi řeší? Existuje nějaký univerzální elegantní řešení?
- hAssassin
- Člen | 293
ahoj, a nestačila by jedna speciální metoda
calculatePrice($userId, $productId)
např. v tom repozitáři
(třeba ProductRepository
)? Jako parametr by měla ID
přihlášenýho uživatele a ID produktu a tam by sis udělat co bys chtěl,
joinoval jak bys chtěl a měl bys to na jediném místě (dokonce by si to mohl
i cachovat).
Ono ostatně takto radit je složitý, protože jen těžko usuzovat jak máš implementovaný model. Pokud ale aspoň částečně podle quickstartu tak by to takto určitě šlo udělat.
Editoval hAssassin (25. 4. 2013 1:12)
- tobice
- Člen | 30
No je pravda, že přímo toto řešení mě nenapadlo a je skutečně jednoduché. Problém je, že bych potom musel do každé šablony dávat k dispozici model a uživatele a pokaždé volat tuto metodu. Nette ještě moc dobře neznám a asi by se to dalo usnadnit nějakým Helperem, ale i tak je to věc, na kterou by bylo potřeba pamatovat a to naprosto pokaždé.
Kdybych měl problém zobecnit, tak bych řekl, že potřebuji, aby
mi model vracel místo hloupých ActiveRows nějaké chytřejší
objekty. Tedy abych kdykoliv mohl
zavolat$product->getPrice()
a dostal aktuální platnou cenu. Či
ještě navíc $product->getPriceWithCurrency()
, která by
vrátila cenu i s uživatelsky preferovanou měnou. To je možná záležitost
zobrazení a šablony, nicméně cenu takto zase potřebuji použít úplně
všude, a proto by mi přišlo vhodné si formát zobrazování ceny nadefinovat
na jednom místě a pak všude jinde už se jen odkazovat.
Model zatím není implementovaný nijak. Zkoumal jsem různá řešení, testoval, ale reálný kód zatím není. Hledám, jestli existuje nějaký univerzální doporučovaný koncept, jsem připraven na cokoliv :-)
Každopádně v mém případě to asi nakonec stejně bude nutné implementovat už do dotazů do databáze, protože podle uživatelské ceny budu potřebovat řadit. Tady se mi líbí dibi::dataSource. Bych si mohl nadefinovat základní dotaz, který by mi vybral správnou cenu, a na ostatních místech aplikace bych už jen dotaz modifikoval. Škoda, že toto neumí přímo Nette Database, resp. aspoň co jsem koukal, tak Selection, které podobný lazy loading umožňuje, nelze vygenerovat na základě vlastního dotazu, je ho nutné postavit pomocí $connection->table() a tam už jsou zase dost omezené možnosti při stavění dotazu. Či se mýlím?
- Tabetha
- Člen | 140
U nas ne to riesene, tak, ze mam 1 model, ktory spracovava celu logiku
ohladom katalogu a cien. V nom je funkcia, ktora vrati produkt (entita
produktu, tj. Spracuje data z DB a nepouzijem ako vystup statement/activerow
ale moju entitu). Tento model je pristupny v celej aplikacii (definoval som ho
v BasePresenter), prave kvoli jeho napojeniu na vsetky casti. Do toho modelu si
odovzdam user a s tym pracuje cely model.
ohladom tych mien to je vyriesene Helperom, do ktoreho ( u nas zo session)
odovzdam info o mene a kurze a prepocitava sa to len n vystupe.
hadam je to zrozumitelne :)
- hAssassin
- Člen | 293
@tobice > ano injektovat by jsi to tam musel, ale repository budes mit jako sluzbu a vzdycky si ji muzes injektnout do BasePresenteru danyho modulu. Pripadne se poohlednout treba po YetORM doplnku, ktery (podle vseho, sam sem ho zatim videl jen z vlaku) umoznuje mit inteligentrni entity, ktere by ty zminovany metody mohly implementovat. Nebo se mrknout po necem jinym nebo implmentovat neco vlastniho (Nette samo o sobe model primo neresi).
- Šaman
- Člen | 2666
Zkus si najít článek „5 vrstev modelu“ – zjistíš že ty repository jsou jen malá část modelu. A to si o tom pětivrstvém modelu nemyslím, že by to byl celý model z MVC/MVP, ale vpodstatě jen ORM. Třeba u matematických aplikací bude mít model hodně čistě počítacích tříd, které nebudou mít vůbec žádnou vazbu na databázi.
Takže podle pětivrstvého modelu si máš vytvořit třídu v servisní vrstvě (fasádu), která dostane všechny potřebné repository a v této třídě budeš počítat cenu. Tím, že je to servisní vrstva si nemusíš lámat hlavu, prostě si udělej třídu, které konstruktorem předáš potřebné repository a používej ji jako službu pro výpočet ceny.
- tobice
- Člen | 30
@Šaman: Ten článek už jsem znal, nicméně tento návrh je pořád dost obecný. Řeší spoustu věcí kolem, ale přímo konkrétně, co já potřebuji, neříká. Nicméně se mi zdá, že principiálně je to podobné tomu, co zmiňoval Tabetha o něco výše, kdy, pokud to dobře chápu, si ActiveRow převádí na vlastní entitu, což je v terminologii onoho článku fasáda. U toho se mi akorát nezdá, že každý výsledek dotazu je nutné ručně projet a v nějaké vrstvě převést každý ActiveRow na fasádu. Ale tomu se asi člověk nevyhne, buď použije vrstvu, která to dělá za něj, nebo si ji napíše sám…
- hAssassin
- Člen | 293
@hAssassin: Díky za návrhy. Mě zajímalo, jak model řeší lidé používající Nette :)
To je prave problem, protoze kazdy pouziva neco jinyho. Jelikoz Nette samo
model primo neresi (coz je jedine dobre), je to ciste vec programatora, takze
nekdo si vystaci s NDb, nekdo sahne po cistym Dibi, nekdo po Doctrine, NotORM
nebo YetORM. A nebo si napise neco vlastniho. Dokonce i obyc PHP a jeho
mysql_*
metody se daji pouzit :-)
… pokud to dobře chápu, si ActiveRow převádí na vlastní entitu, což je v terminologii onoho článku fasáda …
Ne, bohuzel to dobre nechapes. Entita je entita, ktera reprezentuje nejaky objekt at uz s DB nebo z realnyho sveta (na to se nahlizet ruzne, ale zjednodusene to je obraz DB tabulky). Mapper se stara o nacitani a ukladani dat (a mazani) a pracuje primo s ulozistem (at uz s DB nebo file systemem). Obecne kdyz vymenis treba DB mapper za FS mapper nebo se rozhodnes data ukladat do Memcache, melo by stacit prepsat pouze mapper. Sama entita ale s mapperem nepracuje (pak by to bylo neco jako ActiveRecord – neplest s NDb, je to navrhovy vzor), ale s mapperem pracuje prave repozitar, ktery muze i vytvaret entity po nacteni dat z mapperu, nebo je ukladat (zde by mela byt nejaka prevodni logika/funkce, ktera mapuje nazvy sloupecku v DB na property entity – nemelo by to byt totiz 1:1).
No a nad tim vsim muze nebo nemusi byt facada, ktera to cely zastresuje. Striktne by totiz repositor nemel moc komunikovat s dalsimi repozitory, takze to je jedna cinnost o kterou se fasada muze starat. A dalsi je treba to ukladani, pokud je dost slozity, je lepsi ho presunout prave do fasady nez se s tim babrat v Presenteru (navic to pak muzes volat z vic mist/ruznych presenteru). No a konecne zde muzou byt i ty slozity pocetni metody. A pokud tu cenu potrebujes nejak pocitat z DB tak to jde, pouze jen propojis fasadu s mapperem pres nejakou metodu repositor, predas si do mappery IDcka uzivatele a produktu a uz si tahas a joinujes jak potrebujes (nepsal sem to uz nahodou?).
Tot asi vse. Nevim jestli sem ti vubec na neco odpovedel, asi ne. Ale co uz, mazat se mit o nechce :-P
- Šaman
- Člen | 2666
Obecně: zapomeň na databázi. V obecném modelu žádná neexistuje.
Stejně tak tabulky, nebo klíče.
Máš entity a jejich vazby – ERD (entitně relační diagram), kde
výjimečně relační neznamená relační databázi, ale vztahy mezi entitami.
Nad tímto diagramem buduješ model od repository výše..
Teoreticky až úplně nakonec návrhu modelu budeš řešit úložiště a příslušný mapper. To, že často entitě odpovídá jedna tabulka je sice hezké, ale není to pravidlo. Jedna entita může být ve více tabulkách (poměrně často), stejně jako v jedné tabulce může (výjimečně – porušuje to normálové formy) být více entit.
Cokoliv active* přenáší do entity vazbu na mapper, resp. úložiště – proto mám s NDb trochu problém. Entita ti obchází repository i mapper (v QS tyto dvě vrstvy splývají v jedinou třídu repository) a sama si přistupuje k databázi. Začne ti to vadit ve chvíli, kdy data nemáš v databázi.
A tím se dostáváme k tomu, proč neexistuje ultimátní ORM. Buď musí repository skrze mapper vytvořit entitu kompletní, tedy včetně návazných entit a to je neefektivní, nebo si entita sama umí nějak dotahat vazby až když jsou potřeba, ale tím musí mít entita vazbu na konkrétní repository a to není žádoucí.
Když necháme teorii teorií a pořešíme tvůj případ. Určitě
potřebuješ pravou entitu (to, co vrátí NDb není entita v pravém slova
smyslu), tu ti umožní třeba YetORM. Ta entita (třeba Product
)
bude obsahovat sloupečky které jsou v db a navíc property
$price
.
Tuto cenu spočítá repository buď pomocí oné třídy v servisní vrstvě,
anebo mu ji může dodat rovnou mapper (pokud NDb umožňuje definovat dotaz
s virtuálními sloupečky).
- tobice
- Člen | 30
@hAssasin: No tady došlo asi k drobnému zmatení pojmů :-) Jsem se odkazoval na příspěvek Tabethy, který „entitou“ nazývá něco jiného, přičemž to něco, se mi zdá, se nejvíce blíží fasádě.
Jinak děkuji za spoustu podnětného materiálu. YetORM vypadá velice zajímavě. Evidentní je, že řešení je spousta a je potřeba zvolit to správné vzhledem k problému.
- Tabetha
- Člen | 140
@tobice: ano, ano … vysledky z DB spracovávam a a to si potom vraciam ako svoje pole entít, pre všetky produkty alebo len danú entitu pre hladaný jeden produkt …napr. funkcia pre katalog :
array(
"products" => array(...), //tu sú ProductEntity -> entity produktov
"count" => $count,
);
ten count je vlastne súčet všetkých produktov z DB…