Le pagine Web dinamiche sono fantastiche; puoi adattare la pagina risultante al tuo utente, mostrare l'attività di altri utenti, offrire prodotti diversi ai tuoi clienti in base alla cronologia di navigazione e così via. Ma più dinamico è un sito Web, più query di database sarà probabilmente necessario eseguire. Sfortunatamente, queste query del database consumano la maggior parte del tempo di esecuzione.
In questo tutorial, dimostrerò un modo per migliorare le prestazioni, senza eseguire query aggiuntive non necessarie. Svilupperemo un sistema di cache di query per il nostro livello dati con costi di programmazione e implementazione ridotti.
Aggiungere un livello di cache in modo trasparente a un'applicazione è spesso difficile a causa del design interno. Con linguaggi object oriented (come PHP 5) è molto più semplice, ma può ancora essere complicato da un design scadente.
In questo tutorial, impostiamo il nostro punto di partenza in un'applicazione che esegue tutto il suo accesso al database attraverso una classe centralizzata da cui tutti i modelli di dati ereditano i metodi di accesso al database di base. Lo scheletro per questa classe iniziale ha questo aspetto:
class model_Model protected static $ DB = null; funzione __construct () funzione protetta doStatement ($ query) funzione protetta quoteString (valore $)
Implementiamolo passo dopo passo. Innanzitutto, il costruttore che utilizzerà la libreria PDO per interfacciarsi con il database:
function __construct () // si connette al DB se necessario se (is_null (self :: $ DB)) $ dsn = app_AppConfig :: getDSN (); $ db_user = app_AppConfig :: getDBUser (); $ db_pass = app_AppConfig :: getDBPassword (); self :: $ DB = new PDO ($ dsn, $ db_user, $ db_pass); self :: $ DB-> setAttribute (PDO :: ATTR_ERRMODE, PDO :: ERRMODE_EXCEPTION);
Ci colleghiamo al database usando la libreria PDO. Per le credenziali del database, utilizzo una classe statica denominata "app_AppConfig" che centralizza le informazioni di configurazione dell'applicazione.
Per memorizzare la connessione al database, utilizziamo un attributo statico ($ DB). Utilizziamo un attributo statico per condividere la stessa connessione con tutte le istanze di "model_Model" e, a causa di ciò, il codice di connessione è protetto con un if (non vogliamo collegarci più di una volta).
Nell'ultima riga del costruttore impostiamo il modello di errore di eccezione per PDO. In questo modello, per ogni errore rilevato dal PDO, viene generata un'eccezione (classe PDOException) anziché restituire valori di errore. Questa è una questione di gusti, ma il resto del codice può essere tenuto più pulito con il modello eccezionale, che è buono per questo tutorial.
L'esecuzione di query può essere molto complessa, ma in questa classe abbiamo adottato un approccio semplice con un singolo metodo doStatement ():
funzione protetta doStatement ($ query) $ st = self :: $ DB-> query ($ query); if ($ st-> columnCount ()> 0) return $ st-> fetchAll (PDO :: FETCH_ASSOC); else return array ();
Questo metodo esegue la query e restituisce un array associativo con l'intero set di risultati (se presente). Si noti che stiamo usando la connessione statica (self :: $ DB). Si noti, inoltre, che questo metodo è protetto. Questo perché non vogliamo che l'utente esegua query arbitrarie. Invece di fornire modelli concreti all'utente. Lo vedremo più tardi, ma prima implementiamo l'ultimo metodo:
funzione protetta quoteString ($ valore) return self :: $ DB-> quote ($ value, PDO :: PARAM_STR);
La classe "model_Model" è una classe molto semplice ma comoda per la stratificazione dei dati. Anche se è semplice (può essere migliorato con funzionalità avanzate come le istruzioni preparate, se lo si desidera), fa le cose di base per noi.
Per completare la parte di configurazione della nostra applicazione, scriviamo la classe statica "app_Config":
class app_AppConfig funzione pubblica statica getDSN () return "mysql: host = localhost; dbname = test"; public public function getDbUser () return "test"; public public function getDbPassword () return "MyTest";
Come affermato in precedenza, forniremo modelli concreti per accedere al database. Come un piccolo esempio, useremo questo semplice schema: una tabella di documenti e un indice invertito per cercare se un documento contiene una parola data o no:
CREATE i documenti TABLE (id chiave primaria intera, proprietario varchar (40) non null, posizione_server varchar (250) non null); CREATE le parole TABLE (word char (30), doc_id intero non nullo riferimenti documenti (id), PRIMARY KEY (word, doc_id))
Dalla classe di accesso ai dati di base (model_Model), deriviamo tutte le classi necessarie per la progettazione dei dati della nostra applicazione. In questo esempio, possiamo ricavare queste due classi autoesplicative:
class model_Index estende model_Model public function getWord ($ word) return $ this-> doStatement ("SELECT doc_id FROM words WHERE word =". $ this-> quoteString ($ word)); class model_Documents estende model_Model public function get ($ id) return $ this-> doStatement ("SELECT * FROM documents WHEREcoche"); var_dump ($ parole);
Il risultato per questo esempio potrebbe essere simile a quello (ovviamente dipende dai dati effettivi):
array (119) [0] => array (1) ["doc_id"] => string (4) "4630" [1] => array (1) ["doc_id"] => stringa (4 ) "4635" [2] => array (1) ["doc_id"] => string (4) "4873" [3] => array (1) ["doc_id"] => stringa (4 ) "4922" [4] => array (1) ["doc_id"] => string (4) "5373" ...
Quello che abbiamo scritto è mostrato nel seguente diagramma di classe UML:
Quando le cose iniziano a collassare nel server del database, è tempo di fare una pausa e prendere in considerazione l'ottimizzazione del livello dati. Dopo aver ottimizzato le tue query, aggiunto gli indici appropriati, ecc., La seconda mossa è cercare di evitare query non necessarie: perché effettuare la stessa richiesta al database su ogni richiesta utente, se questi dati cambiano difficilmente?
Con un'organizzazione di classe ben pianificata e disaccoppiata, possiamo aggiungere un ulteriore livello alla nostra applicazione quasi senza costi di programmazione. In questo caso, estenderemo la classe "model_Model" per aggiungere cache trasparente al nostro livello di database.
Poiché sappiamo che abbiamo bisogno di un sistema di memorizzazione nella cache, concentriamoci su quel particolare problema e, una volta risolto, lo integreremo nel nostro modello di dati. Per ora, non penseremo in termini di query SQL. È facile astrarre un po 'e costruire uno schema abbastanza generale.
Lo schema di caching più semplice consiste di coppie [chiave, dati], in cui la chiave identifica i dati effettivi che vogliamo archiviare. Questo schema non è nuovo, anzi, è analogo agli array associativi di PHP, e lo usiamo sempre.
Quindi avremo bisogno di un modo per memorizzare una coppia, leggerla e cancellarla. Questo è sufficiente per costruire la nostra interfaccia per gli helper della cache:
interfaccia cache_CacheHelper function get ($ key); funzione put ($ key, $ data); funzione delete ($ key);
L'interfaccia è abbastanza semplice: il metodo get ottiene un valore, data la sua chiave di identificazione, il metodo put imposta (o aggiorna) il valore per una determinata chiave, e il metodo delete lo cancella.
Con questa interfaccia in mente, è il momento di implementare il nostro primo vero modulo di caching. Ma prima di farlo, sceglieremo il metodo di archiviazione dei dati.
La decisione di creare un'interfaccia comune (come cache_CacheHelper) per gli helper della cache ci permetterà di implementarli quasi su ogni storage. Ma in cima a quale sistema di archiviazione? Ce ne sono molti che possiamo usare: memoria condivisa, file, server memcached o anche database SQLite.
Spesso sottovalutati, i file DBM sono perfetti per il nostro sistema di memorizzazione nella cache e verranno utilizzati in questo tutorial.
I file DBM funzionano in modo ingenuo sulle coppie (chiave, dati) e lo fanno molto velocemente grazie alla sua organizzazione B-tree interna. Esse fanno anche il controllo degli accessi per noi: non dobbiamo preoccuparci di bloccare la cache prima di scrivere (come dovremo fare su altri sistemi di storage); DBM lo fa per noi.
I file DBM non sono guidati da server costosi, fanno il loro lavoro all'interno di una libreria leggera sul lato client che accede localmente al file effettivo che memorizza i dati. In realtà sono una famiglia di formati di file, tutti con la stessa API di base per l'accesso (chiave, dati). Alcuni di essi consentono chiavi ripetute, altri sono costanti e non consentono la scrittura dopo aver chiuso il file per la prima volta (cdb), ecc.. Puoi leggere ulteriori informazioni al riguardo su http://www.php.net/manual/en/dba.requirements.php
Quasi ogni sistema UNIX installa un tipo o più di queste librerie (probabilmente Berkeley DB o GNU dbm). Per questo esempio, useremo il formato "db4" (formato Sleepycat DB4: http://www.sleepycat.com). Ho scoperto che questa libreria è spesso preinstallata, ma puoi usare qualsiasi libreria tu voglia (eccetto cdb, ovviamente: vogliamo scrivere sul file). In effetti puoi spostare questa decisione nella classe "app_AppConfig" e adattarla per ogni progetto che fai.
Con PHP, abbiamo due alternative per gestire i file DBM: l'estensione "dba" (http://php.net/manual/en/book.dba.php) o il modulo "PEAR :: DBA" (http: / /pear.php.net/package/DBA). Useremo l'estensione "dba", che probabilmente hai già installato nel tuo sistema.
I file DBM funzionano con stringhe per chiavi e valori, ma il nostro problema è archiviare i set di risultati SQL (che possono variare molto nella struttura). Come potremmo riuscire a convertirli da un mondo all'altro?
Bene, per le chiavi, è molto facile perché la stringa di query SQL identifica molto bene un insieme di dati. Possiamo utilizzare il digest MD5 della stringa di query per abbreviare la chiave. Per i valori, è più complicato, ma qui i tuoi alleati sono le funzioni PHP serialize () / unserialize (), che possono essere utilizzate per convertire da array a stringa e viceversa.
Vedremo come funziona tutto questo nella prossima sezione.
Nel nostro primo esempio, tratteremo il modo più semplice per eseguire il caching: il caching dei valori statici. Scriveremo una classe chiamata "cache_DBM" che implementa l'interfaccia "cache_CacheHelper", proprio così:
class cache_DBM implementa cache_CacheHelper protected $ dbm = null; function __construct ($ cache_file = null) $ this-> dbm = dba_popen ($ cache_file, "c", "db4"); if (! $ this-> dbm) lancia una nuova eccezione ("$ cache_file: Can not open cache file"); function get ($ key) $ data = dba_fetch ($ key, $ this-> dbm); if ($ data! == false) return $ data; return null; function put ($ key, $ data) if (! dba_replace ($ key, $ data, $ this-> dbm)) lancia nuova Exception ("$ key: Could not store"); funzione delete ($ key) if (! dba_delete ($ key, $ this-> dbm)) lancia nuova Exception ("$ key: Could not delete");
Questa classe è molto semplice: una mappatura tra la nostra interfaccia e le funzioni di dba. Nel costruttore, il file specificato viene aperto,
e il gestore restituito viene archiviato nell'oggetto per poterlo utilizzare negli altri metodi.
Un semplice esempio di utilizzo:
$ cache = new cache_DBM ("/tmp/my_first_cache.dbm"); $ cache-> put ("key1", "il mio primo valore"); echo $ cache-> get ("key1"); $ Cache-> delete ( "key1"); $ data = $ cache-> get ("key1"); if (is_null ($ data)) echo "\ nChiusura corretta!";
Di seguito, troverai ciò che abbiamo fatto qui espresso come diagramma di classe UML:
Ora aggiungiamo il sistema di memorizzazione nella cache al nostro modello di dati. Avremmo potuto cambiare la classe "model_Model" per aggiungere il caching a ciascuna delle sue classi derivate. Ma, se lo avessimo fatto, avremmo perso la flessibilità di assegnare la caratteristica di memorizzazione nella cache solo a modelli specifici, e penso che questa sia una parte importante del nostro lavoro.
Creeremo quindi un'altra classe, chiamata "model_StaticCache", che estenderà "model_Model" e aggiungerà funzionalità di caching. Iniziamo con lo scheletro:
class model_StaticCache estende model_Model protected static $ cache = array (); protected $ model_name = null; function __construct () funzione protetta doStatement ($ query)
Nel costruttore, chiamiamo prima il costruttore genitore per connettersi al database. Quindi, creiamo e archiviamo, staticamente, un oggetto "cache_DBM" (se non creato prima altrove). Archiviamo un'istanza per ogni nome di classe derivato perché utilizziamo un file DBM per ognuno di essi. A tale scopo, usiamo l'array statico "$ cache".
function __construct () parent :: __ construct (); $ this-> model_name = get_class ($ this); if (! isset (self :: $ cache [$ this-> model_name])) $ cache_dir = app_AppConfig :: getCacheDir (); self :: $ cache [$ this-> model_name] = new cache_DBM ($ cache_dir. $ this-> model_name);
Per determinare in quale directory dobbiamo scrivere i file della cache, abbiamo usato di nuovo la classe di configurazione dell'applicazione: "app_AppConfig".
E ora: il metodo doStatement (). La logica per questo metodo è: convertire l'istruzione SQL in una chiave valida, cercare la chiave nella cache, se trovato restituisce il valore. Se non trovato, eseguirlo nel database, memorizzare il risultato e restituirlo:
funzione protetta doStatement ($ query) $ chiave = md5 ($ query); $ data = self :: $ cache [$ this-> model_name] -> get ($ key); if (! is_null ($ data)) return unserialize ($ data); $ data = parent :: doStatement ($ query); self :: $ cache [$ this-> nome_modello] -> put (chiave $, serializzare ($ dati)); restituire $ dati;
Ci sono altre due cose che vale la pena notare. Innanzitutto, stiamo utilizzando l'MD5 della query come chiave. In effetti, non è necessario, perché la libreria DBM sottostante accetta chiavi di dimensione arbitraria, ma sembra comunque preferibile accorciare la chiave. Se si utilizzano istruzioni preparate, ricordare di concatenare i valori effettivi alla stringa di query per creare la chiave!
Una volta creato "model_StaticCache", la modifica di un modello concreto per il suo utilizzo è banale, è sufficiente modificare la clausola "extends" nella dichiarazione della classe:
class model_Documents estende model_StaticCache
E questo è tutto, la magia è fatta! Il "model_Document" eseguirà solo una query per ogni documento da recuperare. Ma possiamo farlo meglio.
Nel nostro primo approccio, una volta che una query è memorizzata nella cache, rimane valida per sempre fino a quando non si verificano due cose: cancelliamo la sua chiave esplicitamente, o scolleghiamo il file DBM.
Tuttavia questo approccio è valido solo per alcuni modelli di dati della nostra applicazione: i dati statici (come le opzioni di menu e questo genere di cose). È probabile che i dati normali nella nostra applicazione siano più dinamici di così.
Pensa a un tavolo contenente i prodotti che vendiamo nella nostra pagina web. Non è probabile che cambi ogni minuto, ma c'è la possibilità che questi dati cambino (aggiungendo nuovi prodotti, cambiando i prezzi di vendita, ecc.). Abbiamo bisogno di un modo per implementare il caching, ma abbiamo un modo per reagire ai cambiamenti nei dati.
Un approccio a questo problema è impostare un tempo di scadenza per i dati memorizzati nella cache. Quando memorizziamo nuovi dati nella cache, impostiamo una finestra di tempo in cui questi dati saranno validi. Dopo quel tempo, i dati verranno letti nuovamente dal database e archiviati nella cache per un altro periodo di tempo.
Come prima, possiamo creare un'altra classe derivata da "model_Model" con questa funzionalità. Questa volta, chiameremo "model_ExpiringCache". Lo scheletro è simile a "model_StaticCache":
class model_ExpiringCache estende model_Model protected static $ cache = array (); protected $ model_name = null; protected $ expiration = 0; function __construct () funzione protetta doStatement ($ query)
In questa classe abbiamo introdotto un nuovo attributo: $ expiration. Questo memorizzerà la finestra temporale configurata per i dati validi. Impostiamo questo valore nel costruttore, il resto del costruttore è lo stesso di "model_StaticCache":
function __construct () parent :: __ construct (); $ this-> model_name = get_class ($ this); if (! isset (self :: $ cache [$ this-> model_name])) $ cache_dir = app_AppConfig :: getCacheDir (); self :: $ cache [$ this-> model_name] = new cache_DBM ($ cache_dir. $ this-> model_name); $ this-> expiration = 3600; // 1 ora
La maggior parte del lavoro arriva nel doStatement. I file DBM non hanno un modo interno per controllare la scadenza dei dati, quindi dobbiamo implementarli. Lo faremo memorizzando gli array, come questo:
array ("time" => 1250443188, "data" => (i dati effettivi))
Questo tipo di array è ciò che serializziamo e archiviamo nella cache. La chiave "tempo" è l'ora di modifica dei dati nella cache e i "dati" sono i dati effettivi che vogliamo memorizzare. Al momento della lettura, se rileviamo che la chiave esiste, confrontiamo il tempo di creazione memorizzato con l'ora corrente e restituiamo i dati se non sono scaduti.
funzione protetta doStatement ($ query) $ chiave = md5 ($ query); $ now = time (); $ data = self :: $ cache [$ this-> model_name] -> get ($ key); if (! is_null ($ data)) $ data = unserialize ($ data); if ($ data ['time'] + $ this-> expiration> $ now) return $ data ['data'];
Se la chiave non esiste o è scaduta, continuiamo a eseguire la query e memorizziamo il nuovo set di risultati nella cache prima di restituirla.
$ data = parent :: doStatement ($ query); self :: $ cache [$ this-> model_name] -> put ($ key, serialize (array ("data" => $ data, "time" => $ now))); restituire $ dati;
Semplice!
Ora convertiamo "model_Index" in un modello con cache in scadenza. Come accade, con "model_Documents", abbiamo solo bisogno di modificare la dichiarazione di classe e modificare la clausola "extends":
class model_Documents estende model_ExpiringCache
Circa il tempo di scadenza ... alcune considerazioni devono essere fatte. Usiamo un tempo di scadenza costante (1 ora = 3.600 secondi), per semplicità, e perché non vogliamo modificare il resto del nostro codice. Ma possiamo facilmente modificarlo in molti modi per permetterci di utilizzare tempi di scadenza diversi, uno per ogni modello. In seguito vedremo come.
Il diagramma di classe per tutto il nostro lavoro è il seguente:
In ogni progetto, sono sicuro che avrai diversi tempi di scadenza per quasi tutti i modelli: da un paio di minuti ad alcune ore o addirittura giorni.
Se solo potessimo avere un diverso tempo di scadenza per ogni modello, sarebbe perfetto ... ma, aspetta! Possiamo farlo facilmente!
L'approccio più diretto è quello di aggiungere un argomento al costruttore, quindi il nuovo costruttore per "model_ExpiringCache" sarà questo:
function __construct ($ expiration = 3600) parent :: __ construct (); $ this-> expiration = $ expiration; ...
Quindi, se vogliamo un modello con un tempo di scadenza di 1 giorno (1 giorno = 24 ore = 1.440 minuti = 86.400 secondi), possiamo ottenerlo in questo modo:
class model_Index estende model_ExpiringCache function __construct () parent :: __ construct (86400); ...
E questo è tutto. Tuttavia, lo svantaggio è che dobbiamo modificare tutti i modelli di dati.
Un altro modo per farlo è delegare l'attività a "app_AppConfig":
class app_AppConfig ... public static function getExpirationTime ($ model_name) switch ($ model_name) case "model_Index": restituisce 86400; ... default: restituisce 3600;
E quindi aggiungi la chiamata a questo nuovo metodo sul costruttore "model_ExpiringCache", in questo modo:
function __construct () parent :: __ construct (); $ this-> model_name = get_class ($ this); $ this-> expiration = app_AppConfig :: getExpirationTime ($ this-> model_name); ...
Questo ultimo metodo ci consente di fare cose fantasiose, ad esempio utilizzare valori di scadenza diversi per gli ambienti di produzione o di sviluppo in modo più centralizzato. Ad ogni modo, puoi scegliere il tuo.
In UML, il progetto complessivo ha questo aspetto:
Ci sono alcune query che non possono essere memorizzate nella cache. Le più evidenti stanno modificando query come INSERT, DELETE o UPDATE. Queste query devono arrivare al server del database.
Ma anche con le query SELECT, ci sono alcune circostanze in cui un sistema di caching può creare problemi. Dai un'occhiata a una query come questa:
SELECT * FROM banners WHERE zone = "home" ORDER BY rand () LIMIT 10
Questa query seleziona in modo casuale 10 banner per la zona "home" del nostro sito web. Questo ha lo scopo di generare movimento nei banner mostrati nella nostra casa, ma se memorizziamo questa query in cache, l'utente non vedrà alcun movimento fino a quando i dati in cache non scadrà.
La funzione rand () non è deterministica (come non lo è ora () o altri); quindi restituirà un valore diverso ad ogni esecuzione. Se lo memorizziamo nella cache, congeleremo solo uno di questi risultati per tutto il periodo di memorizzazione nella cache e quindi rompere la funzionalità.
Ma con un semplice ri-factoring, possiamo ottenere i vantaggi del caching e mostrare la pseudo-casualità:
class model_Banners estende model_ExpiringCache public function getRandom ($ zone) $ random_number = rand (1,50); $ banners = $ this-> doStatement ("SELECT * FROM banners WHERE zone =". $ this-> quoteString ($ zone). "AND $ random_number = $ random_number ORDER BY rand () LIMIT 10"); restituire $ banner; ...
Quello che stiamo facendo qui è mettere in cache cinquanta diverse configurazioni di banner casuali e selezionarle casualmente. I 50 SELEZIONATI assomiglieranno a questo:
SELECT * FROM banners WHERE zone = "home" AND 1 = 1 ORDINE BY rand () LIMIT 10 SELECT * FROM banners WHERE zone = "home" AND 2 = 2 ORDER BY rand () LIMIT 10 ... SELECT * FROM banners WHERE zone = "casa" E 50 = 50 ORDINE BY Rand () LIMIT 10
Abbiamo aggiunto una condizione costante alla selezione, che non ha costi per il server di database ma esegue il rendering di 50 chiavi diverse per il sistema di memorizzazione nella cache. Un utente dovrà caricare la pagina cinquanta volte per vedere tutte le diverse configurazioni del banner; quindi l'effetto dinamico è raggiunto. Il costo è di cinquanta query al database per recuperare la cache.
Quali benefici possiamo aspettarci dal nostro nuovo sistema di memorizzazione nella cache?
In primo luogo, si deve dire che, a basse prestazioni, a volte la nostra nuova implementazione sarà più lenta delle query del database, specialmente con query molto semplici e ottimizzate. Ma per quelle query con join, la nostra cache DBM sarà più veloce.
Tuttavia, il problema che abbiamo risolto non è una prestazione grezza. Non avrai mai un server di database di riserva per i tuoi test in produzione. Probabilmente avrai un server con carichi di lavoro elevati. In questa situazione, anche la query più veloce può essere eseguita lentamente, ma con il nostro schema di caching non stiamo nemmeno utilizzando il server e, di fatto, stiamo riducendo il carico di lavoro. Quindi il reale aumento delle prestazioni si presenterà sotto forma di più petizioni al secondo servite.
In un sito web che sto attualmente sviluppando, ho fatto un semplice punto di riferimento per comprendere i vantaggi del caching. Il server è modesto: esegue Ubuntu 8.10 in esecuzione su un AMD Athlon 64 X2 5600+, con 2 GB di RAM e un vecchio disco fisso PATA. Il sistema esegue Apahce e MySQL 5.0, che viene fornito con la distribuzione Ubuntu senza alcuna regolazione.
Il test doveva eseguire il programma di benchmark di Apache (ab) con 1, 5 e 10 client simultanei caricando una pagina 1.000 volte dal mio sito web di sviluppo. La pagina effettiva era un dettaglio del prodotto che ha non meno di 20 query: contenuto del menu, dettagli del prodotto, prodotti consigliati, banner, ecc.
I risultati senza cache erano 4,35 p / s per 1 client, 8,25 per 5 client e 8,29 per 10 client. Con il caching (scadenza diversa), i risultati erano 25.55 p / s con 1 cliente, 49.01 per 5 clienti e 48.74 per 10 client.
Ti ho mostrato un modo semplice per inserire la cache nel tuo modello di dati. Naturalmente, ci sono una miriade di alternative, ma questa è solo una scelta che hai.
Abbiamo usato file DBM locali per archiviare i dati, ma ci sono alternative ancora più veloci che potresti considerare di esplorare. Alcune idee per il futuro: l'utilizzo delle funzioni apc_store () di APC come sistema di archiviazione sottostante, memoria condivisa per i dati veramente critici, utilizzo di memcached, ecc..
Spero che tu abbia apprezzato questo tutorial tanto quanto l'ho scritto io. Caching felice!