di Marco BellinasoUna delle novità più sbandierate sia di SQL Server 2005, ma soprattutto di ASP.NET 2.0 è la possibilità di fare caching con dipendenze direttamente ai dati contenuti nel DB. Ovvero: tengo i dati in cache finchè non cambiano sul DB, invece che per un tempo determinato. Questo permette di risolvere 2 problemi della cache attuale:
- Richiedere nuovamente i dati dal DB anche se in realtà non sono stati chiamati
- I dati vengono cambiati poco dopo che sono stati messi in cache...ci resteranno magari per altri tot minuti, fornendo quindi dati vecchi all'applicazione
Troppo bello per essere vero, no?
Beh, vediamo...Intanto c'è da dire che il meccanismo funziona in modo diverso con SQL Server 7/2000 e con SQL Server 2005. Nel primo caso l'abilitazione al caching di ASP.NET con dipendenza ai DB deve essere fatta a mano, tramite il tool aspnet_regsql da riga di comando. Lo si chiama 2 o più volte: una prima per abilitare il supporto a livello di DB, e poi una volta per ciascuna tabella. Quello che questo tool fa è di creare delle nuove tabelle di supporto, trigger e stored procedure che tengono traccia delle modifiche (insert/update/delete) fatte sulle tabelle tenute sott'occhio. ASP.NET poi implementa un meccanismo di polling che ogni tot millisendi (la frequenza è configurabile da web.config) verifica se i dati in cache sono ancora validi, controllando se la tabella di supporto indica o meno che i dati sono cambiati dall'ultima query (internamente una stored procedure controlla se il contatore per quella tabella è stato incrementato dall'ultima volta). Da notare che la dipendenza è a livello di intera tabella, ovvero se io salvo in cache un record di una tabella, e qualcuno cambia un altro record della stessa tabella, la cache viene comunque invalidata.
Per quando riguarda SQL Server 2005 le cose sono molto diverse. Il nuovo engine è in grado di creare una indexed view per ogni query eseguita (che a differenza di una vista normale è una copia fisica dei valori), per la quale si è specificato che si vuole creare una dipendenza. Quando i dati restituiti dalla query eseguita cambieranno (sempre in seguito ad una insert/update/delete, ma anche altri comandi) SQL Server manderà una notifica al client che si era in precedenza registrato, spedendogli un messaggio tramite un nuovo servizio chiamato Service Broker. Tutto questo nuovo meccanismo si chiama Query Notifications. E' molto interessante, perchè permette di venire notificati solo quando i risultati della nostra specifica query cambierebbero se la query venisse eseguita nuovamente, cosa che quindi non accadrebbe se qualcuno modifica un record che non era incluso nei risultati precedenti. Se volete maggiori dettagli su come funziona il tutto dietro le quinte, potete fare riferimento a quest'ottimo articolo di Bob Beauchemin. In .NET la classe che permette di ricevere tali notifiche è SqlDependency, tramite il suo evento OnChange. Una volta che si ha un comando SqlCommand, ecco come usarla:
SqlDependency dep = new SqlDependency(cmd);
dep.OnChange += new OnChangeEventHandler(dep_OnChange);
...
void dep_OnChange(object sender, SqlNotificationEventArgs e)
{
// ...
}
La classe SqlDependency è una classe di ADO.NET, e può quindi essere usata da qualsiasi applicazione. In ASP.NET poi c'è la nuova classe SqlCacheDependency, che internamente usa SqlDependency per sapere quando i risultati del commando ricevuto in input nel costruttore sono cambiati, e quindi invalidare l'elemento della Cache a cui fa riferimeno. Il tutto si traddurrebbe semplicemente come segue:
SqlCacheDependency dep = new SqlCacheDependency(cmd);
Cache.Insert("Categories", dsCategories, dep);
Fatta la dovuta introduzione, veniamo al punto del post...Per il sito che stò sviluppando al momento stò usando proprio SQL Server 2005, Express Edition. Ovviamente dopo aver letto articoli e articoli sulla cache, averne sentito parlare dappertutto ecc. ecc. mi sentivo anch'io parecchio eccitato a proposito, e non vedevo l'ora di usarla. Bene, ci provo e...non funziona! Il codice di test che ho isolato, usando direttamente SqlDependency in una applicazione WinForm, era il seguente:
private void Form1_Load(object sender, EventArgs e)
{
using (SqlConnection cn = new SqlConnection(@"Data Source=.\SQLEXPRESS;
AttachDbFilename=C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\northwnd.mdf;
Integrated Security=True;User Instance=True"))
{
SqlCommand cmd = new SqlCommand("SELECT * FROM Categories", cn);
cn.Open();
SqlDependency dep = new SqlDependency(cmd);
dep.OnChange += new OnChangeEventHandler(dep_OnChange);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
textBox1.Text += reader["CategoryName"] + Environment.NewLine;
}
}
void dep_OnChange(object sender, SqlNotificationEventArgs e)
{
MessageBox.Show("Changed!");
}
Accadeva che l'evento veniva sollevato subito dopo aver eseguito il comando...ma non perchè i dati fossero cambiati, per qualche altro motivo. Ho cercato in lungo e in largo, fino a quando il grande Alex Homer (quello che ha insegnato ASP2 a generazioni di sviluppatori web
e autore tra l'altro di questo articolo proprio sulla cache) ha risposto ad una mia mail di richiesta spiegazioni. Nel mio codice c'erano fondamentalmente due errori:
- Non è possibile usare * nelle query, ma bisogna elencare tutti i campi esplicitamente
- Il nome delle tabella deve essere completo, ovvero qualcosa tipo dbo.Categories
Nella stored procedure che usavo nel mio caso reale indicavo già tutti i campi esplicitamente (sembre una buona regola), ma in effetti non usavo il nome completo della tabella. Queste comunque sono solo 2 delle molte regole che bisogna rispettare se si vuole poter usufruire delle Query Notifications. Le altre sono indicate nei Books Online di SQL Server 2005...oppure più semplicemente le trovate elencate in questa pagina. Le limitazioni sono davvero tante e significative: niene funzioni di aggregazione come COUNT e SUM, niente funzioni di ranking e paginazione (come la nuova ROW_NUMBER()), niente uso di tabelle temporanee, niente DISTINCT, niente query che restituiscono colonne di tipo text, ntext, o image, e altro ancora...In aggiunta ho scoperto (altra ora di tentativi ovviamente...) che la mia stored procedure non funzionava anche per un altro motico: avevo impostato SET NOCOUNT ON...
Ora, considerando che uso pesantemente la paginazione custom per i vari controlli GridView ecc. di ASP.NET, che ho bisogno di COUNT(*) sempre per supportare la paginazione, che molte delle mie tabelle hanno colonne ntext...praticamente il risultato è che non posso usare SqlCacheDependency da nessuna parte!!! Mi devo accontentare ancora del "vecchio" caching temporizzato, implementando in aggiunta un metodo per fare manualmente il purge dei dati in cache dopo aver eseguito una Insert / Update o Delete tramite i miei oggetti business. Non ho dubbi che ci siano molti casi di utilizzo perfetti, ma molto probabilmente molti meno di quanto tanti cercano di far credere in molti articoli e conferenze. Ah, un'ultima nota (anch'essa poco pubblicizzata), nel caso vogliate usare questa feature. Le SQL Dependencies di SQL Server 2005 NON dovrebbero ASSOLUTAMENTE essere utilizzate in almeno 2 casi:
- Se la tabella in questione viene aggiornata molto spesso. Ad esempio, nella mia tabella Articles c'è una colonna ViewCount che indica quante volte l'articolo è stato letto, e che quindi viene aggiornata ad ogni lettura. Il risultato sarebbe che la cache verrebbe invalidata praticamente ad ogni richiesta di un articolo...non solo annullandone l'effetto positivo, ma peggiornado parecchio la situazione, a causa del lavoro aggiuntivo per salvare i dati in cache, gestire la coda dei messaggi di notifica (in entrata e uscita) e pulire la cache...tutto per niente.
- Per query dinamiche risultanti da tanti filtri, e magari anche dipendenti dall'utente corrente. Questo meccanismo infatti non è fatto per poter supportare centinaia o migliaia di query diverse sullo stesso DB. Va bene se lo usate per una stored procedure che magari prende in input qualche filtro, ma che viene comunque chiamata allo stesso modo da gran parte degli utenti.
Detto questo, buoni esperimenti e buon divertimento 
Acknowledgements: thank you Alex for your kind explanation, you made my day! And thank you Bob for the great article on MSDN.