Introduzione ad ADO.NET

In applicazioni client/server gli ambienti di memorizzazione dei dati maggiormente utilizzati sono i database relazionali. In genere in questi ambienti il client si connette al database-server instaurando unaconnessione permanente durante la quale l'utente effettua tutte le operazioni di manipolazione dei dati.

Questo significa che il database-server deve creare una connessione per ognuno dei client e mantenerla attiva per tutta la sessione di lavoro del client stesso. Tutto funziona correttamente se il software di gestione dei database e l'hardware sono abbastanza potenti da supportare il numero di client connessi, ma soprattutto se la larghezza di banda è adatta a supportare il traffico che viene generato.

Questo diventa molto più critico quando lo scenario è quello di Internet, dovuto alla natura disconnessa del protocollo HTTP, perché per evitare un collasso in termini di prestazioni, la tecnologia di accesso ai dati deve essere in grado di permettere all'utente di accedere ai dati, memorizzarli in una cache e disconnettersi dal database-server.

Una volta manipolati i dati, l'utente deve potersi riconnettere al database-server e poterlo aggiornare.

Ovviamente il .NET Framework che mette a disposizione una serie di classi che implementano una nuova tecnologia di accesso ai dati.

La migliora fondamentale apportata da ADO.NET, rispetto ad ADO, è rappresentata dal modello disconnesso , attraverso l'oggetto DataSet , e da un importante elemento infrastrutturale, il Managed provider .

I namespace di ADO.NET

ADO.NET, mette a disposizione una serie di classi che consentono l'accesso a differenti tipi di database. Essendo parte integrante del .NET Framework e costituite da codice managed, forniscono la massima efficienza possibile

Le classi si trovano in una serie di namespace:

  • System.Data : contiene gli oggetti di base utilizzati per accedere e memorizzare i dati;
  • System.Data.Common : contiene le classi di base utilizzate da altri oggetti;
  • System.Data.Odbc : contiene le classi pubbliche per connettersi/manipolare attraverso driver ODBC;
  • System.Data.OleDb : contiene le classi pubbliche utilizzate per connettersi/manipolare attraverso un provider OLE.DB;
  • System.Data.OracleClient : contiene le classi pubbliche utilizzate per connettersi/manipolare con un database Oracle;
  • System.Data.SqlClient : contiene le classi pubbliche utilizzate per connettersi/manipolare con un database Microsoft SQL Server, utilizzando l'interfaccia nativa TDS (Tabular Data Stream);
  • System.Data.SqlServerCe : contiene le classi pubbliche utilizzate per connettersi/manipolare con una sorgente dati in Windows CE;
  • System.Data.SqlTypes : contiene le classi pubbliche che implementano i tipi di dati che sono differenti dai tipi di dati standard del .NET Framework.

Managed provider

Un Managed provider è qualcosa di molto simile a quello che è conosciuto come OLE-DB Provider. È composto da una serie di classi che vengono utilizzate per connettersi ad un datasource, eseguire comandi, gestire transazioni e manipolare resultset.

A differenza di OLE-DB, il Managed provider è costituito da un più piccolo insieme di interfacce e si basa sui tipi di dati del .NET Framework. Ciascun Managed provider implementa le stesse classi di base così composte:

  • Connection : rappresenta una connessione all'origine dati;
  • Command : rappresenta un comando inviato al database;
  • DataReader : rappresenta un cursore read-only, forward-only, estremamente rapido e leggero;
  • Parameter : rappresenta un parametro da passare all'oggetto Command;
  • Transaction : per la gestione delle transazioni.

A differenza dei Managed provider specifici per database come possono essere SQL Server, Oracle, o MySQL, che sfruttano le classi native e agganciano direttamente a basso livello il database ottenendo un sostanziale aumento delle prestazioni, i managed provider OLE-DB e ODBC servono come ponte di accesso verso un gran numero di database che dispongono di provider OLE-DB e driver ODBC.

Effettuare la connessione al database

Nel modello ad oggetti di ADO.NET, l'oggetto Connection rappresenta una connessione all'origine dati.

Il primo passo da eseguire, prima della creazione dell'oggetto Connection, è quello di importare i riferimenti relativi ai namespace per il managed provider che vogliamo usare.

Nel frammento di codice riportato di seguito, verranno utilizzati i namespace del managed provider OLE-DB:

Imports System.Data
Imports System.Data.OleDb
' stringa di connessione
' provider OLE-DB per database SQL Server
Dim cnString As String = "Provider=SQLOLEDB;Data Source=Server; " & _
"Initial Catalog= Northwind;User ID=uid;Password=pwd;"
' istanza della Connection
Dim cn As New OleDbConnection(cnString)

Una volta che si dispone di un oggetto OleDbConnection con una stringa di connessione valida, è necessario aprire la connessione, con una chiamata al metodo Open, per instaurare una comunicazione con il database:

cn.Open()

Per chiudere un oggetto Connection, è necessaria una chiamata al metodo Close:

cn.Close()

L'oggetto Connection, come molte altre classi ADO.NET, espone un metodo Dispose. Richiamando questo metodo, sarà possibile liberare le risorse che precedono la Garbage Collection e, inoltre, sarà richiamato in modo implicito anche il metodo Close.

cn.Dispose()

Esecuzione di query nel database

Attraverso l'oggetto Command è possibile eseguire statement SQL nel database.

Le query che non restituiscono nessun risultato, ma che hanno come scopo quello di modificare il contenuto del database, vengono definite come query DML (Data Manipulation Language). A questo gruppo di query fanno parte istruzioni del tipo UPDATE, INSERT INTO, DELETE.

Le query che invece modificano la struttura del database, vengono definite come query DDL (Data Definition Language) ed a questo gruppo di query fanno parte istruzioni del tipo CREATE TABLE, DROP TABLE, altER VIEW.

Nel frammento di codice riportato di seguito, verrà creato un oggetto Command utilizzando il metodo CreateCommand dell'oggetto Connection. E' inoltre possibile creare un oggetto Command utilizzando uno dei costruttori disponibili:

...
cn.Open
Dim cmd As OleDbCommand = cn.CreateCommand

cmd.CommandText = "UPDATE tabella SET nomecampo =  'valore'"
cmd.ExecuteNonQuery()

Il metodo ExecuteNonQuery dell'esempio precedente non restituisce alcun risultato, perchè va utilizzato per query di tipo DML e DDL.

Talvolta però si ha la necessità di stabilire se la query ha modificato effettivamente la riga dalla tabella ed il metodo in questione restituisce il numero di righe influenzate dalla query, come variabile intera.

Nel frammento di codice riportato di seguito è mostrato come gestire il valore di ritorno del metodo ExecuteNonQuery:

...
cn.Open
Dim cmd As OleDbCommand = cn.CreateCommand

cmd.CommandText = "UPDATE tabella SET nomecampo =  'valore'"

'variabile che contiene il valore di ritorno del metodo

Dim result As Integer = cmd.ExecuteNonQuery()

If result = 0 Then
  Response.Write("Update fallito!")
Else
  Response.Write("Sono stati aggiornati " & result & " record(s)!")
End If

L'oggetto Command espone anche un metodo ExecuteScalar , necessario per recuperare, attraverso una query al database, un singolo valore.

Nel frammento di codice riportato di seguito, è mostrato usare il metodo ExecuteScalar:

cn.Open
Dim cmd As OleDbCommand = cn.CreateCommand
 
cmd.CommandText = "SELECT COUNT(*) FROM tabella"
dim count As Integer = cmd.ExecuteScalar()

Questo metodo va utilizzato quando si deve prendere la prima riga della prima colonna della query, ad esempio in query di aggregazione.

Come il recordset di ADO? Il DataReader!

L'oggetto DataReader è stato progettato per comunicare direttamente con l'origine dati.

Essendo di dimensioni ridotte è molto efficiente e, considerando l'accesso diretto al cursore firehose, consente di esaminare i risultati, in modalità di sola lettura, in un modo estremamente rapido.

Per creare un oggetto DataReader è necessario chiamare il metodo ExecuteReader di un oggetto Command. Cosa importante da tenere presente è che la prima riga dei risultati non sarà disponibile fino alla chiamata del metodo Read . Quindi la prima volta che si chiama il metodo Read, il DataReader si posizionerà sulla prima riga dei risultati e così via per scorrere tutte le righe disponibili.

Nel frammento di codice riportato di seguito è mostrato come creare un oggetto DataReader:

cn.Open
Dim sql = "SELECT * FROM tabella"
Dim cmd As New OleDbCommand(sql, cn)
Dim objDataReader As OleDbDataReader = cmd.ExecuteReader

While objDataReader.Read
  Response.Write(objDataReader("nomecampo"))
End While

objDataReader.Close()

E' importante sottolineare che il metodo Read restituisce anche un valore di tipo Boolean, quindi se tale valore è False significa che è stato raggiunto la fine del resultset. Di importanza fondamentale è che la chiusura di un oggetto DataReader avvenga il più velocemente possibile, anche perché un oggetto DataReader aperto è considerato bloccato e tiene aperta, tra l'altro, la connessione fisica verso la base dati.

Query parametriche

L'oggetto Parameter di ADO.NET ci consente di memorizzare le informazioni del parametro, da aggiungere ad un oggetto Command.

L'uso di questo oggetto è fondamentalmente quello di creare query parametriche, un ottimo supporto per difendere un'applicazione dagli attacchi di SQL Injection, che puntano ad eseguire codice all'interno delle nostre applicazioni passando tra i parametri codice SQL formattato ad hoc.

Nel frammento di codice riportato di seguito è mostrato come implementare questo oggetto:

cn.Open
Dim sql As String = "SELECT * FROM users WHERE UserID = ? AND Password = ?"
Dim cmd As New OleDbCommand(sql, cn)
' specifico l'UserID
cmd.Parameters.Add("@UserID", OleDbType.WChar, 8)
cmd.Parameters(0).Value = "userid"
' specifico la password
cmd.Parameters.Add("@Password ", OleDbType.WChar, 8)
cmd.Parameters(1).Value = "password"
' eseguo la query
Dim objDataReader As OleDbDataReader = cmd.ExecuteReader

Query transazionali

L'oggetto Command dispone di una proprietà Transaction che è necessario impostare per eseguire l'oggetto Command come transazione.

L'utilizzo di transazioni permette infatti di ritornare al valore iniziale di un dato, effettuando quindi ilrollback , prima di eseguire il commit della transazione stessa, che ha invece l'effetto di confermare la modifica all'interno del database.

Nel frammento di codice riportato di seguito è mostrato come implementare questo oggetto:

cn.Open
' faccio partire la transazione

Dim objTransaction As OleDbTransaction = cn.BeginTransaction
Dim sql As String = "INSERT INTO tabella (...) VALUES (...)"
Dim cmd As New OleDbCommand(sql, cn, objTransaction)
Dim result As Integer = cmd.ExecuteNonQuery()

If result = 0 Then
  Response.Write("Update fallito!")
  objTransaction.Rollback()
Else
  Response.Write("Sono stati aggiornati " & result & " record(s)!")
  objTransaction.Commit()
End If

Il ruolo del DataAdapter

L'oggetto DataAdapter ha la funzione di mettere in comunicazione le nostre applicazioni con quella che possiamo definire la parte disconnessa di ADO.NET rappresentata dall'oggetto DataSet . Quindi, se si desidera memorizzare i risultati di una query, l'oggetto DataSet diventa il contenitore di questi risultati, dove l'oggetto DataAdapter , attraverso il metodo Fill, si occupa di popolare la collezione DataTable dell'oggetto DataSet con i risultati delle query:

OleDbDataAdapter.Fill(DataSet)

A sua volta, il DataAdapter si occupa anche di inviare gli aggiornamenti pendenti all'interno del DataSet al database per il successivo aggiornamento.

Da sottolineare che tutta la logica di aggiornamento è interamente controllata dal DataAdapter, che presenta quattro proprietà fondamentali per effettuare le seguenti operazioni:

  • SelectCommand : specifica la query di selezione;
  • UpdateCommand : specifica la query di agiornamento;
  • InsertCommand : contiene il comando per gli inserimenti;
  • DeleteCommand : contiene la query per la cancellazione dei record.

Nel frammento di codice riportato di seguito è mostrato come si crea un oggetto DataAdapter e come popolare un oggetto DataSet attraverso il metodo Fill:

Dim cn As New OleDbConnection(...)
Dim sql As String = "SELECT * FROM tabella"
Dim objDataAdapter As New OleDbDataAdapter(sql, cn)
Dim objDataSet As New DataSet()
objDataAdapter.Fill(objDataSet)

Il metodo Fill ha portato alla creazione implicita di una collezione di DataTable all'interno del DataSet. Il DataTable ha una struttura similare a quella estrapolata dalla query, mentre il suo nome identificativo, non avendolo dichiarato esplicitamente, sarà uguale a "Table". E' possibile, però, chiamare il DataTable, secondo le nostre esigenze.

Dim objDataAdapter As New OleDbDataAdapter(sql, cn)
objDataAdapter.TableMappings.Add("Table", "Orders")
Dim objDataSet As New DataSet()
objDataAdapter.Fill(objDataSet)

Come si è potuto dai frammenti di codice precedenti, non è stato mai chiamato il metodo di apertura della connessione, questo perché il metodo Fill apre la connessione, invia la query, recupera i risultati e chiude la connessione appena aperta. Di seguito verranno elencati i metodi di un oggetto DataAdapter, compresi quelli che sono stati già trattati come il metodo Fill e il metodo Update:

  • Fill : esegue la query e memorizza i risultati in un oggetto DataTable;
  • FillSchema : recupera le informazioni sullo schema della tabella restituita dalla query;
  • GetFillParameters : restituisce un array contenente i parametri di SelectCommand;
  • Update : invia al database le modifiche pendenti nell'oggetto DataSet.

Anche se utili, queste funzionalità di sincronizzazione vanno evitate nell'ambito di un'applicazione web classica, poiché l'uso delle query (inline o meglio ancora con stored procedure) consente di avere un'applicazione più performante e soprattutto ha più senso in un ambiente disconnesso come il web, al contrario di un'applicazione Windows Forms (che usa le stesse classi di base) che potrebbe invece trarre giovamento da questo approccio.

Il contenitore per eccellenza: il DataSet

L'oggetto DataSet lo si può considerare come un contenitore di n oggetti DataTable, dove noi andremo a riportare i risultati di una query utilizzando l'oggetto DataAdapter.

A differenza dell'oggetto DataReader, il DataSet consente di esaminare il contenuto di qualsiasi riga, passando da una riga di risultati precedente o successiva in qualsiasi momento.

Inoltre consente di memorizzare nella cache le modifiche apportate ad un record ed inviarle al database in qualsiasi momento lo vogliamo utilizzando sempre l'oggetto DataAdapter.

All'interno del DataSet è anche possibile effettuare degli ordinamenti, delle ricerche e applicare filtri, determinare che tipo di operazione è stata effettuata (un inserimento, una modifica e cosi via), ma soprattutto confrontare i valori originali con quelli correnti, per ciascun record.

Il DataSet è composto da una struttura gerarchica formata da oggetti DataTable (le tabelle) eDataRelation (le releazioni). A sua volta, un oggetto DataTable contiene oggetti DataRow (le righe),DataColumn (le colonne) e Costraint (i vincoli).

Come è stato già spiegato in precedenza, i risultati di una query vengono memorizzati all'interno dell'oggetto DataTable che dispone di una proprietà Columns , che a sua volta restituisce un insieme di oggetti DataColumn , dove ciascuno di essi corrisponde a una colonna nei risultati della query.

Lo stesso identico discorso può essere fatto per le colonne, sfruttando la proprietà Rows , che restituisce un oggetto DataRowCollection che a sua volta contiene un insieme di oggetti DataRow .

Nel frammento di codice riportato di seguito è mostrato come una volta impostato il DataRow su una riga, l'accesso ai valori di una particolare colonna risulti essere estremamente semplice. Da notare che è possibile fornire sia il nome della colonna, che un valore intero che rappresenta la posizione della colonna:

Dim cn As New OleDbConnection(...)
Dim sql As String = "SELECT * FROM tabella"
Dim objDataAdapter As New OleDbDataAdapter(sql, cn)
Dim objDataSet As New DataSet()
objDataAdapter.Fill(objDataSet)

Dim objTable As DataTable = objDataSet.Tables(0)
Dim objRow As DataRow = objTable.Rows(1)

Response.Write("Campo1: " & objRow("NomeCampo"))
Response.Write("Campo2: " & objRow("NomeCampo"))

Conclusioni

Gli oggetti ADO.NET sono stati pensati per ridurre al minimo l'impatto delle query sul database, ma soprattutto per diminuire il più possibile la durata della connessione alla sorgente dati.

Molti si chiederanno quando è meglio utilizzare un DataReader e quando un DataSet. Una risposta secca e precisa probabilmente non esiste, la scelta può dipendere dal tipo di applicazione che si sta sviluppando, dalla conoscenza che abbiamo degli oggetti DataReader e DataSet e da tanti altri fattori, ma in linea di massima, quando c'è da leggere semplicemente dei dati, che non devono essere manipolati o tenuti in cache, il DataReader è da preferire in quanto, sotto la coperta, il DataAdapter utilizza proprio quest'ultimo per popolare il DataSet.

Sarebbe opportuno, dove possibile, utilizzare sempre l'oggetto DataReader per spostare la scelta sull'oggetto DataSet in situazioni di questo tipo:

  • quando risulta necessario memorizzare i dati per elaborarli, modificarli e poi ripassarli al client;
  • quando risulta necessario aggiornare i dati utilizzando i metodi del DataSet e DataAdapter piuttosto che utilizzare singoli statement SQL;
  • quando bisogna effettuare il binding dei dati legando gli stessi a più controlli;
  • quando risulta necessario avanzare o tornare indietro all'interno del resultset, cosa questa impossibile con l'oggetto DataReader.

Torna su
Categoria

ASP.NET (9)


Autore

Asp.Net Italia


Data pubblicazione.

30/08/2012



Recensioni

Articolo non ancora recensito