versione italiana versione italiana
english version english version

ADO.NET Data Access (Parte 2)

Pur rappresentando un grande passo avanti nelle tecnologie di accesso ai dati, ADO.NET non mette certo al riparo da tutti i possibili errori o da pratiche scorrette nelle quali uno sviluppatore può incorrere. Il buon senso quindi è ancora la regola numero uno da applicare nel momento in cui si analizza e progetta una soluzione di accesso ai dati.

 

Best Practices

 

Non è mai male quindi ripetere alcuni concetti e linee guida che possono essere presi come riferimento durante questa fase:

 

1)     Aprire la connessione il più tardi possibile e chiuderla appena eseguito il comando

 

Le connessioni verso il database server sono un bene primario, ed occorre gestirlo con “parsimonia”. Le tecnologie come il Connection Pooling ci aiutano a realizzare soluzioni dove il mapping 1:1 tra utenti e connessioni non è più valido e il riutilizzo delle risorse è massimizzato.

 

2)     Eseguire comandi che ritornano la necessaria quantità di informazioni

 

Query come “Select * From Clienti”  tipicamente mettono in crisi le risorse del database server (memoria, cpu), il traffico di rete e l’applicazione client. Occorre sempre cercare di limitare le colonne e le righe ritornate specificando la corretta select list e clausola di WHERE sulla base delle informazioni necessarie. È raro infatti che all’applicazione serva avere in locale 100000 record con 25 colonne ciascuno... J

Quando è possibile poi è sempre conveniente utilizzare strumenti quali le Stored Procedure, che sono ottimizzate per avere un migliore riutilizzo del query plan e un minor traffico di rete, oltre ovviamente a rendere più flessibile l’achitettura dell’applicazione. Anche sull’utilizzo delle Stored Procedure torneremo in modo più specifico in un prossimo articolo.

 

3)     Limitare i round-trip inutili

 

Le chiamate inutili tra client e server generano sempre una quantità di overhead rilevante che, soprattutto quando eseguiti su componenti lato-server, rischiano di far peggiorare le performances di una applicazione. In ADO.NET esistono molti strumenti per ottimizzare il tipo di comunicazione nel caso, ad esempio, servano più o meno informazioni sui metadati di un resultset oppure sulle caratteristiche dello schema del database sottostante. Grazie a questa flessibilità lo sviluppatore ha la possibilità di scegliere, questa volta in modo esplicito rispetto a quanto accadeva con ADO 2.x dove queste modalità erano spesso nascoste dietro nomi di proprietà o paramentri poco comprensibili.

 

4)     Ottimizzare il database

 

Nessuna tecnologia di accesso ai dati, per quanto ottimizzata e performante, può prescindere da una base dati ottimizzata e correttamente indicizzata. Infatti senza strumenti di ricerca quali gli indici, nessun database server può garantire delle performance accettabili su quantità di dati significative. Oltre agli indici, altre tecniche proprie dei vari database server possono aiutare: in SQL Server , ad esempio, sono tipicamente da evitare strumenti quali i cursori server-side per ottenere applicazioni scalabili, così come scegliere i corretti tipi di dato per memorizzare le informazioni può dare ovvi benefici sulle performance.

 

Apertura e gestione delle connessioni

 

Tralasciando per un momento l’utilizzo dei vari strumenti visuali presenti in Visual Studio.NET, il nostro codice di accesso ai dati avrà solitamente un pattern di questo tipo:

 

DIR:                   FILE:
// Instanzio un nuovo oggetto Connection – Xxx dipende dal provider
XxxConnection cnn = new XxxConnection(“connection string”);
try
{
            // Apertura della connessione
cnn.Open();
            // Eseguo le operazioni sul database...
}
catch(Exception e)
{
            // gestisco le eventuali eccezioni
}
finally
{
            // Chiusura della connessione – opzionale se chiamo Dispose
cnn.Close();
            // Rilascio le risorse utilizzate dalla connessione
cnn.Dispose();
}

 

Un’alternativa a questo blocco di codice potrebbe essere l’utilizzo, solo in C#, della parola chiave using. Quest’ultima garantisce che venga invocato il metodo Dispose() sull’oggetto indicato, e che quindi implicitamente venga anche chiusa la connessione, oltre che rilasciate le risorse da essa allocate. Il codice risultante quindi sarà:

 

DIR:                   FILE:
// Instanzio un nuovo oggetto Connection – Xxx dipende dal provider
using(XxxConnection cnn = new XxxConnection(“connection string”))
{
try
{
                  // Apertura della connessione
cnn.Open();
                   // Eseguo le operazioni sul database...
}
catch(Exception e)
{
                  // gestisco l’eccezione
}
}

 

Connection String e Pooling

 

La connessione verso la fonte dati in realtà non sara interrotta ma verrà resa disponibile in un pool, gestito dal Data Provider utilizzato. Nel caso del SQL Server .NET Data Provider, il connection pooling è basato sui servizi offerti da COM+ ed è completamente controllabile attraverso la stringa di connessione con i parametri:

 

Pooling (default true)

Abilita o disabilita il pooling delle connessioni

Min Pool Size (0)

Numero minimo di connessioni nel pool

Max Pool Size (100)

Numero massimo di connessioni nel pool

Connection Lifetime (0)

Tempo di vita massimo di una connessione

Connection Reset (true)

Reset del contesto della connessione quando viene estratta dal pool (con SQL 7.0 ‘false’ evita round-trip con il db server)

Enlist (true)

La connessione viene automaticamente associata ad una transazione se il contesto del thread lo richiede

 

Una connessione può essere riciclata in un pool se le sue catatteristiche (nome server, nome database, contesto di sicurezza, contesto di una transazione) è identico alle altre connessioni in quel pool, altrimenti viene creato un nuovo pool con quelle caratteristiche. Per quanto riguarda l’OleDb .NET Data Provider invece, vengono utilizzati i servizi nativi di OleDb che richiedono che il provider utilizzato supporti questa funzionalità. I provider evoluti come quello per SQL Server e Oracle lo supportano automaticamente, mentre per altri occorre verificarne le caratteristiche e può essere necessario gestirne da codice le funzionalità.

Un’altra caratteristica della stringa di connessione è il parametro ‘Persist Security Info’ che specifica se le informazioni riguardanti la sicurezza (tipicamente User Name e Password) devono essere visibili attraverso le proprietà dell’oggetto Connection oppure no.

 

Dove mettere la Connection String?

 

Una delle domande più frequenti quando si progetta la parte di accesso ai dati per una applicazione è proprio questa. Le alternative solitamente sono diverse, e con il .NET Framework aumentano:

 

-         File XML di configurazione dell’applicazione

-         File .UDL

-         Registry

-         ConstructorString in COM+

-         Global.aspx in ASP.NET

 

Ogniuna di queste soluzioni ha vantaggi e svantaggi, e può essere la più indicata a seconda che si tratti di una applicazione client, di un componente server o un Web Service piuttosto che una applicazione ASP.NET anche se, il registry sembra ormai una soluzione obsoleta e poco portabile in altre piattaforme. Il file XML di configurazione dell’applicazione sembra la soluzione indicata da Microsoft dato che il .NET Framework mette a disposizione una serie di classi per leggere e utilizzare queste informazioni applicativamente, mentre dal punto di vista dell’amministratore del sistema è presente un wizard, il .NET Framework Configuration Tool (Mscorcfg.msc), per evitare l’editing diretto dei file.

 

Esecuzione di un comando

 

Dopo aver aperto la connessione, normalmente si definisce il comando da inviare alla fonte dati. I comandi possono variare per tipo e per valori e/o parametri restituiti dopo l’esecuzione. Il costruttore dell’oggetto Command accetta il testo del comando stesso e un riferimento alla connessione che si vuole utilizzare per l’esecuzione. La proprietà CommandType invece determina il tipo di comando che si vuole definire, e può assumere uno dei valori presenti nell’enum associato:

 

StoredProcedure

Il testo del comando è il nome della SP. Viene utilizzata la sintassi “EXEC @ret=nome_sp @par1,@par2....”

TableDirect

Il testo del comando è il nome della tabella. La query diventa “SELECT * FROM table_name”.

Text

Il testo del comando è la query da inviare alla fonte dati.

 

Oltre al tipo di specificato, è possibile differenziare i comandi a seconda delle informazioni che esso ritorna. Queste possono essere:

 

-         uno o più resultset

-         un unico valore scalare

-         un valore di ritorno

-         uno o più parametri di output

 

e in più il comando può eventualmente accettare uno o più parametri di ingresso.

Attraverso l’oggetto Command, nei vari .NET DataProvider, è possibile gestire tutte queste varianti e utilizzare alcuni metodi che sono ottimizzati per ciascuna situazione. Ecco un esempio di esecuzione di un comando di tipo testo:

 

DIR:                   FILE:
// Instanzio un nuovo oggetto Connection
using(OleDbConnection cnn = new OleDbConnection(“connection string”))
{
try
{
     // Creo l’oggetto Command
OleDbCommand cmd = new OleDbCommand("Select CompanyName From Customers
Where ID=?", cnn);
     // Apertura della connessione
cnn.Open();
     // Definisco il tipo di comando
cmd.CommandType = CommandType.Text;
     // Creo il parametro di Input
cmd.Parameters.Add(new OleDbParameter("@Id",OleDbType.VarWChar,255));
cmd.Parameters["@Id"].Direction = ParameterDirection.Input;
cmd.Parameters["@Id"].Value = "ALFKI";
     // Eseguo il comando
string sName = cmd.ExecuteScalar();
}
catch(Exception e)
{
     // gestisco l’eccezione
}
}

 

La sequenza delle operazioni non si discosta da quello che veniva fatto in ADO 2.x:

-         apertura della connessione

-         creazione del comando di tipo indicato

-         definizione dei parametri

-         esecuzione del comando

-         recupero dei risultati

-         gestione di eventuali errori

 

ma quello che cambia decisamente è il modo con il quale si accede ai risultati.

A seconda del tipo di risultati ritornati dal comando occorre chiamare i corretti metodi e utilizzare strumenti adeguati. Il metodo ExecuteReader() consente l’esecuzione di un comando che ritorna uno o più resultset permettendo all’applicazione di accedere a quelle informazioni utilizzando un oggetto di tipo DataReader, in modalità read-only e forward-only scorrendo dal primo all’ultimo record. Se il comando invece dovesse ritornare un resultset composto da un unica riga e un’unica colonna (un valore scalare, appunto), in quel caso il metodo ExecuteScalar() è ottimizzato per recuperare quell’unica informazione nel modo più performante possibile, scartando tutti gli eventuali altri record o colonne presenti nel resultset. Nel caso la query non dovesse ritornare nessun resultset, allora il metodo ExecuteNonQuery() risulterebbe la scelta migliore, tenendo presente che questo metodo è ancora in grado di recuperare eventuali parametri di output di una S.P. nel caso questi esistano e siano l’unico valore recuperato. Solo per il SQL Server .NET Data Provider, e in congiunzione con la clausola FOR XML nella sintassi del comando, è possibile utilizzare anche il metodo ExecuteXmlReader() che ritorna un oggetto di tipo XmlReader, permettendo così una navigazione gerarchica delle informazioni e un utilizzo orientato allo scambio di informazioni in formato XML.

Ecco alcuni esempi di utilizzo nelle varie condizioni:

 

 

DIR:                   FILE:
// Instanzio un nuovo oggetto Connection
using(SqlConnection cnn = new SqlConnection(“connection string”))
{
try
{
     // Creo l’oggetto Command
SqlCommand cmd = new SqlCommand("Select * From Customers", cnn);
     // Apertura della connessione
cnn.Open();
     // Definisco il tipo di comando
cmd.CommandType = CommandType.Text;
     // Eseguo il comando e ottengo un DataReader
SqlDataReader dr = cmd.ExecuteReader();
     // mi posiziono sul primo record e inizio a scorrere
While(dr.Read())
{
            // Accedo ai valori attraverso il nome colonna
Console.WriteLine(dr[“CustomerId”]);
            // utilizzo il “typed accessor” ottimizzato
Console.WriteLine(dr.GetString(1));
}
}
catch(Exception e)
{
     // gestisco l’eccezione
}
}

 

In questo caso viene restituito un resultset attraverso l’oggetto DataReader che può essere analizzato scorrendolo dal primo all’ultimo in modo read-only. Se il testo del comando fosse stato composto da un batch di comandi nella forma “Select * From Customers;Select * From Orders” il DataReader avrebbe consentito l’accesso al secondo resultset attraverso il metodo NextResult().

 

DIR:                   FILE:
// Instanzio un nuovo oggetto Connection
using(SqlConnection cnn = new SqlConnection(“connection string”))
{
try
{
     // Creo l’oggetto Command
SqlCommand cmd = new SqlCommand("spGetWithParams", cnn);
     // Apertura della connessione
cnn.Open();
     // Definisco il tipo di comando
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Id",SqlDbType.NVarChar,255));
cmd.Parameters["@Id"].Direction = ParameterDirection.Input;
cmd.Parameters["@Id"].Value = "ALFKI";
cmd.Parameters.Add(new SqlParameter("@Name",SqlDbType.NVarChar,255));
cmd.Parameters["@Name"].Direction = ParameterDirection.Output;
     // Eseguo il comando
cmd.ExecuteNonQuery();
     // Accedo ai valori attraverso il parametro di output
Console.WriteLine(cmd.Parameters["@Name"].Value);
}
catch(Exception e)
{
     // gestisco l’eccezione
}
}

 

È possibile ovviamente combinare, come in questo esempio, le varie situazioni ed avere un comando che ritorna sia un DataReader contenente il resultset che parametri di input, output e valore di ritorno della Stored Procedure eseguita. In questo caso occorre avere l’avvertenza di chiudere il DataReader per ottenere i valori dei parametri, perchè questi giungono al client solo alla fine dello stream di byte che il server invia al client.

Un’altro tip importante riguarda l’utilizzo dei parametri. Occorre ricordare che:

 

-         OleDbCommand supporta l’utilizzo del ? come placeholder per i parametri presenti nel comando, e quindi occorre dichiare gli oggetti OleDbParam in modo strettamente posizionale.

-         SqlCommand NON consente l’utilizzo del ? ma invece richiede una sintassi del tipo “...WHERE CustomerID=@NomeParam” e, durante la dichiarazione dei parametri, non importa la posizione ma il nome del parametro stesso, che deve essere uguale a quello usato nel comando T-SQL / Stored Procedure.

 

Il DataReader è un componente indicato per tutte quelle situazioni dove vengono richieste caratteristiche di prestazioni e scalabilità nell’accesso ai dati in modalità read-only, ma occorre tenere a mente alcune particolarità:

 

-         Occorre ricordarsi di chiudere il DataReader una volta utilizzato

-         un solo record alla volta è presente in un dato istante “in memoria”

-         la connessione DEVE rimanere aperta fino a che il DataReader è utilizzato

-         un solo DataReader alla volta può essere utilizzato per ogni Connection

-         il DataReader consente l’accesso a campi di tipo BLOB

-         esistono una serie di metodi chiamati “typed accessor” (GetString(), GetInt(), ecc.) che consentono un accesso più performante ai dati nelle colonne del DataReader nel caso si conosca in ancicipo la struttura dello stesso

-         il parametro CommandBehavior.CloseConnection passato all’esecuzione di ExecuteReader() chiude automaticamente la connessione alla chiusura del DataReader

-         chiamando il metodo Cancel() dell’oggetto Command è possibile interrompere lo stream di record dal server verso il client nel caso si siano già estratte le informazioni richieste dal DataReader. Utilizzando Close() del DataReader invece i dati verrebbero comunque tutti estratti dal server prima di chiuderlo.

 

DIR:                   FILE:
// Instanzio un nuovo oggetto Connection
using(SqlConnection cnn = new SqlConnection(“connection string”))
{
try
{
     // Creo l’oggetto Command
SqlCommand cmd = new SqlCommand("Select * From Customers FOR XML AUTO", cnn);
     // Apertura della connessione
cnn.Open();
     // Definisco il tipo di comando
cmd.CommandType = CommandType.Text;
     // Eseguo il comando e ottengo un XmlReader
XmlReader xr = cmd.ExecuteXmlReader();
     // mi posiziono sul primo nodo e inizio a scorrere
While(dr.Read())
{
            // Accedo ai valori attraverso I vari attributi
Console.WriteLine(MessageBox.Show(r.GetAttribute(2).ToString());
}
}
catch(Exception e)
{
     // gestisco l’eccezione
}
}

 

L’oggetto XmlReader invece può essere un sostituto “leggero” del DataSet in quelle situazioni dove è sufficiente estrarre le informazioni da SQL Server e ottenerne la rappresentazione in forma gerarchica, elaborandole poi con gli altri oggetti presenti nella Base Class Library per manipolare informazioni in formato XML. In questo modo si possono evitare trasformazioni dalla forma relazionale (DataSet) a quella gerarchica (XML) estraendo così le informazioni direttamente dalla fonte dati.

Utilizzando l’XmlReader occorre ricordare che:

-         è da utilizzare solo insieme alla clausola FOR XML

-         ExecuteXmlReader() non supporta CommandBehavior.CloseConnection e la connessione va chiusa esplicitamente