Návrh modelu produktu, který načítá z databáze cenu dle vnějšího kontextu

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

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

@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
+
0
-

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
+
0
-

@Tabetha: Takže když vybereš z databáze seznam produktů, tak všechny ty řádky v modelu převedeš na danou entitu a vrátíš pole entit?

@hAssassin: Díky za návrhy. Mě zajímalo, jak model řeší lidé používající Nette :)

tobice
Člen | 30
+
0
-

@Š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
+
0
-

@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
+
0
-

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
+
0
-

@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
+
0
-

@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…