Autor Zpráva
Mikrouš
Profil
Zdravím,

řeším jistý "filozofický" problém s příliš velkým počtem dotazů na databázi. Děje se to zřejmě mým nesprávným návrhem tříd, chtěl bych tak požádat o názor, jak z toho ven. Napadlo mě několik více či méně kostrbatých řešení, ale nepodařilo se mi dohled nějaké ověřené a univerzálnější řešení, pokud vůbec existuje... Teď k věci.

Příklad. Mám třídu Uživatel, veřejnou statickou metodu načti, která má jako parametr identifikátor záznamu v DB. Metoda provede přes mysql_query vyhledání a vracení dat o uživateli a uloží je prostřednictvím konstruktoru do připravených privátních členských proměnných a vrátí instanci nově vytvořeného objektu. Tady není co řešit.

Mám pak třeba třídu Komentář, která pracuje totožně. A protože má komentář vždy nějakého autora, provede metoda načti kromě vytvoření objektu třídy Komentář ještě novou instanci třídy Uživatel (tu předá konstruktoru u Komentáře; nejdřív Uživatel pak Komentář), a funkce vrátí novou instanci Komentáře. Pokud mám komentářů deset, provede se dvacet dotazů.

Občas některé konstruktory vyžadují jako parametr více objektů. To se nám pak nakupí počet přístupů do databáze i na stovky, v administračním rozhraní, ve vyhledávání nebo i v obyčejném výpisu článků (či i komentářů) nevyjímaje. Prostě je s tím spojeno šílených problémů. A to ještě zmíním skutečnost, že kolikrát tentýž záznam (např. uživatele) načítám z databáze několikrát. Hrůza a děs.

=====

Napadlo mě několik ulehčovacích berliček. I za předpokladu, že bych jako parametry nedával objekty ale jen jejich identifikátory, v momentě vrácení jejich identifikátoru vytvářím jeho prostřednitvím nový objekt a další přístup do databáze je znovu na světě. Občas se stane, že některé údaje nepotřebuji, takže množství přístupů se tím sníží, ale neděje se tak často.

I za předpokladu, že bych využíval joiny jde pořád o to procházet dvě tabulky a promrhaný čas včetně zátěže i tak nakyne.

Když pominu ještě různé drobné úpravy v návrhu tříd, tak abych to zkrátil: mám v kapse poslední berli. Vytvořit si jakési skryté globální úložiště dat v paměti, které již byly z databáze načteny a při pokusu načíst údaje, které jsou již v paměti (a tedy existuje takový objekt), vrátit namísto toho již načtený objekt. Jsou s tím spojené i další problémy, jako nutnost udržovat aktuálnost dat (ta nebude nikdy stoprocentní) a rozhodovat se při složitějších a "výpisových" selectech. Je tu i riziko příliš velké alokace paměti.

Řešil někdo už tento problém? Jsou nějaká východiska? Opravdu mi připadne neobvyklé přistupovat do databáze víc než desetkrát na stránku. Jak je to s kešováním na straně MySQL (např.)? Už jsem se dostal do problému s vyšší návštěvností a vytíženost DB serveru je na maximu, to se kolikrát i třebas půl dne nepřipojím. Indexy a podobné "drobné" optimalizace už jsem dávno provedl...

Budu vděčný za vaše názory a jakoukoli radu.
DarkKnight
Profil
Smím vidět ty stránky? Abych to lépe pochopil...
Mikrouš
Profil
Používám tenhle svůj vzor na více místech a vždy je to pomalejší, když se pracuje s větším množství dat. Spíš bych to řešme na obecné úrovni. Navíc nechci na sebe dělat reklamu. ;-) Třeba ten můj návrhový vzor nebude jediný problém (a taky není, Dreamhost je sám o sobě shnilej, leč co je moc to je moc - jenže o tuto šílenou vytíženost tu v tuhle chvíli ani tak nejde), ale to už je můj problém. :-)

Co byste jinak potřeboval přesněji vysvětlit k lepšímu pochopení? Kdyžtak to nakreslím.
Joker
Profil
Mikrouš:
Mám třídu Uživatel, veřejnou statickou metodu načti, která má jako parametr identifikátor záznamu v DB.
Jak může být Načti statická metoda, když má naplnit instanci objektu daty? Ještě bych chápal, kdyby ta třída měla takový konstruktor (tj. můžu zkonstruovat třídu předáním databázového spojení, ze kterého se naplní).
V každém případě velkou nevýhodou takového návrhu je, že logika načítání dat je roztříštěná v těch jednotlivých třídách. Změna datového úložiště (třeba kdyby se mezi databázi a skript zavedla nějaká ta cache) pak znamená přepsat všechny třídy, jejichž instance potřeba nějak ukládat/načítat.
Navíc to porušuje single responsibility principle („Jedna třída se má starat o jednu věc“), kdy třída Uživatel řeší (nebo by měla řešit) funkčnost samotného toho uživatele a zároveň řeší práci s databází.

Doporučovaný přístup je mít pro načítání a ukládání dat speciální třídy, v angličtině zvané repository, přeložitelné jako „sklad“. Čili můžu mít „repository“ pro uživatele, kde se bude řešit načítání a ukládání uživatelů.
Načtení uživatele pak může vypadat jako $user = $userRepository->GetUser($id);, uložení jako $userRepository->saveUser($user);, načtení seznamu uživatelů třeba $userRepository->GetUserList($filter);
Čili když chci načíst nějakého uživatele, řeknu repository, že chci toho uživatele a zpátky dostanu jeho objekt.

Ještě flexibilnější je místo konkrétní třídy používat rozhraní (třeba IUserRepository), aby jako repository mohla sloužit jakákoliv třída, která to rozhraní implementuje.
Pak se o načítání a ukládání dat může starat i jedna velká třída třeba MysqlRepository, která bude implementovat ta rozhraní (IUserRepository, ICommentRepository atp.)

Ohledně těch mnoha přístupů do databáze bych tipoval, že asi načítáte seznamy objektů sekvenčně (stylem: Načti uživatele s ID 1, načti uživatele s ID 2, načti uživatele s ID 3) v situaci, kdy by bylo možné z databáze všechny získat jedním dotazem a pak je vrátit jako pole (stylem: SELECT deset uživatelů podle nějakých podmínek, projdi vrácené záznamy a vytvoř z nich pole příslušných objektů)
Mikrouš
Profil
Joker:
Jak může být Načti statická metoda, když má naplnit instanci objektu daty?
Ono jde o to, že konstruktor přebírá jako argumenty jednotlivé údaje nového objektu. Název "načti" jsem zvolil nevhodně, to uznávám. Má být něco jiného.

Ještě bych chápal, kdyby ta třída měla takový konstruktor (tj. můžu zkonstruovat třídu předáním databázového spojení, ze kterého se naplní).
V každém případě velkou nevýhodou takového návrhu je, že logika načítání dat je roztříštěná v těch jednotlivých třídách. Změna datového úložiště (třeba kdyby se mezi databázi a skript zavedla nějaká ta cache) pak znamená přepsat všechny třídy, jejichž instance potřeba nějak ukládat/načítat.
Navíc to porušuje single responsibility principle („Jedna třída se má starat o jednu věc“), kdy třída Uživatel řeší (nebo by měla řešit) funkčnost samotného toho uživatele a zároveň řeší práci s databází.

Na přístup do databáze mám Singleton (nazvěme ji třeba Databaze), ta mysql_query byla jen pro zjednodušení a urychlení. Ale je to jenom taková obalovačka nad starými mysql_* funkcemi. Ve své podstatě tedy řeším pouze tu cache (?).

Doporučovaný přístup je mít pro načítání a ukládání dat speciální třídy, v angličtině zvané repository, přeložitelné jako „sklad“. Čili můžu mít „repository“ pro uživatele, kde se bude řešit načítání a ukládání uživatelů.
Načtení uživatele pak může vypadat jako $user = $userRepository->GetUser($id);, uložení jako $userRepository->saveUser($user);, načtení seznamu uživatelů třeba $userRepository->GetUserList($filter);
Čili když chci načíst nějakého uživatele, řeknu repository, že chci toho uživatele a zpátky dostanu jeho objekt.
>
Ještě flexibilnější je místo konkrétní třídy používat rozhraní (třeba IUserRepository), aby jako repository mohla sloužit jakákoliv třída, která to rozhraní implementuje.
Pak se o načítání a ukládání dat může starat i jedna velká třída třeba MysqlRepository, která bude implementovat ta rozhraní (IUserRepository, ICommentRepository atp.)

Částečně chápu. Takže Databaze by byla po úpravě variací Repository nebo by bylo třeba ještě vytvořit mezistupeň? Předpokládám že ano, pokud by měl být jeho "hlavní" funkce kešování a další odstínění od konkrétního dolování dat, to vlastní dolování by bylo záležitostí třídy na "nižší" úrovni (např. ta obalovačka Databaze).

Jen mi není zcela jasné, jak udělat tu cache. Pokud bych měl například tisíc záznamů, tam naházený řetězce a podobně, a to vše načetl do paměti, rychle se obyčejné návštěvnosti dostanu na memory_limit.

Ohledně těch mnoha přístupů do databáze bych tipoval, že asi načítáte seznamy objektů sekvenčně (stylem: Načti uživatele s ID 1, načti uživatele s ID 2, načti uživatele s ID 3) v situaci, kdy by bylo možné z databáze všechny získat jedním dotazem a pak je vrátit jako pole (stylem: SELECT deset uživatelů podle nějakých podmínek, projdi vrácené záznamy a vytvoř z nich pole příslušných objektů)

Ano, občas se tak děje, v administraci je to denní chleba. Teď mě napadlo ještě jedno kostrbaté řešení (využít joiny), ale pořád je to záplata, která nenabízí komplexní východisko, musí se řešit každý případ individuálně.

====

No, pokusím se něco o tom repository vyhledat.
Mikrouš
Profil
Takže super, moc děkuju za nakopnutí!

Více informací pro ostatní např. zde (PHP Guru) nebo zde (Microsoft).
Joker
Profil
Mikrouš:
Takže Databaze by byla po úpravě variací Repository nebo by bylo třeba ještě vytvořit mezistupeň?
Samostatnou obálku nad databází bych asi nevytvářel, takové třídy už v PHP jsou (Mysqli a podobně). Podle mě ta repository klidně může přímo posílat dotazy do databáze (nebo třeba číst data ze souborů a podobně).
Pokud bych nechtěl v repository mít přímo napsané celé SQL dotazy, nabízejí se uložené procedury v databázi, to už mi přijde jako dostatečná úroveň abstrakce.

Jen mi není zcela jasné, jak udělat tu cache.
Tak v ideálním případě upravit návrh aplikace, tak, aby nebyla potřeba.
Popis problému je moc obecný aby se dalo konkrétně poradit, ale jsem si jistý, že by to šlo.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: