PARTE 3 – CAPITOLO 6 – ADO.NET
Le applicazioni, che siano esse Windows o Web, hanno sempre più la necessità di collegarsi ad un Database per ricevere, modificare, inserire e/o cancellare dati.
A prescindere da quale sia il Data Store sottostante, si sentiva la necessità di un nuovo sistema di accesso ai dati che superasse per semplicità e funzionalità tutte le diverse soluzioni attualmente esistenti. È da queste esigenze che è nato ADO.NET, ovvero un insieme di classi che permettono a ASP.NET di connettersi con Data Source (Database) per eseguire comandi. La semplicità di ADO.NET sta nel fatto che vengono gestiti quasi allo stesso modo per qualsiasi scenario di connessione.
ADO.NET lavora su quattro concetti principali: Connection (per stabilire una connessione), Command (per eseguire comandi SQL), DataSet (fornisce un rapido accesso read-only e forward-only ai dati ottenuti con una query) e DataAdapter (per riempire un Dataset con informazioni prese da un Data Source e modificare un Data Source seguendo le modifiche di un DataSet).
Il Data Provider è l’oggetto che fornisce un set di classi ADO.NET che permettono di connettersi ad uno specifico tipo di database.
Per aprire la connessione con un database occorre specificare una stringa di connessione (ConnectionString). Date le molteplici possibilità per dichiarare una stringa di connessione si farà riferimento alla metodologia standard: “Data Source=(address);Initial Catalog=nome-tabella;Integrated Security=SSPI;”. Nel caso si volesse interagire con un database SQL Express basta inserire come address il nome della macchina seguito dallo statement “\SQLExpress”. Bisogna inoltre verificare, tramite gli opportuni pannelli di gestione, che i servizi dei relativi agent siano attivi sulla macchina.
Una stringa di connessione può essere ripetuta nel codice ogni volta che ce n’è la necessità oppure memorizzata in una risorsa esterna: il caso più frequente è quello di utilizzare il file Web.Config, assegnando un nome a tale ConnectionString che sarà richiamabile da codice a patto di includere “System.Web.Configuration” e di specificare il comando “< % $ ConnectionStrings:NomeDellaConnection % >”. La stringa va inserita all’interno della sezione “< connectionStrings >< add name=’nome’ connectionString=’stringa-di-connessione’ providerName=’System.Data.SqlClient (o il relativo provider)’ / >< /connectionStrings >”.
Le eccezioni che possono essere sollevate sono di due tipi: InvalidOperationException (connessione già aperta o parametri di connessione mancanti/errati) e SqlException (problemi specifici di accesso ad un database).
Quando si tratta di codice hard-coded le metodologie di connessione sono generalmente due: la prima è quella del blocco try (in cui inserire il comando connessione.Open())/catch/finally (in cui inserire il comando connessione.Close()) mentre la seconda è quella di includere tutta la porzione di codice che usa la connessione all’interno del blocco using(connessione). In quest’ultimo caso non sarà necessario chiudere esplicitamente la connessione stessa.
Un comando SQL può essere parametrizzato (ovvero ricevere i parametri da altri controlli presenti sulla pagina). È sufficiente inserire all’interno del controllo di selezione (ipotizzando che sia di tipo selezione) il parametro ‘< selectparameters >< asp:controlparameter name=”Title” controlid=”DropDownList1″ propertyname=”SelectedValue”/ >< /selectparameters >’ e specificare quindi il parametro con @Title. In questo esempio il valore del parametro “Title” verrà prelevato dal valore selezionato della DropDown con ID DropDownList1. Altre “risorse” da cui prelevare parametri (e che funzionano a grandi linee allo stesso modo) sono offerti dai controlli QueryStringParameter, SessionParameter, CookieParameter, ProfileParameter, FormParameter e Parameter.
A livello di database (SQL Server) è utile conoscere la pratica del Connection Pooling che permette di mantenere aperte un set di connessioni da condividere tra le sessioni che usano gli stessi Data Source (si può aggiungere il parametro Max/Min Pool Size alla stringa di connessione) e le Connection Statistics che possono essere abilitate (sono disabilitate di default).
La classe Command permette di eseguire tutti i comandi SQL. È possibile eseguire anche comandi di Data Definition (creare e alterare database e tabelle) anche se spesso questi vengono eseguiti direttamente sul DB e naturalmente sono incluse le Data Manupulation (Select, Update, Insert, Delete ecc..). I valori della CommandType Enumeration sono CommandType.Text (eseguire query SQL dirette), CommandType.StoredProcedure (per eseguire Stored Procedures) e CommandType.TableDirect (funzioni legacy).
Segue un esempio di connessione ad un Database (Northwind) e di selezione dati da una tabella (Employees):
String connectionString = WebConfigurationManager.ConnectionStrings[“Northwind”].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
String sql = “SELECT * FROM Employees”;
SqlCommand cmd = new SqlCommand(sql, con);
con.Open();
SqlDataReader reader = cmd.ExecuteReader();
StringBuilder htmlStr = new StringBuilder(“”);
while (reader.Read()) //.NextResult() restituisce se c’è un record successivo
{
htmlStr.Append(reader[“TitleOfCourtesy”]); //dato recuperato dal nome di colonna
htmlStr.Append(reader.GetString(1)); //recupera il primo valore di tipo String
htmlStr.Append(reader.GetString(2)); //recupera il secondo valore di tipo String
htmlStr.Append(reader.GetDateTime(6).ToString(“d”)); //recupera la 6° data
}
reader.Close();
con.Close();
Quando si utilizzano i Data Reader bisogna controllare e gestire manualmente gli eventuali dati nulli visto che non esiste un supporto nativo. In particolare andrebbero fatti i seguenti controlli:
int? nullableInteger = null;
if (nullableInteger.HasValue) { }
oppure
int? numberOfHires;
if (reader[“NumberOfHires”] == DBNull.Value)
{
numberOfHires = null;
}
else
{
numberOfHires = (int?)reader[“NumberOfHires”];
}
I comandi diversi dalla Select vanno eseguiti con ExecuteNonQuery().
Le Query da e verso un database sono uno dei punti di una applicazione più soggetti ad attacchi informatici: il fenomeno in particolare è noto con il nome di SQL Injection: SQLInjection è un tipo di attacco che manomette l’esecuzione di una query introducendo parametri non previsti dallo sviluppatore. Spesso sfruttano debolezze sul controllo dei dati (tipicamente presi da QueryString). Le soluzioni più comuni sono trattare con Replace gli apici singoli e gli spazi, limitare i privilegi dell’utente sui vari DB e usare Parametrized Commands (@CustomerID) al posto di (‘MyCustId’).
Per migliorare la gestione della comunicazione tra applicazione e DB è spesso utile ricorrere all’uso di Stored Procedures. Le SP sono delle strutture preconfezionate per lavorare sulle tabelle (simili alle funzioni). Prima si scrive la SP DB-side, poi nel codice, per ogni componente della SP, va indicato il tipo e il valore da assegnare. In particolare:
cmd.Parameters.Add(new SqlParameter(“@LastName”, SqlDbType.NVarChar, 20));
cmd.Parameters[“@LastName”].Value = lastName;
A questo punto si procede con la normale esecuzione con il comando ExecuteNonQuery().
Capita che si renda necessario controllare se un insieme di istruzioni sia stato completato con successo e, in caso contrario, riportare i dati allo stato iniziale. Per compiere questa operazione sono disponibili le Transaction. Le Transactions sono set di operazioni che possono avere esito positivo o negativo per singola unità. La Transaction tipicamente consegna le modifiche al Data-Source solo se tutte le azioni sono state completate con successo. Le proprietà di una Transaction sono elencate in un acronimo: ACID (Atomic, Consistent, Isolated,Durable).
Sempre all’interno della definizione di una Transaction si trovano i concetti di Isolation Level e Savapoint.
Isolation level è un concetto che permette di impostare il livello di “sensibilità” alle modifiche operate da altre Transactions. Nelle SP scritte lato Server uso la direttiva SET TRANSACTION ISOLATION LEVEL, mentre in ADO.NET devo passare un valore dall’enumerazione IsolationLevel.
I Savepoints sono dei marker che memorizzano un certo istante nel flow della Transaction e sono in grado di riportare indietro la situazione a quel punto. Si utilizzano chiamando il metodo Save() su un oggetto di tipo SqlTransaction. Per tornare ad un Savepoint uso, sempre sullo stesso oggetto, il comando Rollback() e per confermare Commit().
In una applicazione professionale il codice di comunicazione con i database non è incluso direttamente nel codice di una pagina ma salvato in apposite classi. Per compiere operazioni sui database sarà necessario creare una istanza di questa classe e chiamare i metodi appropriati.
Le strategie di concorrenza per risolvere il problema dell’accesso contemporaneo ai dati possono essere, come già accennato in precedenza, di tipo Last-in-wins updating (clausola WHERE sull’ID), Match-All updating (clausola WHERE su tutti i campi per evidenziare eventuali cambiamenti), Timestamp-based-updating (in cui vengono confrontati i timestamp per evidenziare le modifiche) e Changed-value-updating (permette update contemporanei se su attributi diversi).
L’oggetto DataSet permette di lavorare con copie “locali” dei dati presenti nel DB (dopo averli scaricati dal DB stesso) fornendo poi la possibilità di salvare l’insieme di dati modificati in un’unica operazione batch. È prevista una funzione built-in per serializzare i dati in formato XML.
DataSet permette di lavorare su una collezione di zero o più tabelle e una di zero o più relazioni.
DataAdapter è la classe che si occupa di gestire i DataSet, recuperando i dati con il metodo Fill().
La ricerca, resa possibile da un array di tipo DataRow[], viene effettuata chiamando il metodo Select(/*condizione*/)
DataView permette di creare delle View su sugli oggetti DataTable.
Typed DataSet sono una serie di classi specializzate derivati dalla base-class DataSet, DataTable o DataRow. Il vantaggio è che sono molto più “integrati” con il data-source con cui vanno a lavorare.
Se dovessimo fare un tour “virtuale” partendo dalla applicazione fino ad arrivare al dato avremmo la User interface che comunica con le Classi per tabelle, che a loro volta si legano ad un sistema di lettura/scrittura (come può essere una query o una Stored Procedure) per poi andare direttamente a lavorare con i dati presenti nel database.
Un esempio di utilizzo è quello seguente (in cui si fa riferimento al database “Northwind”):
string connectionString =
WebConfigurationManager.ConnectionStrings[“Northwind”].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
string sql = “SELECT * FROM Employees”;
SqlDataAdapter da = new SqlDataAdapter(sql, con);
DataSet ds = new DataSet();
da.Fill(ds, “Employees”);