Statická proměnná v DI, opravdu zlo?
- Klobás
- Člen | 113
V tomto mimochodem velmi pěkném článku https://doc.nette.org/…global-state#… je napsáno, že
Neexistují žádné výjimky, žádné situace, kdy by globální stav byl užitečným kamarádem. Každé static $foo uvnitř kódu znamená problém. Aby váš kód byl prostředím respektujícím DI, je nezbytné úplně vymýtit globální stav a nahradit ho pomocí dependency injection.
Já třeba dost často používám statickou proměnnou kvůli zakešování SQL výsledku v metodě. Něco jako
<?php
public function getList()
{
static $cache;
if ($cache) {
return $cache
}
$cache = $this->table('xx')->where(..)->fetch()->toArray();
return $cache;
}
?>
a když volám funkci víckrát na 1 stránce v různých částech, tak se vykoná jen 1× dotaz.
Znamená to, že to je špatně a mám to tedy nahradit, čím a jak?
- Infanticide0
- Člen | 109
použij https://doc.nette.org/cs/caching#…
třeba nějak takhle:
private Cache $cache;
public function __construct(Storage $storage) {
$this->cache = new Cache($storage, 'xx');
}
public function getList()
{
$data = $cache->load('xx', function (&$deps) {
$deps[Cache::Expire] = "60 minutes"; // automatická expirace po 60 minutách (ve starší verzi je to ::EXPIRE)
return $this->table('xx')->where(..)->fetch()->toArray();
});
}
Editoval Infanticide0 (30. 3. 2023 21:43)
- Martk
- Člen | 661
Zkus spustit tento kód
class Foo {
public function __construct(
private int $value,
)
{
}
public function getList(): int
{
static $cache;
if ($cache) {
return $cache;
}
return $cache = $this->value;
}
}
$first = new Foo(1);
$second = new Foo(2);
var_dump($first->getList());
var_dump($second->getList());
Výsledek bude:
int(1)
int(1)
PS: Tohle asi není odpověď úplná odpověď na tvoji otázku, ale tady je jen příklad proč jsou static dobrý sluha, ale zlý pán.
Editoval Martk (30. 3. 2023 23:03)
- Klobás
- Člen | 113
Martk napsal(a):
Zkus spustit tento kód
class Foo { public function __construct( private int $value, ) { } public function getList(): int { static $cache; if ($cache) { return $cache; } return $cache = $this->value; } } $first = new Foo(1); $second = new Foo(2); var_dump($first->getList()); var_dump($second->getList());
Výsledek bude:
int(1) int(1)
PS: Tohle asi není odpověď úplná odpověď na tvoji otázku, ale tady je jen příklad proč jsou static dobrý sluha, ale zlý pán.
Ahoj, díky za reakci.
Tomuhle kódu samozřejmě rozumím, nicméně, je to zcela něco jiného než můj ne? Ten můj je klasická služba s 1 instancí. A kešuju tímto volání SQL dotazu do statické proměnné.
Kdybych potřeboval parametr tak si z toho udělám pole a kešuju to třeba dle ID nebo jiných parametrů.
Takže mi přijde, že zrovna mého případu se onen špatný návrh netýká, v tvém případě je jasné, že by to nefungovalo dobře.
Nebo je tedy jiná alternativa? Kešování je imho zbytečné, nepotřebuji kešovat ani na 1s jen zabránit vícenásobnému volání. Samozřejmě, jde to napsat i jinak, ale ptám se na konkrétni use-case.
- MajklNajt
- Člen | 502
a čo tak nešpekulovať a proste to napísať takto?
class Foo {
private ?array $cache = null;
public function getList(): array
{
if ($this->cache === null) {
return $this->cache;
}
return $this->cache = $this->table('xx')->where(..)->fetch()->toArray();
}
}
používam úplne bežne…
EDIT: opravené if ($this->cache)
– diky
@h4kuna
Editoval MajklNajt (31. 3. 2023 13:18)
- Klobás
- Člen | 113
MajklNajt napsal(a):
a čo tak nešpekulovať a proste to napísať takto?
class Foo { private ?array $cache = null; public function getList(): array { if ($this->cache) { return $this->cache; } return $this->cache = $this->table('xx')->where(..)->fetch()->toArray(); } }
používam úplne bežne…
Ahoj,
to není spekulace.
Tvůj způsob je možný také, ale ten bych použil na sdílení více dotazů nebo bych to ukládal do pole.
Viz. 1 a 2 https://diskuse.jakpsatweb.cz/?…
Jen se snažím pochopit viz ten začátek, proč je statická proměnná zlo :-), přijde mi totiž, že v tomto případe je to v pořádku.
Editoval Klobás (31. 3. 2023 9:40)
- Klobás
- Člen | 113
mystik napsal(a):
Problem je ze to spoleha na to ze vzdy a vsude bude prave jen ta jedna instance. Coz je mozna pravda ted, ale nemusi byt pravda az ten kod za rok nekdo upravi podle novych pozadavku.
A alternativa? Jinak snad každý kdo to vidí, musí chápat co to dělá ne?
Editoval Klobás (31. 3. 2023 10:24)
- Klobás
- Člen | 113
mystik napsal(a):
- Cachovat v instancni a ne staticke property.
- Injectnout si cache jako zavislost (pak se lip pracuje s invalidaci)
Ten kdo s tim kodem bude pracovat nemusi cist kod celyho projektu, aby tyhle chytaky objevil a vedel ze si na ne ma dat pozor.
- Do kódu se musí podívat tak jako tak ne (Je jedno jestli si to daný člověk zkontroluje v instanční property nebo přímo v té metodě jako statickou proměnnou)? Navíc je zřejmé, k čemu ta statická slouží. Když to bude property třídy, tak bude definovaná někde nahoře a není ani hned v té metodě. Když budu chtít kešovat víc SQL dotazů, tak budu mít víc properties nebo jednu sdílenou (obojí se mi nelíbí).
- Invalidovat netřeba, nejedná se o keš jako takovou s nějakým nastaveným TTL, tady jde přeci jen o to, aby se zamezilo vykonání ekvivalentního dotazu napříč jedním requestem.
Díky za názor.
- h4kuna
- Backer | 740
Pokud potřebuješ dočasnou keš, tak stále není potřeba statika, právě kvůli tomu co ti ukazoval @Martk.
- Mohl by jsi použít keš s MemoryStorage, která drží data jen po dobu requestu, pak to jde udělat pomocí DI.
- Můžeš si udělat property a ulož to do ní.
Pro tyhle případy jde udělat traita, kterou používám. Funguje jen při requestech, pokud to pustíš v procesu, nikdy se neinvaliduje, je to tak schválně.
- zvaš co píše @dakur
Editoval h4kuna (31. 3. 2023 12:18)
- Klobás
- Člen | 113
dakur napsal(a):
A proč tu funkci vůbec voláš se stejnými parametry víckrát na jedné stránce – k čemu to potřebuješ? Nedává smysl ten výsledek spíš předat nějak v presenteru nebo kde to používáš? Pak bys žádnou cache ani nepotřeboval.
Mám třeba následující model.
<?php
public function getList(): array
{
$this->getBase();
return $this->list;
}
public function getFullList(): array
{
$this->getBase();
return $this->fullList;
}
A metoda getBase je práve zakešovaná a ukládá do členských properties (list / fullList .. je jich víc) data a já na to mám pak gettery, a různě je volám .. přijde mi to OK.
?>
- Klobás
- Člen | 113
h4kuna napsal(a):
Pokud potřebuješ dočasnou keš, tak stále není potřeba statika, právě kvůli tomu co ti ukazoval @Martk.
- Mohl by jsi použít keš s MemoryStorage, která drží data jen po dobu requestu, pak to jde udělat pomocí DI.
- Můžeš si udělat property a ulož to do ní.
Pro tyhle případy jde udělat traita, kterou používám. Funguje jen při requestech, pokud to pustíš v procesu, nikdy se neinvaliduje, je to tak schválně.
- zvaš co píše @dakur
- Zbytečně složité proti jedné statické proměnné, kterou vím jak používat, nevidím v tom bohužel zlo jako vy všichni ostatní …
- To dělám viz předchozí ukázka.
- mystik
- Člen | 313
Kdyz pouzivas nejakou tridu nemelo by byt nutne si nejdriv prostudovat cely jeji kod.
Je spousta moznosti jak ti statika muze zpusobit problemy.
Napr. Co kdyz jian trida zmeni data ktera mas nacachovana v te staticke cachi? Najednou se system zkatujr chybna data a nevis proc dokud nanajdes nekde schovanou tuhle cache.
Je to sice jednodussi zapis ale za cenu toho ze je mnohem slozitejsi si udrzet prehled kde vsude se neco takoveho deje a dat si na to pozor.
Udrzovat takovy kod je pak mnohem slozitejsi.
- Klobás
- Člen | 113
mystik napsal(a):
Kdyz pouzivas nejakou tridu nemelo by byt nutne si nejdriv prostudovat cely jeji kod.
Je spousta moznosti jak ti statika muze zpusobit problemy.
Napr. Co kdyz jian trida zmeni data ktera mas nacachovana v te staticke cachi? Najednou se system zkatujr chybna data a nevis proc dokud nanajdes nekde schovanou tuhle cache.
Je to sice jednodussi zapis ale za cenu toho ze je mnohem slozitejsi si udrzet prehled kde vsude se neco takoveho deje a dat si na to pozor.
Udrzovat takovy kod je pak mnohem slozitejsi.
Oceňuji, že to řešíš do hloubky a snažíš se mi to vysvětlit.
Nicméně, to co tu padlo a to mít to v členské property, je to samé
v bleděmodrém …
Fungovat to bude stejně, pokud to budu stejně používat a stejně se do té
třídy budu muset podívat.
Nevidím v tom rozdíl. Argument, že kdybych chtěl jinou instanci nebo jiné parametry tak to shoří obojí (jak statická tak členská).
Ale jinak mi to asi stačí.
- m.brecher
- Generous Backer | 873
@Klobás
Jen se snažím pochopit viz ten začátek, proč je statická proměnná zlo :-), přijde mi totiž, že v tomto případe je to v pořádku.
Takhle teoreticky s vymýcením statických proměnných nelze než souhlasit. Máš ale pravdu, že je potřeba jednoduše vyřešit problém duplicity sql dotazů, protože to se stává dost často, že jedny data (typicky ActiveRow, ale i Selection) se v rámci jednoho presenteru využívají na více místech.
Jak průběžně vidím na fóru, řeší to lidi různými způsoby a žádné nejlepší řešení není nejlepší.
Ty Jsi to vyřešil cachováním sql dotazu rovnou v metodě modelu a to v rámci jednoho requestu, kde jak píšeš není žádná invalidace cache nutná. S tím já souhlasím, pokud je presenter jednoduchý tak problém nebude. Při dalším rozvoji presenteru ale k chybě dojít může díky tomu, že cachování je schované (popisuje @mystik).
Cachování můžeš zachovat, ale do property presenteru:
class ArticlePresenter extends BasePresenter
{
private Selection $articles;
public function __construct(
private ArticleModel $articleModel,
private ArticleMenuFactory $articleMenuFactory,
)
public function actionList()
{
$this->articles = $this->articleModel->getList();
... // první použití dat
}
public function renderList()
{
$this->template->articles = $this->articles; // druhé použití dat
}
public function createComponentArticleMenu(): ArticleMenu
{
$this->articleMenuFactory->create(articles: $this->articles); // třetí použití dat
}
}
Data se cachují, ale přehledným způsobem.
Bohužel nám toto nabourává jiný doporučovaný návrhový vzor a sice delegování práce presenteru do komponent, kdy factory dodá komponentě služby které potřebuje nezávisle na presenteru. Takhle jak jsem to napsal totiž data komponentě dodává presenter.
Bohužel pro často se vyskytující případy kdy více komponent využívá stejná data ideální řešení neexistuje:
a) data dodáme opakovaně z modelu – sql bude duplicitní
b) zacachujeme data v modelu – nepřehledné
c) data cachujeme v property presenteru – komponenta nebude nezávislá
I když nejde splnit všechny dobře míněné návrhové vzory současně, protože se vlastně navzájem vylučují, tak třeba řešení c) vůbec špatné není a pokud se komponenta přesune do traity tak skoro ideální.
- m.brecher
- Generous Backer | 873
@mystik
Za me idealni cachovat v modelu.
A používáš jakou databázovou knihovnu? Nette\Database\Explorer nebo nějaký ORM?
Mohu požádat o kousek kódu na ukázku jak takový cachovaný model vypadá? Ideálně ve variantě pro Nette\Database\Explorer, ale nevadí i ve verzi ORM.
- m.brecher
- Generous Backer | 873
@mystik
Mam vlastni „ORM“ nad Nette\Database\Explorer. Nejsem ted u PC, ale neni tam noc sloziteho.
Joo, to by mě zajímalo, kdyby Jsi si na mě náhodou někdy vzpomněl, něco takového bych si rád prohlédl, abych nemusel vynalézat kolo ;)
Obvykle resim jen sem tam pro veci jako ciselniky.
To znamená, že to cachuje dlouhodobě v přesahu přes několik requestů, třeba i řádově měsíce – do další změny v číselníku?
Btw v tomto vlákně se řeší duplicita sql v rámci jednoho requestu. Napadá mě, že když proběhnou dva/tři duplicitní sql dotazy v jednom presenteru bleskově za sebou v rámci jednoho připojení, tak databáze do hloubky neznám, ale tipoval bych, že toto bude nějakým způsobem v databázovém serveru cachováno a zátěž vzniklá duplicitními dotazy bude minimální. Takže v nízkozátěžové aplikaci by se duplicity sql možná nemusely řešit.
- Klobás
- Člen | 113
mystik napsal(a):
Clenska neshori protoze nova instance bude mit nezavislou cache. Ale nejlepsi je mit cache externe jako explicitni zavislost, aby jsi mohl resit i ty invalidace.
Ano :), ale pořád opakuji, že to je služba a ta novou instanci mít nebude. Je to až moc hypotetické a moc kdyby / kdyby nahodou / až.
- Klobás
- Člen | 113
m.brecher napsal(a):
@Klobás
Jen se snažím pochopit viz ten začátek, proč je statická proměnná zlo :-), přijde mi totiž, že v tomto případe je to v pořádku.
Takhle teoreticky s vymýcením statických proměnných nelze než souhlasit. Máš ale pravdu, že je potřeba jednoduše vyřešit problém duplicity sql dotazů, protože to se stává dost často, že jedny data (typicky ActiveRow, ale i Selection) se v rámci jednoho presenteru využívají na více místech.
Jak průběžně vidím na fóru, řeší to lidi různými způsoby a žádné nejlepší řešení není nejlepší.
Ty Jsi to vyřešil cachováním sql dotazu rovnou v metodě modelu a to v rámci jednoho requestu, kde jak píšeš není žádná invalidace cache nutná. S tím já souhlasím, pokud je presenter jednoduchý tak problém nebude. Při dalším rozvoji presenteru ale k chybě dojít může díky tomu, že cachování je schované (popisuje @mystik).
Cachování můžeš zachovat, ale do property presenteru:
class ArticlePresenter extends BasePresenter { private Selection $articles; public function __construct( private ArticleModel $articleModel, private ArticleMenuFactory $articleMenuFactory, ) public function actionList() { $this->articles = $this->articleModel->getList(); ... // první použití dat } public function renderList() { $this->template->articles = $this->articles; // druhé použití dat } public function createComponentArticleMenu(): ArticleMenu { $this->articleMenuFactory->create(articles: $this->articles); // třetí použití dat } }
Data se cachují, ale přehledným způsobem.
Bohužel nám toto nabourává jiný doporučovaný návrhový vzor a sice delegování práce presenteru do komponent, kdy factory dodá komponentě služby které potřebuje nezávisle na presenteru. Takhle jak jsem to napsal totiž data komponentě dodává presenter.
Bohužel pro často se vyskytující případy kdy více komponent využívá stejná data ideální řešení neexistuje:
a) data dodáme opakovaně z modelu – sql bude duplicitní
b) zacachujeme data v modelu – nepřehledné
c) data cachujeme v property presenteru – komponenta nebude nezávisláI když nejde splnit všechny dobře míněné návrhové vzory současně, protože se vlastně navzájem vylučují, tak třeba řešení c) vůbec špatné není a pokud se komponenta přesune do traity tak skoro ideální.
Asi záleží jak člověk potřebuje, ale kešování v presenteru jsem ještě nedělal nikdy. Ale i třeba vytváření komponent v Presenter lze → doporuji přečíst https://f3l1x.io/…te-sablonach
Díky všem, bylo to poučné i náročné.
Editoval Klobás (31. 3. 2023 14:50)
- m.brecher
- Generous Backer | 873
@Klobás
doporuji přečíst https://f3l1x.io/…te-sablonach
+ jak psal @mystik :
Injectnuta nette Cache a volani metod z Repository se v nekterych pripadech hodi do cache. Obvykle resim jen sem tam pro veci jako ciselniky.
V obou případech to tam sice není jasně napsané, ale dělá to na mě dojem, že autoři cachují na delší dobu nějaké složité dotazy – třeba dny/týdny, že se tam nemá na mysli cachování v rámci jednoho requestu. Ty dotazy mohou za sebou následovat v řádu milisekund. Má smysl na tu dobu to ukládat do cache úložiště a zase z něj číst? Řekl bych že smysl má to uložit do proměnné v RAM paměti – na tak krátkou dobu – tedy property presenteru nebo nějaká proměnná v modelu.
- Klobás
- Člen | 113
m.brecher napsal(a):
@Klobás
doporuji přečíst https://f3l1x.io/…te-sablonach
+ jak psal @mystik :
Injectnuta nette Cache a volani metod z Repository se v nekterych pripadech hodi do cache. Obvykle resim jen sem tam pro veci jako ciselniky.
V obou případech to tam sice není jasně napsané, ale dělá to na mě dojem, že autoři cachují na delší dobu nějaké složité dotazy – třeba dny/týdny, že se tam nemá na mysli cachování v rámci jednoho requestu. Ty dotazy mohou za sebou následovat v řádu milisekund. Má smysl na tu dobu to ukládat do cache úložiště a zase z něj číst ?? Řekl bych že smysl má to uložit do proměnné v RAM paměti – na tak krátkou dobu – tedy property presenteru nebo nějaká proměnná v modelu.
Ano. Víceméně se tu řeší jak kešovat a moc to nesouvisí s tím jak
přesně píšeš: cachování v rámci jednoho requestu.
Za mě je použití statické property přímo v metodě prostě OK.
- mystik
- Člen | 313
Nette ma i MemoryStorage to pouzivam na cache v ramci requestu.
Uvedu ti priklad, jak si snadno muzes neco rozbit takovouhle cache. V ramci nejake akce aktualizujes nejaky udaj, nactes data a zobrazis je. Vsechno ok data vidis aktualizovana. Pak ale nejaka nesouviseji zmena (treba komponenta co z zahlaci zobrazi nejaky udaj) sahne na ta data pred tim nez je aktualizujes. Coz naplni cache. A najednou porad vidis stara data i kdyz dotaz co je aktualizuje se provede. A hledas chybu. Pokud to je project co detailne neznas je to problem kterym muzes stravit par hodin.
A takovy kod je taky temer nemozne unit testovat.
- Klobás
- Člen | 113
mystik napsal(a):
Nette ma i MemoryStorage to pouzivam na cache v ramci requestu.
Uvedu ti priklad, jak si snadno muzes neco rozbit takovouhle cache. V ramci nejake akce aktualizujes nejaky udaj, nactes data a zobrazis je. Vsechno ok data vidis aktualizovana. Pak ale nejaka nesouviseji zmena (treba komponenta co z zahlaci zobrazi nejaky udaj) sahne na ta data pred tim nez je aktualizujes. Coz naplni cache. A najednou porad vidis stara data i kdyz dotaz co je aktualizuje se provede. A hledas chybu. Pokud to je project co detailne neznas je to problem kterym muzes stravit par hodin.
A takovy kod je taky temer nemozne unit testovat.
Můžu poprosit o ukázku jak to používáš? Stačí mi jen to zakešování dotazu.
- m.brecher
- Generous Backer | 873
@mystik
Nette ma i MemoryStorage to pouzivam na cache v ramci requestu.
vidím že mám znalostní mezery a mám co dohánět
Pokud bys ale chtel ozvi se mi na mail a poslu ti nejake ukazky jak s tim pracuju.
Díky, ale zatím to nehoří, asi bude stačit když počkám na veřejný opensource