\

Školení Návrhové vzory, OOP a UML


 Sunday, August 07, 2005
Cachování řádků z databáze pro business objekty - třída DataCacheHelper

Při psaní business vrstvy libovolné aplikace jste jistě narazili na následující problém, který můžeme demonstrovat na profláknutém příkladu s objednávkami a jejich položkami. Pod položkou rozumíme instanci třídy, ve které jsou uloženy informace o vybraném produktu, počtu objednaných kusů produktu a celkové ceně. Objednávka a položka objednávky jsou třídy, které jste už určitě psali tolikrát, že nemáte rádi ani jejich názvy ;)

Každá objednávka má 0..n položek a vyžádáme-li si konkrétní položku z kolekce všech položek (Items) u objednávky, která již byla uložena do databáze, musí dojít nejprve k nahrání kolekce. Je jedno, zda položky nahráváte ihned privátní metodou nazvanou třeba loadItems volanou z konstruktoru třídy Objednávka, nebo zda (a lépe) používáte zpožděné nahrávání, kdy kolekce Items je naplněna stejnou metodou teprve po prvním přístupu a konstruktory všech business objektů pouze uloží předané unikátní identifikátory (Id) a naplní své vlastnosti uloženými daty (kromě kolekcí) až po prvním přístupu k nějaké vlastnosti (kombinace vzorů Lazy Load a Ghost). Na přístupu k plnění dat objektů nezáleží, jen vždy musíte garantovat, že uživatel objektu musí  přistupovat k datům objektu, aniž by si byl vědom provedených výkonnostních optimalizací - interní impementace je i zde nedotknutelné privatissimum objektu.

Jak zjistíte v metodě loadItems, jaké položky objednávka (třída Order obsahuje? Přes datovou vrstvu spustíte dotaz, která vrátí všechny záznamy s daty pro všechny položky objednávky - parametrem dotazu je samozřejmě Id objednávky. Kruciálním problémem je ale vytvoření položek objednávky (třída OrderItem) z vrácených záznamů. Jaké máme možnosti?

  1. Vytvoříme novou instanci OrderItem a předáme jí do konstruktoru její Id. Výhodou je, že třída Order není s třídou OrderItem nijak svázána a pouze jí předává její identifikátor. Velkou nevýhodou ale je to, že object OrderItem při obnovování svých dat z databáze bude znovu posílat dotaz do databáze "dej mi data pro mé Id", což nebude nijak efektivní přístup zvláště, když v objednávce budou desítky nebo stovky položek a každá z nich s opulentní rozhazovačností pošle svůj dotaz do databáze. Navíc je to zcela zbytečné, protože data již byla vyzvednuta v metodě loadItems, ale "jen" nebyly objektu OrderItem předána.
  2. V metodě loadItems přečteme záznamy položek objednávky, vyzvedneme data ze všech sloupců a pro každou položku vyvoláme konstruktor, který akceptuje všechny vlastnosti položky. Tedy kromě Id předáme i celkovou cenu, počet kusů atd. Sice jsme se již zbavili opakovaného dotazování do databáze položkami objednávky, ale objevily se další zásadní nevýhody. Třída objednávka je zatížena zbytečnou znalostí rozhraní (konstruktoru) třídy OrderItem a při změně třídy OrderItem, jakými jsou přidání nebo odebrání vlastnosti, budeme muset provést i změny ve třídě Order. Při tomto řešení jsme se právě vydali na strastiplnou a rozháranou životní cestu,  nazývanou moderními mudrci ze softwarových pousteven ve svých písemných testamentech "Maintenance Nightmare". Stejný problém budeme mít při plnění vlastností OrderItem, navíc bychom u každé vlastnosti museli mít i set přístupovou metodu, z čehož by se měl všem autokritickým vývojářům obracet žaludek.
  3. Třídě OrderItem přidáme konstruktor, který přijímá záznam z databáze - v případě .Net Frameworku tedy objekt DataRow. Tohle řešení sejme povinnost nést břemeno znalosti rozhraní třídy OrderItem z třídy Order, ale zanechá nás s třídami, jejichž veřejné (nebo minimálně "internal") rozhraní je zapráskáno objekty z nižších vrstev. Proč by třída měla ukazovat, že je závislá na objektech DataRow, jejichž hlavní doménou je databázová vrstva? Perzistence je u business objektů nutné zlo, ne alfou a omegou a hlavním důvodem jejich existence. Bohužel, i v mnoha složitých projektech jsou stále k vidění jen truchlivé krabičky s daty z databáze a metodami Load a Save doplněné o honosnou etiketu "business vrstva" od nějakého rekvalifikovaného outsidera s kriplovsky laděným elánem Horsta Fuchse, co se v pomatení smyslů rozhodl pomoci saturovat deficit pracovních sil na trhu s vývojáři.

Než přejdu ke cachování dat, jen drobná poznámka, abych předešel dotazům. Metoda loadItems může poslat dotaz, který vrátí jen id položek objednávky, takže se data na klienta netahají dvakrát a můj problém pak působí jen jako vykonstruovaná obsese. I v tomte případě se ale místo jednoho dotazu budete potýkat s desítkami dotazů, které vyzvedávají - většinou zcela zbytečně - data jen pro jednu instanci OrderItem. Jestliže mám k tomu příležitost, je lepší vyzvednou všechna data pro všechny objekty najednou jedním "úsporným" dotazem a přitom dovolit v případě potřeby objektu přímé obnovení dat z databáze svým separátním dotazem.

Když tedy není pro nás přijatelný konstruktor s objektem DataRow, musíme při vytváření objektu "OrderItem" sdělit, odkud může načíst svá data. Jinými slovy, objekt OrderItem musí vědět, kam mu třída Order "schovala" objekt DataRow. Proč bychom ale neefektivně každému objektu extra sdělovali, odkud může načíst svá data? Zavedeme raději jeden centrální objekt - cache na příchozí data z databáze. Pak stačí objektu OrderItem předávat jen Id jako v první variantě a přitom  využívat všech výhod cachování dat.

Co od takové datové cache požadovat?

  1. Třída musí být globálně viditelná pro všechny objekty. Půjde tedy o Singleton, respektive pro webové aplikace bude vhodné použít PseudoSingleton (Thread Specific Storage).
  2. Rozhraní, které se nemění s přidáváním dalších business tříd a které mohou používat jednotným způsobem všechny objekty. Takže chceme univerzální metody pro ukládání i vyzvedávání dat jakéhokoli objektu, ne nějaké stále bobtnající rozhraní s metodami StoreOrdeItem, StoreOrder, StoreXX, StoreXY, ... .

Zde je jednoduchá implementace třídy, která zatím splňuje naše nároky. Zobrazit kód

Myslím, že kód je sám o sobě dostatečně vypovídající, takže jen stručný komentář. Statický atribut Instance vrátí pro každý thread specifickou instanci třídy, která se pro jeden thread chová jako běžný Singleton. Metoda CloseInstance odstraní instanci třídy pro daný thread a tuto metodu je vhodné volat ve webové aplikaci na konci každého požadavku v obsluze události EndRequest v souboru global.asax. Metoda StoreData uschová předaný datový řádek (argument row) pro objekt daného typu (argument type) - metoda předpokládá, že řádek obsahuje sloupec Id, který je jednoznačným identifikátorem každého business objektu. Metoda GetData vrátí dříve uložený řádek pro typ v argumentu type a pro objekt, jehož id bylo předáno v argumentu businessObjectId. Po vrácení dat je uložený řádek odstraněn, aby nebyly vlastnosti business objektu permanentně obnovovány z dříve uložených a již neaktuálních dat.

Přístě si ukážeme, jak si u business objektů vynutit používání třídy DataCacheHelper bez duplikace kódu a rizika, že zapomenete v nově přidané třídě řádky z instance DataCacheHelper vyzvedávat, a také postupně z DataCacheHelperu vydělíme různé aspekty jeho chování, abychom umožnili rekonfiguraci DataCacheHelperu za běhu aplikace a adekvátně vyladili jeho činnost pro různé typy aplikací.



Sunday, August 07, 2005 7:26:34 PM (Central Europe Standard Time, UTC+01:00)       
Comments [11]  .NET Framework | Návrhové vzory


Sunday, August 07, 2005 11:03:10 PM (Central Europe Standard Time, UTC+01:00)
preco vam tak strasne vadi DataRow v rozhrani? take Int32.Parse(string) mi pride velmi podobne. moze to byt vec vkusu ale zisk zavedenim cachehelpra sa mi zda byt minimalny.
naraga
Monday, August 08, 2005 12:23:25 AM (Central Europe Standard Time, UTC+01:00)
(komentuji hlavne prilozeny zdrojak, ktery se v nekterych vecech lisi od textu clanku)

Rene,
jsem presvedcen o tom, ze prakticke vyuziti tohoto konceptu (takto cachovat) u aplikaci je nulove, ne-li skodlive.

Nejvice nevyhod vidim u web aplikaci, ale i u BL ci WinForm jsou uskali tohoto pristupu.

1) web aplikace:
Cachovani u web aplikaci je efektivni pouze tehdy, pokud jde o cache spolecnou pro vice uzivatelu (tzn. nevazanou na thread,) - plnohodnotny singelton pres AppDomain.
K tomu v 90% pripadu neni potreba psat vlastni tridy a infrastrukturu, ale je vhodne pouzit tridu System.Web.Caching.Cache. Ackoliv se to nezda, je velmi snadne pouzit tuto tridu i mimo web aplikace ( viz stary trik http://blog.vyvojar.cz/michal/archive/2003/11/23/308.aspx )

2) Cache vazana pouze na 1 thread u web aplikace prakticky existuje pouze od okamziku vzniku instance objektu (zjednodusene od pozadavku na web stranku) po dokonceni vykonavani requestu.
Jinak receno, udelam-li refresh stranky, musi dojit k znovunaplneni cache.
Troufam si rici,ze operace spojene s naplnenim cache budou shodne jako prace s objectem bez Cache, ci pravdepodobneji zvysi zatizeni systemu (pamet, sit, zatizeni SQL).

Samozrejme zalezi na konkretni implemetaci, ale obecne chci rici, ze cache platna a existujici po dobu vykonavani requestu bude efektivni ve VELMI SPECIALNICH pripadech, realny NIKDY.

3) U web aplikaci to nehrozi, nebo jen zcela vyjimecne, protoze doba vykonavani requestu je obvykle (ci spravne ma byt) v milisekundach,ale u winform to muze byt vazny problem :
Pokud je cache singletonem per thread, pak se snadno ve Winform aplikaci stane, ze budou existavat 2 nezavisle instance cache jedne objednavky, s rozdilnym obsahem.
Opravdu idealni situace pro hledani chyb....

4) ciste osobni pocit, mozna zkusenost.
Nevidim moc vyznam v cachovani DataRow objectu. Krome toho, ze pro ziskani obsahu sloupce se musi pretypovavat , tak me osobne prijde efektivnejsi cachovat jiz radne instance objectu.

5) neodvazil bych rici jako ty, ze Lazy Load je lepsi. Podle mne zalezi na konkretnim objektu, datech a situaci. Nekdy je to uplne jedno (u me tak 80% pripadu) a v 20% je jedna ci druha strategie nahravani dat (lazy load x nahrad ihned vse) vyhodnejsi...


To jen par pro mne nejkriklavejsich problemu u tohoto kodu, ktere vidim ted v 1 rano :) . Vzhledem k tomu ze brzo bude pokracovani a ty zakladni chyby nedelas :-), nejspise nektere z mych pripominek budes resit pozdeji a ja jen tradicne predbiham :-D



PS: okno na komentare bych zvetsil o 50+% jak do strany, tak smerem dolu :-)
Monday, August 08, 2005 7:46:02 AM (Central Europe Standard Time, UTC+01:00)
Ja pro tento problem pouzivam podedenou tridu z DataTable, ktera ma metodu, ktera danou tabulku prevede do strongly typed kolekce objektu pomoci reflection.
Vsechny tridy z perzistentni vrstvy vraci tohoto potomka. Pokud chci jen zdroj do datagridu, pouziju primo DataTable, pro nejake business procesy si ji prevedu do kolekce.
Monday, August 08, 2005 7:48:34 AM (Central Europe Standard Time, UTC+01:00)
To Michal: Diky za pripominky Michale, pokusim se na ne reagovat :)
Mozna te zmatlo a vyprovokovalo to, ze DataCacheHelper ma v sobe slovo cache a ze mluvim o cachovani dat - OK, co treba misto DataCacheHelperu Temporary Storage.

DataCacheHelper (Temporary Storage) ma vyznam v jednom pripade:
Takze
Varianta a) Objekt Objednavka nacte data pro 150 polozek objednavky, vytvori instance, kterym preda Id a KAZDA instance OrderItem (napriklad pri iterovani kolekce posle dotaz do db, aby se vratily jeji data). V njhorsim pripade jen zde jsme
a) Prenesli vsechny data z db 2x (jednou v tride Order a podruhe si je vyzvedla kazda instance) a provedli jsme 150 + 1 dotaz

b) Pouzijeme DataCacheHelper. Metoda loadItems v tride Order vsechny radky ulozi do DataCacheHelperu a odtud si je tridy OrderItem vyzvednou. Byl proden jeden dotaz do db, to je o 150 dotazu mene nez v predchozim pripade ;)

K pripominkam
Ad 1) a 2 Nejde o tradicni cachovani dat - tridu We.Cache nepotrebuji, zde je zbytecna - viz vyse. Ja nechci (a nesmim) cachovat data mezi jednotlivymi pozadavky (objednavky se meni!), DataCacheHelper je jen docasne uloziste pro jiz vyzvednuta data z databaze platna prave pro jeden request - to je zamer. Neco jineho je cachovani ciselniku - ty mohou byt uklozeny ve web.cache. Myslim, ze vykonnostne na tom budu lip (viz pocet SQL dotazu vyse).


Ad 3) Presne toho se tyka moje posledni veta "a také postupně z DataCacheHelperu vydělíme různé aspekty jeho chování, abychom umožnili rekonfiguraci DataCacheHelperu za běhu aplikace a adekvátně vyladili jeho činnost pro různé typy aplikací". Ano pro win aplikace bude zapnuto jine vychozi chovani, navic rolisene dle typy objektu - ale to predbiham

Ad 4) Snad je i tohle jz vyse napsaneho jasnejsi, ale znovu. DataCacheHelper nerika, ze nemame cachovat objekty - samozrejme, ze pouzijeme ve spojeni s nim IdentityMap, ve ktere budou vsechny zijici instance. DataCacheHelper JEN zrostredkuje vazbu mezi objednavkou a polozkou objednavky pri ziskavani dat.
Ve svem projektu pouzovam DataCacheHelper, IdentityMap + bazovou tridu pro business objekty, ktera "inteligentne" zapouzdruje praci s ttmito tridami, takze odvozene business tridy uz o techto pocnych tridach nemusi takrka nic znat.

Ad 5) Zalezi na typu aplikace, samozrejme, ale zavedeni Lazy Load je tak nenarocne, ze se vyplati vsude. Pri nahravani sloziteho grafu objektu je ale Lazy Load neocenitelny a jak ukazu nejen z vykonnostnoho hlediska, ale take kvuli cyklickym vazbam mezi objekty.
Priklad z realneho projektu - aplikace nasazena, zakaznik ji pouziva ypusobem, ktery jsme necekali a ktery vede k tomu, ze na jednu instanci jsou "nalepeny" stovky agregovanych objektu s dalsimi agregovanymi objekty. Po dodatecnem zavedeni Lazy Load se vykon aplikace zlepsil o 75%.
Monday, August 08, 2005 7:54:35 AM (Central Europe Standard Time, UTC+01:00)
To Naraga) int.parse(string), protoze typy int a string jsou soucasti domeny zakladu.
DataRow patri do vrstvy pro psristup k datum, business objekty do business vrstvy.
A druhy duvod, nejen "esteticky", ktery jsem nezminil v clanku.
Konstruktor prijimajici DataRow muzete pouzit jen pri vytvareni objektu, DataCacheHelper i pri pozadavaku na opakovane nahrani objektu (reload)
Monday, August 08, 2005 7:57:04 AM (Central Europe Standard Time, UTC+01:00)
To Nikola: To je dobry napad - jen ta reflection pri stovkach nebo tisicovkach polozek neni zrovna vykonny nastroj:(
Jinak aby nedoslo k mylce - v UI pro zobrazovani dat samozrejme pouzivam DataTabe, DataSet, DataTable -> nema smysl znasilnovat business vrstvu pro prezentaci dat.

Monday, August 08, 2005 12:24:25 PM (Central Europe Standard Time, UTC+01:00)
to ze je DataRow v namespace System.Data z neho este nerobi "mimozemstana". ide predsa o pomerne abstraktny zdroj a pristup k rekonstrukcii stavu je obmedzeny (internal). nakoniec vnutorne ste na tejto triede aj tak zavysli. ad refresh: mozte mat internu metodu Load(datarow), ktora dobre posluzi aj pre refresh.
tesim sa na pokracovanie. zatial temporarystorage projektu velmi neublizil a mozno konecne pochopim jeho vyznam :)
naraga
Monday, August 08, 2005 1:06:26 PM (Central Europe Standard Time, UTC+01:00)
ad vykonnost:
Rene, ja nesouhlasim s vykonnosti.

Ty do DataCacheHelperu nactes vice radku objednavek, nez ve skutecnosti pote pouzijes.

Do extremu prevedeno do DataCacheHelper nactes 1000 radek dat, ale pak si z nej vezmes jen 2 radky.

Ano, v pripade vypisovani seznamu objednavek se ti to - >>mozna<< - vyplati, pokud vypises alespon 50 objednavek (tzn. vyuzijes 5% nectenych dat).

Do zateze musis pocitat nejen jednotlive dotazy na SQL, ale i mnozstvi zobrazenych a prenesenych dat.

A pokud toto budes delat pro kazdy request, pak pro kazdy request zbytecne nacitas a prenasis 1000 radek, namisto jednotek radek (anebo malych desitek).

Zalezi na konecne aplikaci, ale uz pri mnozstvi tisicu a desetitisicu objednavek zacnes mit problem se zatezi SQL a budes muset optimalizovat jinym zpusobem (napr. specialne plnena kolekce objednavek apod).

Tuesday, August 09, 2005 7:43:48 AM (Central Europe Standard Time, UTC+01:00)
To Michal: Tady s tebou souhlasim Michale (a myslim, ze jsme se dokonce o tom pred rokem bavili take na mem skoleni)
Ja neresim na 20 radcich kodu vsechny optimalizece, a je mi jasne ze se v aplikaci navic optimalizuje pristup ke kolekcim atd. - ale to neni tematem clanku,

Ten proklad se vsemi OrderItem jsem uvadel zamerne, protoze se v projektech vyskytuje casto.
Jak uvidis dale - tento kod business objektu (presneji receji to bude zakodovano na urovni predka") rika: "Pred tim nez zacnes nahravat data z databaze, podivej se do docasneho wellknown uloziste, zda jiz nebyla pro tebe pred chvili pripravena a dotaz do db je tedy zbytecny"

To Naraga: DataRow neni mimozemstan, to urcite ne ;) a samozrejme, ze je objekt na DataRow zavisly - kdyz ale muzu pred svetem skryt fakt, ze pouzivam DataRow, proc bych to neudelal? Cim mene zverejnim, tim vetsi mam prostor pro zmeny v dalsich verzich. Hlavni duvody uvidite v pokracovani spotu.
Metodu samozrejme pridat muzete, ale uz mate konstruktor i metodu pro takrka stejny ucel (vim, ze v kosntruktoru budete volat inkriminovanou metodu, ale presto bych to takto nedelal)
Tuesday, August 09, 2005 7:55:36 AM (Central Europe Standard Time, UTC+01:00)
Rene,
mozna trochu odbocuji, ale proc nepouzit nejaky O/R mapovaci nastroj, ktery transparentne zaridi persistenci business objektu (vcetne asociaci, kompozici atd.). Takovy nastroj muze resit i lazy loading a cachovani. V Jave k plne spokojenosti pouzivame Hibernate (www.hibernate.org). Jeji mladsi .NET sestrickou, zrejme mnohem mene vyspelou, je NHibernate (www.nhibernate.org).
Tuesday, August 09, 2005 8:06:52 AM (Central Europe Standard Time, UTC+01:00)
To Vojtech: Samozrejme, OR mapper je mozne pouzit - tohle je reseni, kdyz pisete vlastni perzistenci.

Zkousel jsem ruzne OR mappery a zatim mi zadny nevyhovoval stoprocentne - u vetsiny jsou problemy s vykonnosti, nebo si neporadi se vsemi vztahy. Hibernate je spicka, ja vim;). Ja zase cekam na vylepsene Object Spaces;)
Comments are closed.