Capitolo 6
Query
Nel precedente capitolo abbiamo visto come collegarsi a un database. Abbiamo
parlato dell’oggetto Connection, delle sue proprietà e dei suoi metodi. Abbiamo
visto come usare tale oggetto per aprire una connessione a un database, ma
soprattutto abbiamo visto come gestire la connessione, ovvero come aprirla,
usarla e chiuderla. Si apre una connessione perché si intende compiere una o
più operazioni sul database. In questo capitolo vederemo che tipo di operazioni si possono eseguire e come possono essere eseguite con l’ausilio della tecnologia .NET.
In particolare parleremo dei seguenti argomenti:
• Quali tipi di operazione si possono eseguire su un database
• L’oggetto Command di ADO.NET: proprietà e metodi
• Query di lettura dei dati
• Query di scrittura dei dati
• Query parametriche
• Possibilità di query
Tipi di operazione di accesso ai dati
Le operazioni di accesso ai dati eseguite da un’applicazione client-server sono
essenzialmente di due tipi:
• operazioni di lettura: si esegue un’operazione di lettura dei dati per
recuperare informazioni; le operazioni di lettura non modificano la struttura
o il contenuto informativo di un database;
• operazioni di scrittura: si esegue un’operazione di scrittura per modificare o cancellare dati o per inserirne dei nuovi; le operazioni di scrittura,
quindi, modificano il contenuto informativo di un database.
Vi è, in realtà, un terzo tipo di operazioni che possono essere eseguite su un
database: sono le operazioni che modificano la struttura del database stesso.
114
Capitolo 6
Attraverso tali operazioni è possibile creare dei nuovi oggetti (ad esempio,
tabelle, viste, indici e vincoli), modificare le caratteristiche degli oggetti già
esistenti (ad esempio, modificare il formato di una colonna di una tabella o
rinominare una colonna) oppure eliminare fisicamente alcuni oggetti del database (ad esempio, droppare una tabella o una vista). Queste operazioni rientrano nella categoria dei comandi DDL del linguaggio SQL di cui si è parlato in
dettaglio nel Capitolo 3. Le operazioni DDL, però, data la portata strutturale dei
loro interventi di modifica, non sono generalmente eseguite attraverso un’applicazione client-server, ma sono demandate e riservate all’esercizio esclusivo del
DBA che le esegue attraverso l’utilizzo dei tool specifici di governo e controllo
del database. In questo capitolo, quindi, dedicheremo particolare attenzione
alle operazioni tipicamente eseguite da un’applicazione client-server, ovvero le
operazioni di lettura e scrittura dei dati (eseguite attreverso comandi di tipo
DML), pur non tralasciando qualche breve cenno alle operazioni di tipo strutturale (comandi DDL). La Figura 6.1 illustra le relazioni tra i tipi di operazione fin
qui descritti.
Le operazioni di lettura hanno essenzialmente lo scopo di estrarre da una fonte
di dati un set di informazioni. L’estrazione dei dati può essere fine a se stessa,
ovvero soddisfare le sigenze meramente informative dell’utilizzatore, ad esempio, l’interrogazione di un database per ricavare una precisa informazione, oppure può essere propedeutica e preliminare a una successiva operazione di
scrittura dei dati. In tal caso la fase di lettura è indispensabile per conoscere lo
stato dell’informazione che si intende modificare: ad esempio, l’estrazione e
visualizzazione dei dati anagrafici di un cliente o fornitore è propedeutica e
preliminare all’aggiornamento degli stessi.
Possiamo ulteriormente suddividere le operazioni di lettura, distinguendo, nell’ambito delle interrogazioni della fonte dati a fini esclusivamente informativi, le
estrazioni di dati puntuali dalle estrazioni di dati di sintesi. Le prime restituisco-
Applicazioni
transazionali
DBA
DML
DDL
Lettura dati:
• estrazione
Scrittura dati:
• modifica
• cancellazione
• inserimento
Figura 6.1
Relazioni tra tipi di operazione e modalità di gestione.
Modifica struttura/
oggetti:
• modifica
• cancellazione
• creazione
Query
115
no informazioni di dettaglio relative a un oggetto del database, come lo stato di
avanzamento di un ordine o l’indirizzo di un cliente; le seconde, invece, forniscono informazioni sintetiche relative a un fenomeno aziendale misurabile attraverso un’estrazione di dati aggregati, come la distribuzione degli ordini nelle
varie aree geografiche dei mercati trattati. In Figura 6.2 sono illustrati i diversi
tipi di operazione di lettura.
Una prima classificazione delle operazioni di scrittura è quella che tiene conto
del tipo di intervento che eseguono sui dati. Possiamo, pertanto, distinguere tra
operazioni di modifica, inserimento e cancellazione dei dati. Un’ulteriore classificazione è quella che tiene conto del modo in cui è organizzata la logica
dell’operazione di scrittura. Questa cambia radicalmente a seconda del numero
di oggetti interessati. L’intervento sui dati, infatti, può riguardare un singolo
record di una tabella, ad esempio l’inserimento di un nuovo cliente nella relativa anagrafica, o un set coerente di dati, ad esempio tutte le fatture non ancora
pagate intestate a un dato cliente. La differenza tra i due interventi non è solo di
tipo numerico, ma attiene al modo di organizzare un’operazione più o meno
complessa di scrittura dei dati. Quando l’operazione di scrittura non presenta
particolari complessità logiche, è generalmente ottenuta con un’istruzione SQL
direttamente eseguita dall’applicazione; negli altri casi l’esecuzione dell’istruzione SQL è demandata a una stored procedure.
Tipo di lettura
Scopo
Estrazione di dati puntuali
• Consultazione puntuale
• Preliminare alla modifica
Estrazione di dati aggregati
• Consultazione di sintesi
(reporting)
Figura 6.2
Tipi di operazioni di lettura dei dati.
Oggetto Command di ADO.NET
Un’applicazione che fa riferimento ai dati contenuti in un database, in particolare un’applicazione client-server, nel corso del suo funzionamento esegue un
gran numero di operazioni di accesso alla fonte dei dati. Sono sia operazioni di
lettura sia operazioni di scrittura. In entrambi i casi l’operazione vera e propria
non è eseguita dall’applicazione richiedente, ma dal DBMS gestore dei dati.
L’applicazione, ovvero il lato client del sistema client-server, si limita a generare
una richiesta di accesso ai dati che trasmette al sistema gestore dei dati stessi. È
116
Capitolo 6
quest’ultimo che si occupa di eseguire l’operazione e dare una risposta al richiedente. Il DBMS, in particolare, riceve la richiesta di accesso ai dati, ne
controlla la correttezza formale, la esegue e restituisce all’applicazione richiedente l’esito e il risultato dell’operazione stessa.
Esito e risultato sono due concetti distinti. L’esito dell’operazione è un’informazione sintetica che ci dice se l’operazione è stata portata a termine positivamente o se si sono generati errori. Il risultato, invece, nel caso in cui l’operazione
abbia avuto esito positivo, può essere sia il contenuto informativo di cui l’applicazione aveva chiesto l’estrazione, sia un riepilogo sintetico dell’operazione
eseguita, ad esempio, il numero di record modificati.
L’oggetto Command ha la funzione di trasmettere al DBMS una richiesta di accesso ai dati e di riportare all’applicazione richiedente l’esito e il risultato dell’operazione eseguita dal motore del database. Potremmo immaginarlo come un
messaggero che veicola il comando dall’applicazione al database, attende l’esito e la conclusione delle operazioni e riporta al richiedente le informazioni
richieste in caso di operazioni di lettura e, in ogni caso, l’esito posisivo o negativo della transazione. Il funzionamento dell’oggetto Command è schematizzato
in Figura 6.3.
Il framework .NET ci mette a disposizione tre diversi oggetti di tipo Command:
• SqlCommand: è progettato per interfacciare e trasmettere comandi a database di tipo Microsoft SQL Server di versione pari o superiore alla 7.0;
• OleDbCommand: è progettato per interfacciare e trasmettere comandi ai fornitori di dati del tipo OLEDB Managed Providers, come database di tipo SQL
Server 6.5 e precedenti, database Oracle e database Access;
• OdbcCommand: è progettato per interagire con una fonte dati ODBC.
Applicazione richiedente
DBMS
testo del
comando
Command
motore
• trasmette
comando
• riceve
comando
• riceve
esito e
risultato
• controlla
• esegue
• restituisce
esito e
risultato
Figura 6.3
Funzionamento dell’oggetto Command.
dati
Query
117
Creazione del comando
Per creare un oggetto Command è necessario aver prima importato il namespace
corrispondente al tipo di oggetto che si intende istanziare. Abbiamo visto come
si usa l’istruzione Imports nel capitolo precedente, quando abbiamo spiegato
come creare una connessione attraverso l’oggetto Connection. Saltiamo, quindi,
la fase di import del namespace e vediamo come istanziare e creare un oggetto
Command. Anche per l’oggetto Command, così come per l’oggetto Connection, c’è
un modo esplicito e un modo implicito per creare un nuovo oggetto.
La creazione esplicita passa attraverso l’uso dell’istruzione New:
Dim myCommand As System.Data.SqlClient.SqlCommand
myCommand = New SqlClient.SqlCommand()
oppure, avendo specificato di importare il namespace System.Data.SqlClient,
si può creare più semplicemente un nuovo oggetto Command nel seguente modo:
Dim myCommand As SqlCommand
myCommand = New SqlCommand()
La creazione implicita, invece, passa attraverso l’uso dell’oggetto Connection. In
particolare, bisogna invocarne il metodo CreateCommand:
Dim myConnection As SqlClient.SqlConnection
Dim myCommand As SqlClient.SqlCommand
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
‘ creazione implicita del comando
‘ con il metodo CreateCommand
myCommand = Connection.CreateCommand()
Proprietà dell’oggetto Command:
la stringa di comando
La proprietà CommandText è una delle proprietà più importanti dell’oggetto Command. Contiene il testo del comando che si intende far eseguire al database.
Ovviamente il comando deve essere scritto con la corretta sintassi SQL del
DBMS che dovrà eseguire le operazioni sul database.
La proprietà può essere valorizzata al momento della creazione dell’oggetto
Command, specificando subito il testo del comando come parametro dell’istruzione di creazione, o in un secondo momento assegnando alla proprietà una stringa contenente il comando SQL. Nell’esempio che segue vediamo come valorizzare la proprietà in modo contestuale alla creazione dell’oggetto:
Dim myCommand As SqlCommand
Dim mySQL As String
‘ scrittura dell’istruzione SQL
118
Capitolo 6
mySQL = “SELECT * FROM Impiegati”
‘ creazione esplicita del comando
‘ e contestuale assegnazione della stringa SQL
myCommand = New SqlCommand(mySQL)
In alternativa, la proprietà può essere valorizzata in un secondo momento,
come vediamo nell’esempio che segue:
Dim myConnection As SqlClient.SqlConnection
Dim myCommand As SqlClient.SqlCommand
Dim mySQL As String
‘ scrittura dell’istruzione SQL
mySQL = “SELECT * FROM Impiegati”
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
‘ creazione implicita del comando
myCommand = myConnection.CreateCommand()
‘ passaggio della stringa SQL
myCommand.CommandText = “SELECT * FROM Impiegati”
Nel secondo esempio abbiamo istanziato l’oggetto Command in modo implicito e
poi ne abbiamo valorizzato la proprietà CommandText. Non avremmo potuto
fare altrimenti perché la creazione implicta del comando non prevede il passaggio
di alcun parametro. Nel caso di creazione esplicita del comando, invece, è possibile
scegliere se passare subito la stringa SQL o se passarla in un secondo momento.
Proprietà dell’oggetto Command: la connessione
Un’applicazione può eseguire operazioni di accesso ai dati solo se è attiva una
connessione presso la fonte dati di destinazione. La connessione, infatti, rappresenta il canale attraverso cui l’applicazione interagisce con il database. L’oggetto Command, invece, è lo strumento che consente di veicolare un’istruzione
SQL dall’applicazione alla fonte dati e di riceverne il risulato. Questo, però, può
funzionare, ovvero trasmettere la sua richiesta di esecuzione di un comando al
DBMS di riferimento, solo se è attiva una connessione sul database. La dipendenza dell’oggetto Command dalla connessione è resa evidente dalla presenza
della poprietà Connection.
La proprietà Connection deve essere valorizzata con un oggetto valido di tipo
Connection. Per intenderci, la proprietà Connection è di tipo Connection. Per
valorizzarla, quindi, è necessario creare una nuova connessione e assegnarla
alla proprietà stessa. Vediamo un esempio:
Private Function LeggiImpiegati()
Dim myConnection As SqlClient.SqlConnection
Dim myCommand As SqlClient.SqlCommand
Query
Dim myStrConn As String
Dim mySQL As String
Dim myDataReader As SqlClient.SqlDataReader
Try
‘ scrittura dell’istruzione SQL
mySQL = “SELECT cognome, nome FROM Impiegati”
‘ creazione esplicita del comando
myCommand = New SqlClient.SqlCommand()
‘ creazione esplicita del DataReader
myDataReader = New SqlClient.SqlDataReader()
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
‘ composizione della stringa di connessione
myStrConn = BuildConnectionString()
‘ assegnazione della stringa di connessione
myConnection.ConnectionString = myStrConn
‘ apertura della connessione
myConnection.Open()
‘ passaggio della connessione attiva
‘ all’oggetto Command
myCommand.Connection = myConnection
‘ assegnazione del comando SQL
myCommand.CommandText = mySQL
‘ esecuzione del comando di lettura
myDataReader = myCommand.ExecuteReader()
With myDataReader
‘ legge una riga alla volta
‘ fino alla fine dei dati estratti
Do While .Read = True
Console.Write(.Item(“Cognome”) & “, “)
Console.Write(.Item(“Nome”) & Chr(13))
Loop
End With
‘ chiusura della connessione
myConnection.Close()
Catch thisExcept As Exception
‘ se si verifica un errore
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“LeggiImpiegati()”)
End Try
End Function
119
120
Capitolo 6
La funzione LeggiImpiegati estrae i campi nome e cognome contenuti nella
tabella Impiegati e ne visualizza il contenuto nella console di output. La funzione utilizza l’oggetto myDataReader per ricevere dalla fonte dati le informazioni
estratte. L’oggetto appartiene al tipo DataReader, di cui parleremo nel dettaglio
nel prossimo capitolo, per ora ci basti sapere che è un oggetto che consente di
ricevere e gestire il risultato di una query di lettura. I dati contenuti nel DataReader possono essere solo letti, scorrendo il cursore in maniera sequenziale
dal primo record estratto all’ultimo.
Analizziamo nel dettaglio il codice appena visto. Nella prima parte sono dichiarati gli oggetti e le variabili che saranno usate nella routine. Sono necessari un
oggetto Connection, un oggetto Command e due variabili stringa, una per contenere i parameti di connessione, l’altra per contenere l’istruzione SQL:
Dim
Dim
Dim
Dim
myConnection As SqlClient.SqlConnection
myCommand As SqlClient.SqlCommand
myStrConn As String
mySQL As String
Sono valorizzate le variabili stringa contenenti i parametri di connessione e
l’istruzione SQL:
‘ scrittura dell’istruzione SQL
mySQL = “SELECT cognome, nome FROM Impiegati”
‘ composizione della stringa di connessione
myStrConn = BuildConnectionString()
Sono istanziati gli oggetti myCommand, myDataReader e myConnection:
‘ creazione esplicita del comando
myCommand = New SqlClient.SqlCommand()
‘ creazione esplicita del DataReader
myDataReader = New SqlClient.SqlDataReader()
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
È assegnata la stringa di connessione all’oggetto myConnection ed è aperta la
connessione:
‘ assegnazione della stringa di connessione
myConnection.ConnectionString = myStrConn
‘ apertura della connessione
myConnection.Open()
A questo punto la connessione attiva può essere assegnata all’oggetto myCommand:
‘ passaggio della connessione attiva
‘ all’oggetto Command
myCommand.Connection = myConnection
Query
121
Il comando SQL è trasmesso alla fonte dati invocando il metodo ExecuteReader:
‘ esecuzione del comando di lettura
myDataReader = myCommand.ExecuteReader()
il risultato dell’operazione di lettura è assegnato all’oggetto myDataReader.
Si noti che al momento dell’esecuzione del comando la proprietà Connection
dell’oggetto myCommand è valorizzata con una connessione attiva. Se la connessione non fosse stata attiva, l’esecuzione del comando ExecuteReader avrebbe
generato un errore. Non è importante che la connessione sia attivata prima del
passaggio all’oggetto myCommand, l’attivazione, infatti, può avvenire anche in un
secondo momento, purché sia prima dell’esecuzione del metodo ExecuteReader:
‘ esecuzione del metodo Open
‘ della proprietà Connection
myCmd.Connection.Open()
I dati estratti sono visualizzati nella console di output di Visual Studio:
With myReader
‘ legge una riga alla volta
‘ fino alla fine dei dati estratti
Do While .Read = True
Console.Write(.Item(“Cognome”) & “, “)
Console.Write(.Item(“Nome”) & Chr(13))
Loop
End With
L’istruzione Chr(13) è il CharCode del codice 13 (corrispondente al tasto Invio),
serve per andare alla riga successiva dopo ogni record visualizzato.
Infine, prima della conclusione della funzione, la connessione deve essere chiusa
invocandone il metodo Close:
‘ chiusura della connessione
myConnection.Close()
In alternativa, il metodo Close poteva essere invocato attraverso la proprietà
Connection dell’oggetto myCommand:
‘ chiusura della connessione
‘myCommand.Connection.Close()
Proprietà dell’oggetto Command: il tipo di comando
L’oggetto Command è in grado di gestire sia istruzioni SQL sia chiamate a stored
procedure. In entrambi i casi il comando deve essere scritto nella proprietà
CommandText. L’oggetto Command, però, ha bisogno di sapere come interpretare
il testo di comando contenuto nella sua proprietà. L’informazione sul tipo di
comando è specificata nella proprietà CommandType.
122
Capitolo 6
La proprietà può assumere uno dei tre valori definiti per il CommandType. Di
seguito sono elencati tali valori con la relativa descrizione:
Tabella 6.1
Valore
Descrizione
Text
È il valore assegnato di default alla proprietà; fa sì che l’oggetto
Command interpreti il testo del comando come un’istruzione SQL.
Indica che il testo del comando contiene la chiamata a una stored
procedure.
Il valore non è valido in caso di Managed Provider di tipo SQL Server;
se il tipo di comando è TableDirect è necessario che la proprietà
CommandText contenga il nome di una tabella; in tal caso il metodo
ExecuteReader restituisce tutti i campi e tutti i record della tabella
referenziata.
StoredProcedure
TableDirect
Nella funzione LeggiImpiegati dell’esempio precedente abbiamo usato un comando di tipo SQL, ma non abbiamo valorizzato la proprietà CommandType perché il suo valore era per default già impostato su Text. Vediamo come avremmo
dovuto scrivere il codice nel caso in cui non fosse previsto il valore di default:
‘ definizione del tipo di comando per istruzioni SQL
myCommand.CommandType = CommandType.Text
Se, invece di un’istruzione SQL, avessimo voluto eseguire una chiamata a una
stored procedure avremmo dovuto definire la proprietà CommandType in questo
modo:
‘ definizione del tipo di comando per
‘ chiamate a stored procedure
myCommand.CommandType = CommandType.StoredProcedure
Possiamo fare un esempio di chiamata di stored procedure, provando a
usare la stored procedure Clienti e fornitori per città del database
Northwind, che abbiamo fatto migrare alla versione SQL Server nel precedente capitolo. La procedura è molto semplice: esegue due operazioni di
SELECT, una sulla tabella Clienti e una sulla tabella Fornitori, e riunisce i
dati estratti in un unico set di dati con un’istruzione UNION. È possibile
leggere il codice del-la stored procedure attraverso il tool Server Explorer
fornito da Visual Studio .NET: basta navigare all’interno della struttura ad
albero raffigurante i vari tipi di oggetti del database Northwind e aprire il
dettaglio della voce Stored Procedures; se siete fortunati troverete la procedura Clienti e fornitori per città ; per visualizzarne il codice è sufficiente selezionare la voce Modifica Stored Procedure (o Edit Stored Procedure se avete la versione inglese di Visual Studio); dovreste riuscire a vedere
il codice riportato nella Figura 6.4.
Query
123
Figura 6.4
Stored procedure “Clienti e fornitori per città”.
Riprendiamo, quindi, la precedente funzione LeggiImpiegati e modifichiamola
per leggere la stored procedure Clienti e fornitori per città; le cambiamo
il nome e la chiamiamo LeggiClientiFornitori:
Private Function LeggiClientiFornitori()
Dim
Dim
Dim
Dim
Dim
myCommand As SqlClient.SqlCommand
myConnection As SqlClient.SqlConnection
myDataReader As SqlClient.SqlDataReader
myStrConn As String
mySQL As String
Try
‘ il testo del comando contiene la
‘ chiamata alla stored procedure
mySQL = “[Clienti e fornitori per città]”
‘ creazione esplicita del comando
‘ e assegnazione della stringa SQL
myCommand = New SqlClient.SqlCommand(mySQL)
‘ creazione implicita del comando
‘myCommand= myConnection.CreateCommand()
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
myStrConn = BuildConnectionString()
myConnection.ConnectionString = myStrConn
myConnection.Open()
‘ passaggio della connessione attiva
‘ all’oggetto Command
myCommand.Connection = myConnection
‘myCommand.Connection.Open()
‘ definizione del tipo di comando
myCommand.CommandType = CommandType.StoredProcedure
124
Capitolo 6
‘ esegue il comando di lettura
myDataReader = myCommand.ExecuteReader()
With myDataReader
‘ legge una riga alla volta
‘ fino alla fine dei dati estratti
Do While .Read = True
Console.Write(.Item(“Città”) & “, “)
Console.Write(.Item(“NomeSocietà”) & “, “)
Console.Write(.Item(“Contatto”) & “, “)
Console.Write(.Item(“Relazione”) & Chr(13))
Loop
End With
‘ invoca il metodo Close per chiudere la connessione
myConnection.Close()
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“LeggiClientiFornitori()”)
End Try
End Function
Commentiamo solo le parti di codice nuove e tralasciamo quelle note. Innanzitutto, è cambiato il testo del comando, non contiene più un’istruzione SQL, ma
la chiamata alla stored procedure:
‘ il testo del comando contiene la
‘ chiamata alla stored procedure
mySQL = “[Clienti e fornitori per città]”
Il tipo di comando è impostato sul valore StoredProcedure:
‘ definizione del tipo di comando
myCommand.CommandType = CommandType.StoredProcedure
Nella console di output sono visualizzati i campi pubblicati dalla stored procedure:
With myDataReader
‘ legge una riga alla volta
‘ fino alla fine dei dati estratti
Do While .Read = True
Console.Write(.Item(“Città”) & “, “)
Console.Write(.Item(“NomeSocietà”) & “, “)
Console.Write(.Item(“Contatto”) & “, “)
Console.Write(.Item(“Relazione”) & Chr(13))
Loop
End With
Query
125
Proprietà dell’oggetto Command:
il tempo di esecuzione
Può essere settato un tempo massimo, espresso in secondi, di esecuzione del
comando. Il valore è definito valorizzando la proprietà CommandTimeout. La
proprietà ha per default il valore di trenta secondi. Se l’esecuzione del comando
supera il tempo massimo definito dalla proprietà CommandTimeout l’oggetto genera un’eccezione di tipo SqlException.
Negli esempi precedenti avremmo potuto definire un tempo massimo di esecuzione inferiore al valore di default:
‘ definizione del tempo massimo di esecuzione
myCommand.CommandTimeout = 10
Potete provare a eseguire il codice della funzione LeggiImpiegati dopo aver
aggiunto la riga di codice appena vista prima dell’esecuzione del comando di
lettura.
I metodi dell’oggetto Command:
l’esecuzione del comando
I metodi più importanti dell’oggetto Command sono quelli che eseguono il comando specificato nella proprietà CommandText. Sappiamo che in realtà il comando vero e proprio è eseguito dal DBMS gestore della fonte dati, per cui
l’invocazione del metodo di esecuzione ha solo la funzione di trasmettere al
database la richiesta di esecuzione dell’operazione sui dati.
Possiamo classificare i metodi di esecuzione del comando in due gruppi: metodi che eseguono comandi di lettura e metodi che eseguono operazioni di scrittura. Al primo tipo appartengono i metodi ExecuteReader ed ExecuteXmlReader. I due metodi si assomigliano molto, la differenza principale sta nel fatto
che il metodo ExecuteXmlReader restituisce i dati sotto forma di un oggetto
XML. Per il significato e le caratteristiche degli oggetti XML vi rimandiamo all’appendice D, per il momento ci basti sapere che il contenuto informativo non
cambia, cambia solo il modo in cui i dati sono rappresentati e trasmessi. Per
quanto riguarda l’esecuzione di comandi di scrittura, invece, abbiamo solo il
metodo ExecuteNonQuery.
Negli esempi precedenti abbiamo usato metodi di esecuzione dei comandi sia
di scrittura sia di lettura. Nel capitolo 5, infatti, abbiamo inserito dei record nella
tabella Impiegati, mentre negli esempi proposti in questo capitolo abbiamo
visto come leggere tali dati. Abbiamo usato, infatti, sia il metodo ExecuteReader
sia il metodo ExecuteNonQuery. Vediamo ora nel dettaglio le caratteristiche di
entrambi.
Il metodo ExecuteReader trasmette alla fonte dati di riferimento il comando di
lettura contenuto nella proprietà CommandText e trasferisce i dati estratti in un
oggetto di tipo DataReader. È necessario che la proprietà CommandType sia settata con il valore Text. L’esecuzione del comando consente di specificare un
126
Capitolo 6
parametro che specifica il comportamento (CommandBehavior) dell’oggetto DataReader generato dal metodo ExecuteReader. Di seguito elenchiamo i possibili valori del parametro CommandBehavior:
Tabella 6.2
Valore
Descrizione
CloseConnection
Fa sì che la connessione associata al comando, ovvero l’oggetto
Connection associato alla proprietà Connection, sia chiusa non appena terminata la lettura del DataReader.
Fa sì che il Data Provider non restituisca alcun record di dati ma solo
la struttura dei campi letti.
Fa sì che a ogni lettura di dati sia sempre aggiunta una colonna con la
primary key dei dati estratti
Il Data Provider restituisce un risultato singolo.
Il Data Provider restituisce solo i dati del primo record letto.
SchemaOnly
KeyInfo
SingleResult
SingleRow
Proviamo qualche CommandBehavior. Facciamo una prova, ad esempio, con il
valore SingleRow. Riprendiamo la funzione LeggiImpiegati e invochiamo il
metodo ExecuteReader passando il valore SingleRow come parametro CommandBehavior:
‘ esecuzione del comando di lettura
myDataReader = _
myCommand.ExecuteReader(CommandBehavior.SingleRow)
Proviamo a eseguire il codice. Nella console di output di Visual Studio saranno
visualizzati i dati di un solo impiegato.
Il metodo ExecuteNonQuery deve essere usato, invece, nei casi in cui il comando contenuto nella proprietà CommandText non contenga un’istruzione SQL di
lettura. In alternativa la proprietà può contenere un’istruzione di scrittura di dati
o un’istruzione di intervento sulla struttura del database (istruzione SQL di tipo
DDL). In entrambi i casi la fonte dati non restituisce alcun set di dati, ma
un’informazione sintetica sull’esito dell’operazione. Nel caso di istruzioni di
scrittura dei dati (INSERT, UPDATE, DELETE) il metodo restituisce l’informazione sul numero di record interessati dall’operazione; nel caso di istruzioni di tipo
DDL, invece, il metodo restituisce l’informazione sull’esito positivo o negativo
dell’istruzione.
Cominciamo con un esempio di istruzione SQL di tipo DML, proviamo cioè a
inserire dei record nella tabella Impiegati.
Private Function InserisciImpiegato()
Dim
Dim
Dim
Dim
myCommand As SqlClient.SqlCommand
myConnection As SqlClient.SqlConnection
myStrConn As String
mySQL As String
Query
127
Dim nroRecord As Integer
Try
‘ il testo del comando contiene
‘ un’istruzione SQL di inserimento record
mySQL = “INSERT INTO Impiegati (cognome,nome) “ & _
“VALUES(‘Padovano’,’Antonio’);” & _
“INSERT INTO Impiegati (cognome,nome) “ & _
“VALUES(‘Bellè’,’Paolo’)”
‘ creazione esplicita del comando
‘ e assegnazione della stringa SQL
myCommand = New SqlClient.SqlCommand(mySQL)
‘ creazione implicita del comando
‘myCommand= myConnection.CreateCommand()
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
myStrConn = BuildConnectionString()
myConnection.ConnectionString = myStrConn
myConnection.Open()
‘ passaggio della connessione attiva
myCommand.Connection = myConnection
‘ definizione del tipo di comando
myCommand.CommandType = CommandType.Text
‘ esegue il comando di scrittura e
‘ l’esito è assegnato alla variabile nroRecord
nroRecord = myCommand.ExecuteNonQuery()
Console.Write(“Nro record inseriti: “ & nroRecord)
‘ invoca il metodo Close per chiudere la connessione
myConnection.Close()
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“InserisciImpiegato()”)
End Try
End Function
La funzione InserisciImpiegato è sostanzialmente uguale alle altre già viste in
questo capitolo, commentiamo, quindi, solo le parti nuove. La proprietà CommandText contiene un’istruzione SQL che inserisce due record nella tabella Impiegati:
‘ il testo del comando contiene
‘ un’istruzione SQL di inserimento record
mySQL = “INSERT INTO Impiegati (cognome,nome) “ & _
“VALUES(‘Padovano’,’Antonio’);” & _
128
Capitolo 6
“INSERT INTO Impiegati (cognome,nome) “ & _
“VALUES(‘Bellè’,’Paolo’)”
Il metodo di esecuzione del comando è specifico per le istruzioni di scrittura e
il risultato dell’operazione è assegnato alla variabile numerica nroRecord:
‘ esegue il comando di scrittura e
‘ l’esito è assegnato alla variabile nroRecord
nroRecord = myCommand.ExecuteNonQuery()
Il numero di record inseriti, ovvero il valore della variabile nroRecord, è visualizzato nella console di output:
Console.Write(“Nro record inseriti: “ & nroRecord)
Possiamo provare ora a eseguire un’istruzione SQL di tipo DDL. Scriviamo una
funzione che crei la tabella Nipoti con le seguenticaratteristiche :
Tabella 6.3
Campo
Tipo (datatype)
Cognome
Nome
Città
varchar 30 caratteri
varchar 30 caratteri
varchar 30 caratteri
Di seguito, il codice della funzione CreaTabella:
Private Function CreaTabella()
Dim
Dim
Dim
Dim
Dim
myCommand As SqlClient.SqlCommand
myConnection As SqlClient.SqlConnection
Esito As Integer
myStrConn As String
mySQL As String
Try
‘ il testo del comando contiene
‘ un’istruzione SQL di tipo DDL
mySQL = “CREATE TABLE Nipoti “ & _
“(cognome VARCHAR(30), “ & _
“nome VARCHAR(30), “ & _
“città VARCHAR(30))”
‘ creazione esplicita del comando
‘ e assegnazione della stringa SQL
myCommand = New SqlClient.SqlCommand(mySQL)
‘ creazione implicita del comando
‘myCommand= myConnection.CreateCommand()
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
Query
129
myStrConn = BuildConnectionString2()
myConnection.ConnectionString = myStrConn
myConnection.Open()
‘ passaggio della connessione attiva
myCommand.Connection = myConnection
‘myCommand.Connection.Open()
‘ definizione del tipo di comando
myCommand.CommandType = CommandType.Text
‘ esegue il comando di creazione della tabella
‘ e l’esito è assegnato alla variabile Esito
Esito = myCommand.ExecuteNonQuery()
Console.Write(“Esito DDL: “ & Esito)
‘ invoca il metodo Close per chiudere la connessione
myConnection.Close()
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“CreaTabella()”)
End Try
End Function
Anche stavolta tralasciamo il codice che già conosciamo e analizziamo solo le
parti nuove. Cominciamo dall’istruzione SQL:
‘ il testo del comando contiene
‘ un’istruzione SQL di tipo DDL
mySQL = “CREATE TABLE Nipoti “ & _
“(cognome VARCHAR(30), “ & _
“nome VARCHAR(30), “ & _
“città VARCHAR(30))”
L’istruzione crea la tabella Nipoti composta dai campi cognome, nome e città;
tutti e tre i campi sono del tipo VARCHAR e hanno una lunghezza di 30 caratteri. Un campo dichiarato VARCHAR(30), a differenza di uno definito CHAR(30),
se occupato da una stringa di lunghezza inferiore a 30 caratteri occupa soltanto
il numero di byte effettivi della stringa. È consigliabile utilizzare il datatype
VARCHAR, quindi, nei casi in cui si prevede una grande variabilità nella popolazione di un campo.
Il metodo di esecuzione del comando è anche stavolta quello specifico per le
istruzioni di scrittura. Il risultato dell’operazione di creazione della tabella è
assegnato alla variabile numerica Esito:
‘ esegue il comando di creazione della tabella
‘ e l’esito è assegnato alla variabile Esito
Esito = myCommand.ExecuteNonQuery()
130
Capitolo 6
L’esito della creazione della tabella, ovvero il valore della variabile Esito, è
visualizzato nella console di output:
Console.Write(“Esito DDL: “ & Esito)
A conferma del buon esito dell’istruzione DDL il valore del codice di ritorno
deve essere –1, per cui nella console di output si dovrebbe leggere il seguente
messaggio:
Esito DDL: -1
Se ancora non vi fidate e volete avere la certezza di aver creato la tabella, potete
controllare usando il Server Explorer: troverete la tabella Nipoti con i campi
cognome, nome e città all’interno della sezione Tables del database Northwind.
Ricordiamo, comunque, che è alquanto improbabile che in un’applicazione
client-server interfacciata da utenti operativi si eseguano operazioni di tipo DDL.
Tipicamente queste sono demandate al DBA, che le esegue usando i tool specifici rilasciati dal DBMS su cui opera e solo per interventi strutturali sul database.
Query parametriche
Negli esempi di accesso ai dati che abbiamo visto finora non abbiamo mai
avuto bisogno di specificare l’insieme dei dati su cui intendevamo eseguire le
nostre operazioni. Abbiamo inserito dei record nella tabella Impiegati, ne abbiamo letto tutti i record, abbiamo creato la tabella Nipoti.
In particolare, negli esempi di operazioni di lettura non abbiamo avuto bisogno
di filtrare i dati da estrarre, così come non abbiamo avuto bisogno di specificare
su quali dati intendevamo intervenire negli esempi di operazioni di scrittura. La
realtà lavorativa è molto più varia. È molto frequente, infatti, che sia necessario
leggere o modificare solo alcuni record di una tabella, ponendo, cioè, dei filtri
alle query che si intendono eseguire verso il database e definendo dei criteri in
grado di delimitare il set di dati oggetto dell’operazione. Quest’esigenza si soddisfa generalmente apponendo delle clausole WHERE o HAVING alle istruzioni
SQL che si eseguono su un database. Nel capitolo 3 abbiamo visto vari esempi
di utilizzo di tali clausole, per cui in questo capitolo ne faremo uso senza
soffermarci troppo sul loro significato e sulle loro differenze. La costruzione
delle clausole WHERE o HAVING prevede la concatenazione di tre elementi: il
nome del campo da filtrare, un operatore logico di confronto e un valore di
confronto, che può essere anche il nome di un altro campo.
Non solo, quando abbiamo eseguito delle operazioni di inserimento di record
in una tabella, abbiamo scritto all’interno della query quelli che dovevano essere i valori che avrebbero dovuto assumere alcuni campi dei nuovi record. Abbiamo, cioè, composto la stringa di comando in modo che fosse specifica per
l’inserimento di quel determinato record. Se volessimo, ora, riciclare quel codice per inserire un record diverso, dovremmo riscrivere la stringa di SQL specificando valori differenti.
Query
131
Facciamo subito alcuni esempi. Riprendiamo la funzione InserisciImpiegato
e modifichiamo la stringa di comando per inserire un nuovo impiegato:
‘ istruzione SQL per l’inserimento di un nuovo record
mySQL = “INSERT INTO Impiegati (cognome,nome) “ & _
“VALUES(‘Menozzi’,’Alessandro’)”
Eseguiamo il codice. Il risultato è l’inserimento nella tabella Impiegati di un
nuovo record con i campi cognome e nome valorizzati. Notiamo che, per eseguire un’operazione pressoché identica alla precedente, dobbiamo modificare
parte del codice.
Facciamo un altro esempio. La tabella Impiegati possiede svariati campi che
nella precedente operazione di inserimento non abbiamo valorizzato. Ci proponiamo, quindi, in una successiva operazione di aggiornamento, di valorizzarne
alcuni. Creiamo, pertanto, la funzione AggiornaImpiegati che ci consente di
aggiornare i campi del record appena inserito. Copiamo il codice della funzione
InserisciImpiegato e sostituiamo la stringa di comando con la seguente:
‘ il testo del comando contiene
‘ un’istruzione SQL di modifica di un record
mySQL = “UPDATE Impiegati “ & _
“SET posizione = ‘collega’, “ & _
“città = ‘Zibello’, “ & _
“zona = ‘Pieveottoville’ “ & _
“WHERE cognome = ‘Menozzi’ “ & _
“AND nome = ‘Alessandro’”
Sostituiamo inoltre la scrittura del messaggio nella console con il seguente:
‘ visualizzazione del nro di record aggiornati
Console.Write(“Nro record aggiornati: “ & nroRecord)
Eseguiamo il codice. Vedremo nella console di output il seguente messggio:
Nro record aggiornati: 1
Il comando SQL è l’unico elemento di novità della funzione cha abbiamo appena creato. Analizziamone in dettaglio la struttura. Nella prima parte troviamo
l’istruzione UPDATE seguita dai campi oggetto della modifica, ognuno associato al valore che si vuole che il campo assuma. Nella seconda parte sono specificate due clausole WHERE legate dall’operatore logico AND, una relativa al
campo cognome, l’altra relativa al campo nome. Il comando, quindi, chiede al
DBMS di riferimento di modificare tutti i record della tabella Impiegati in cui il
campo cognome contiene il valore ‘Menozzi’ e il campo nome il valore ‘Alessandro’, scrivendo il valore ‘collega’ nel campo posizione, il valore ‘Zibello’ nel
campo città e il valore ‘Pieveottoville’ nel campo zona.
Le funzioni InserisciImpiegato e AggiornaImpiegati che abbiamo appena
visto sono due pessimi esempi di scrittura di codice. Sono, infatti, costruite su
misura per l’inserimento e la modifica di un record con specifici valori. Se
volessimo utilizzare la stessa struttura per trattare record con valori differenti,
132
Capitolo 6
dovremmo riscriverle modificando la stringa SQL. Ovviamente, avendo gli esempi
una valenza prettamente didattica, ci sentiamo in un certo senso perdonati, ma
per fare ammenda ci proponiamo di suggerire delle modalità più dinamiche per
strutturare il codice.
Ad esempio, potremmo rendere più dinamica l’interazione tra codice e database separando la parte di codice dedicata all’accesso alla fonte dati dalla parte di
logica applicativa, che determina il contenuto della stringa di comando e decide l’accesso ai dati. Modifichiamo, pertanto, la precedente funzione AggiornaImpiegati e creiamo la funzione EseguiWrite specifica per l’esecuzione della
query di scrittura:
Private Function EseguiWrite( _
ByVal pSQL As String) As Integer
Dim
Dim
Dim
Dim
myCommand As
myConnection
nroRecord As
myStrConn As
SqlClient.SqlCommand
As SqlClient.SqlConnection
Integer
String
Try
‘ creazione esplicita del comando
‘ e assegnazione della stringa SQL
myCommand = New SqlClient.SqlCommand(pSQL)
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
myStrConn = BuildConnectionString()
myConnection.ConnectionString = myStrConn
myConnection.Open()
‘ passaggio della connessione attiva
‘ all’oggetto Command
myCommand.Connection = myConnection
‘ definizione del tipo di comando
myCommand.CommandType = CommandType.Text
‘ esecuzione del comando di scrittura
EseguiWrite = myCommand.ExecuteNonQuery()
‘ invoca il metodo Close per chiudere la connessione
myConnection.Close()
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“EseguiWrite()”)
End Try
End Function
Query
133
Private Sub AggiornaImpiegati()
Dim nroRecord As Integer
Dim mySQL As String
Try
‘ il testo del comando contiene
‘ un’istruzione SQL di modifica di un record
mySQL = “UPDATE Impiegati “ & _
“SET posizione = ‘collega’, “ & _
“città = ‘Zibello’, “ & _
“zona = ‘Pieveottoville’ “ & _
“WHERE cognome = ‘Menozzi’ “ & _
“AND nome = ‘Alessandro’”
‘ chiamata della funzione che esegue
‘ l’operazione di scrittura
nroRecord = EseguiWrite(mySQL)
MessageBox.Show(“Nro Record aggiornati: “ & _
nroRecord, “AggiornaImpiegati()”)
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“AggiornaImpiegati()”)
End Try
End Sub
La funzione EseguiWrite ha come parametro di input la stringa pSQL destinata
a contenere il comando SQL. La funzione contiene tutti i comandi specifici
dell’accesso ai dati. Il comando di esecuzione della query è eseguito invocando
il metodo ExecuteNonQuery e l’esito del comando, ovvero il numero di record
aggiornati, è asssegnato alla funzione stessa.
‘ esecuzione del comando di scrittura
EseguiWrite = myCommand.ExecuteNonQuery()
Nella funzione AggiornaImpiegati, invece, è scomparso ogni riferimento al
codice specifico dell’accesso ai dati. Rimane solo la scrittura della stringa SQL
nella variabile mySQL. L’esecuzione del comando è demandata alla funzione
EseguiWrite chiamata trasferendo la variabile mySQL come parametro di input.
L’esito dell’operazione è comunicato alla procedura chiamante che provvede a
registrare il valore nella variabile nroRecord.
‘ chiamata della funzione che esegue
‘ l’operazione di scrittura
nroRecord = EseguiWrite(mySQL)
134
Capitolo 6
Per ultimo, la funzione AggiornaImpiegati segnala il numero di record aggiornati mediante un messaggio che visualizza la variabile nroRecord:
MessageBox.Show(“Nro Record aggiornati: “ & _
nroRecord, “AggiornaImpiegati()”)
La funzione EseguiWrite può essere chiamata da qualsiasi procedura dell’applicazione purché sia passato come parametro di input un comando SQL sintatticamente corretto. L’onere della definizione del comando è comunque demandato alla procedura chiamante, che deve comporre in modo dinamico una
stringa di comando da passare alla funzione che eseguirà l’accesso ai dati. È
senz’altro un passo avanti, ma si può fare di meglio.
Un altro modo per rendere più dinamica la scrittura di codice di accesso ai dati
è l’utilizzo di query parametriche. In una query parametrica, al posto del valore
dei campi o dei filtri, è indicato il nome di un parametro. Facciamo subito un
esempio. L’istruzione SQL di inserimento di un record vista prima nella funzione AggiornaImpiegati diventa:
‘ istruzione SQL parametrica
mySQL = “UPDATE Impiegati “ & _
“SET posizione = @posizione, “ & _
“città = @città, “ & _
“zona = @zona “ & _
“WHERE cognome = @cognome “ & _
“AND nome = @nome”
Come si vede, al posto delle stringhe contenenti i valori dei campi posizione,
città e zona che si vogliono aggiornare e al posto del valore dei filtri sui campi
cognome e nome, si trovano dei nomi di variabili preceduti dal simbolo @. Le
variabili in questione sono i parametri della query. Prima di fare ulteriori esempi di scrittura di query parametriche, però, è necessario spiegare il significato e
il funzionamento dei parametri all’interno dell’oggetto Command.
Proprietà dell’oggetto Command:
la collection dei parametri
L’oggetto Command possiede la proprietà Parameters. È una proprietà di tipo
collection e può essere popolata con oggetti di tipo Parameter. L’oggetto
Parameter, quindi, è il componente elementare della collection dei parametri e
delle query parametriche. Vediamo quali sono le sue proprietà:
Tabella 6.4
Proprietà
Descrizione
ParameterName
È il nome del parametro.
È il datatype generico del parametro; può essere scelto tra uno di quelli
specificati dall’elenco del framework .NET System.Data.DbType.
DbType
Query
135
Proprietà
Descrizione
OleDbType
È il datatype specifico del data provider .NET; le proprietà DbType e
OleDbType sono correlate internamente, per cui se si cambia il valore
della prima si aggiorna automaticamente il valore della seconda.
È il valore che si assegna al parametro.
Indica se il parametro è di input, di output o se è un parametro di
ritorno di una stored procedure.
Indica se il parametro può accettare valore nullo.
Indica la grandezza massima in byte del parametro.
In caso di parametri numerici, può indicare il numero massimo di cifre
intere.
In caso di parametri numerici, può indicare il numero massimo di cifre
decimali.
Value
Direction
IsNullable
Size
Precision
Scale
La proprietà Parameters possiede dei metodi e delle proprietà comuni a tutte le
collection di Visual Basic. In particolare, possiede il metodo Add, attraverso
cui è possibile aggiungere un nuovo Item (nel nostro caso di tipo Parameter);
possiede ovviamente la poprietà Item, che consente di referenziare un particolare elemento della collection; possiede i metodi Insert e Remove, attraverso
i quali è possibile inserire o rimuovere un Item preciso dalla collection, e il
metodo Clear, che consente di svuotare rapidamente l’intera collection. Facciamo alcuni esempi. Un oggetto parametro si istanzia in questo modo:
Dim myParameter As SqlClient.SqlParameter
‘ creazione esplicita
myParameter = New SqlClient.SqlParameter()
oppure, avendo importato il namespace, più semplicemente:
Dim myParameter As SqlParameter
‘ creazione esplicita
myParameter = New SqlParameter()
Si può istanziare un oggetto Parameter anche in modo implicito, usando il
metodo CreateParameter dell’oggetto Command:
Dim myCommand As SqlCommand
Dim myParameter As SqlParameter
myCommand = New SqlCommand()
‘ creazione implicita
myParameter = myCommand.CreateParameter()
L’aggiunta di un parametro alla collection dei parametri, ovvero alla proprietà
Parameters, è molto semplice; basta invocare il metodo Add passando l’oggetto
Parameter:
Dim myCommand As SqlCommand
Dim myParameter As SqlParameter
myCommand = New SqlCommand()
136
Capitolo 6
myParameter = New SqlParameter()
‘ aggiunge un parametro in coda all’elenco
myCommand.Parameters.Add(myParameter)
Si può aggiungere un parametro alla collection specificando l’esatta posizione
in cui inserirlo usando il metodo Insert. Nell’esempio che segue si inserisce un
parametro nella posizione 0 della collection, ovvero in cima all’elenco:
Dim myCommand As SqlCommand
Dim myParameter As SqlParameter
myCommand = New SqlCommand()
myParameter = New SqlParameter()
‘ aggiunge un parametro in cima all’elenco
myCommand.Parameters.Insert(0, myParameter)
Per rimuovere il parametro di cui si conosce l’esatta posizione si può usare il
metodo RemoveAt:
Dim myCommand As SqlCommand
Dim myParameter As SqlParameter
myCommand = New SqlCommand()
myParameter = New SqlParameter()
‘ aggiunge un parametro in cima all’elenco
myCommand.Parameters.Insert(0, myParameter)
‘ rimuove il primo parametro della lista
myCommand.Parameters.RemoveAt(0)
Per svuotare la collection dei parametri è sufficiente invocare il metodo Clear:
Dim myCommand As SqlCommand
myCommand = New SqlCommand()
‘ svuota l’intera collection dei parametri
myCommand.Parameters.Clear()
Ora che abbiamo le idee un po’ più chiare sulle proprietà dell’oggetto Parameter e sul funzionamento della collection Parameters possiamo vedere come
sfruttarne le potenzialità per gestire in modo efficiente l’accesso ai dati. Possiamo, infatti, strutturare il codice separando la parte di definizione della query
dalla parte specifica di connessione ed esecuzione del comando. Per quanto
riguarda la parte di definizione della query, possiamo evitare di riscrivere il
testo del comando SQL, in quanto, grazie alle query parametriche, possiamo
limitarci a passare all’oggetto Command l’elenco dei parametri richiesti dalla query specificando il valore di ognuno di essi.
Private Function AggiornaImpiegatiParametrica()
Dim nroRecord As Integer
Dim mySQL As String
Dim myCommand As SqlClient.SqlCommand
Try
Query
‘ il testo del comando contiene
‘ un’istruzione SQL di inserimento record
mySQL = “UPDATE Impiegati “ & _
“SET posizione = @posizione, “ & _
“città = @città, “ & _
“zona = @zona “ & _
“WHERE cognome = @cognome “ & _
“AND nome = @nome”
myCommand = New SqlClient.SqlCommand()
‘ invoca la routine che aggiunge un parametro alla
‘ collection dei parametri dell’oggetto myCommand
AggiungiParametro(myCommand, “@posizione”, & _
SqlDbType.VarChar, “collega”)
AggiungiParametro(myCommand, “@città”, & _
SqlDbType.VarChar, “Zibello”)
AggiungiParametro(myCommand, “@zona”, & _
SqlDbType.VarChar, “Pieveottoville”)
AggiungiParametro(myCommand, “@cognome”, & _
SqlDbType.VarChar, “Menozzi”)
AggiungiParametro(myCommand, “@nome”, & _
SqlDbType.VarChar, “Alessandro”)
‘ invocazione della funzione che esegue la query
nroRecord = EseguiWriteParametrica(mySQL, myCommand)
MessageBox.Show(“Nro Record aggiornati: “ & _
nroRecord, “ AggiornaImpiegatiParametrica()”)
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“ AggiornaImpiegatiParametrica()”)
End Try
End Function
Private
ByRef
ByVal
ByVal
ByVal
Sub AggiungiParametro( _
pCommand As SqlClient.SqlCommand, _
pNomeParametro As String, _
pDbType As SqlDbType, _
pValore As String)
Dim myParameter As SqlClient.SqlParameter
Try
‘ creazione implicita del parametro
myParameter = pCommand.CreateParameter()
‘ valorizzazione delle proprietà del parametro
myParameter.ParameterName = pNomeParametro
137
138
Capitolo 6
myParameter.SqlDbType = pDbType
myParameter.Value = pValore
‘ aggiunge il parametro alla collection
pCommand.Parameters.Add(myParameter)
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“AggiungiParametro()”)
End Try
End Sub
Private Function EseguiWriteParametrica( _
ByVal pSQL As String, _
ByVal pCommand As SqlClient.SqlCommand) As Integer
Dim myConnection As SqlClient.SqlConnection
Dim nroRecord As Integer
Dim myStrConn As String
Try
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
myStrConn = BuildConnectionString()
myConnection.ConnectionString = myStrConn
myConnection.Open()
‘ passaggio della connessione attiva
‘ all’oggetto Command
pCommand.Connection = myConnection
‘ definizione del tipo di comando
pCommand.CommandType = CommandType.Text
‘ assegnazione della stringa comando
pCommand.CommandText = pSQL
‘ esegue il comando di scrittura
EseguiWriteParametrica = pCommand.ExecuteNonQuery()
‘ invoca il metodo Close per chiudere la connessione
myConnection.Close()
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“EseguiWriteParametrica()”)
End Try
End Function
Query
139
La funzione AggiornaImpiegatiParametrica compone la stringa della query
parametrica, istanzia l’oggetto myCommand e invoca la routine AggiungiParametro per ogni parametro richiesto dalla query.
‘ invoca la routine che aggiunge un parametro alla
‘ collection dei parametri dell’oggetto myCommand
AggiungiParametro(myCommand, “@posizione”, & _
SqlDbType.VarChar, “collega”)
La routine AggiungiParametro riceve come parametri l’oggetto Command destinato a eseguire la query parametrica e le informazioni necessarie a valorizzare le
proprietà del parametro: il nome (la stringa pNomeParametro), il tipo (la variabile pDbType di tipo SqlDbType) e il valore (la stringa pValore). Si limita a istanziare un nuovo oggetto di tipo parametro invocando il metodo CreateParameter
dell’oggetto pCommand:
‘ creazione implicita del parametro
myParameter = pCommand.CreateParameter()
valorizza alcune proprietà del parametro appena creato:
‘ valorizzazione delle proprietà del parametro
myParameter.ParameterName = pNomeParametro
myParameter.SqlDbType = pDbType
myParameter.Value = pValore
aggiunge il parametro alla collection dei parametri dell’oggetto pCommand:
‘ aggiunge il parametro alla collection
pCommand.Parameters.Add(myParameter)
La funzione AggiungiParametro è invocata per ogni parametro richiesto dalla
query parametrica. A questo punto la collection dei parametri è completa e la
query può essere eseguita. Per eseguirla si invoca la funzione EseguiWriteParametrica che resti-tuisce il numero di record interessati dalla query di scrittura.
‘ invocazione della funzione che esegue la query
nroRecord = EseguiWriteParametrica(mySQL, myCommand)
La funzione EseguiWriteParametrica si limita a eseguire il comando di accesso
ai dati, preoccupandosi di gestire la connessione. Riceve il testo del comando
SQL nel parametro pSQL e l’oggetto comando nel parametro pCommand:
‘ assegnazione della stringa di comando
pCommand.CommandText = pSQL
Il comando eseguito è di scrittura, infatti il metodo invocato è ExecuteNonQuery:
‘ esegue il comando di scrittura
EseguiWriteParametrica = pCommand.ExecuteNonQuery()
140
Capitolo 6
Può essere utile scrivere una funzione specifica per l’esecuzione di query di
lettura, da usare negli esempi che proporremo nel prosieguo del libro. Per
coerenza con la funzione di scrittura la chiameremo EseguiReadParametrica.
Private Function EseguiReadParametrica( _
ByVal pSQL As String, _
ByVal pCommand As SqlClient.SqlCommand) _
As SqlClient.SqlDataReader
Dim myConnection As SqlClient.SqlConnection
Dim nroRecord As Integer
Dim myStrConn As String
Try
‘ creazione esplicita della connessione
myConnection = New SqlClient.SqlConnection()
myStrConn = BuildConnectionString()
myConnection.ConnectionString = myStrConn
myConnection.Open()
‘ passaggio della connessione attiva
‘ all’oggetto Command
pCommand.Connection = myConnection
‘ definizione del tipo di comando
pCommand.CommandType = CommandType.Text
‘ assegnazione della stringa comando
pCommand.CommandText = pSQL
‘ esegue il comando di lettura
EseguiReadParametrica = pCommand.ExecuteReader
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“EseguiReadParametrica()”)
End Try
End Function
Vediamo quali sono le differenze rispetto alla precedente funzione EseguiWriteParametrica. Innanzitutto il valore restituito non è un semplice valore numerico, ma un oggetto complesso di tipo SqlDataReader. Inoltre, il metodo di esecuzione del comando è quello specifico delle query di lettura, ovvero il metodo
ExecuteReader. Un’ultima cosa: la connessione è lasciata attiva perché la routine chiamante deve avere la possibilità di leggere l’oggetto DataReader, se si
chiude la connessione, infatti, la lettura non è più possibile. Per il resto non è
cambiato nulla.
Query
141
Possibilità di query
Nei paragrafi precedenti abbiamo visto come usare la tecnologia .NET per eseguire dei comandi di lettura o di scrittura verso un database. In questo paragrafo, invece, ci proponiamo di fornire qualche esempio complesso di accesso ai
dati per mettere alla prova le nozioni di SQL che abbiamo appreso nel corso del
Capitolo 3.
Abbiamo classificato le operazioni di accesso ai dati facendo una prima distinzione tra operazioni di scrittura e operazioni di lettura. Queste ultime, poi, le
abbiamo ulteriormente suddivise tra lettura di dettaglio e lettura di sintesi. Nell’esempio che segue proponiamo la lettura di sintesi dei dati contenuti su più
tabelle del database Northwind. In particolare, vogliamo conoscere il numero
di ordini e il fatturato complessivo di ogni impiegato.
Le tabelle interessate dall’estrazione sono: la tabella Ordini, la tabella Dettagli
Ordini e la tabella Impiegati. Prima di scrivere la query che estrae i dati vediamo quali sono le informazioni che ci interessano e quali sono le relazioni che
intercorrono tra le varie tabelle. La tabella Ordini e la tabella Dettagli Ordini
sono legate da una relazione uno a molti: a ogni record della tabella Ordini,
infatti, corrispondono uno o più record della tabella Dettagli Ordini. La tabella
Ordini contiene le informazioni specifiche dell’oggetto ordine, ad esempio, la
data dell’ordine, il codice del cliente, il codice dell’impiegato, la data di spedizione, mentre la tabella Dettagli Ordini contiene le informazioni specifiche
dell’oggetto riga d’ordine, ovvero dei prodotti venduti nell’ordine, ad esempio,
il codice del prodotto, il prezzo, la quantità venduta e lo sconto praticato. La
tabella Impiegati contiene, oltre al codice, le informazioni anagrafiche dei vari
impiegati.
Per estrarre le informazioni di cui abbiamo bisogno dobbiamo mettere in join le
tre tabelle: colleghiamo la tabella Ordini con la tabella Dettagli Ordini attraverso il campo idordine e colleghiamo la tabella Ordini con la tabella Impiegati
attraverso il campo idimpiegato. Per ottenere dei dati di sintesi è necessario
raggruppare i record estratti e aggregare i dati numerici mediante le funzioni di
conteggio e di somma. Vediamo il codice SQL della query:
SELECT
Impiegati.cognome, Impiegati.nome,
COUNT(DISTINCT [Dettagli ordini].idordine) AS nr_ordini,
SUM(([Dettagli Ordini].prezzounitario *
[Dettagli Ordini].quantità) *
(1 - [Dettagli Ordini].sconto)) AS importo
FROM Ordini, [Dettagli Ordini], Impiegati
WHERE
Ordini.idordine = [Dettagli Ordini].idordine AND
Ordini.idimpiegato = Impiegati.idimpiegato
GROUP BY
Impiegati.cognome, Impiegati.nome
Analizziamo nel dettaglio la query appena vista. Le dimensioni di analisi, ovvero le informazioni in cui si vuole articolare la sintesi, sono i campi cognome e
nome; le misure dell’analisi, ovvero le informazioni quantitative di sintesi, sono
142
Capitolo 6
il conteggio degli ordini e la somma degli importi dei vari ordini. Il campo
nr_ordini è ottenuto mediante la funzione COUNT. Si noti che, se non avessimo specificato l’opzione DISTINCT, la funzione COUNT non avrebbe restituito
il numero di ordini, ma il numero di righe d’ordine di ogni impiegato. Il campo
importo, invece, è ottenuto moltiplicando la quantità ordinata (il campo quantità) per il prezzo di vendita (il campo prezzounitario) e sottraendo lo sconto,
determinato come fattore di riduzione (1 – sconto).
Possiamo scrivere una routine che esegue la query.
Private Sub OrdiniPerImpiegato()
Dim mySQL As String
Dim myCommand As SqlClient.SqlCommand
Dim myDataReader As SqlClient.SqlDataReader
Try
‘ il testo del comando contiene
‘ un’istruzione SQL di lettura di dati aggregati
mySQL = “SELECT “ & _
“Impiegati.cognome, Impiegati.nome, “ & _
“COUNT(DISTINCT [Dettagli ordini].idordine) “ & _
“AS nr_ordini, “ & _
“SUM(([Dettagli Ordini].prezzounitario * “ & _
“[Dettagli Ordini].quantità) * “ & _
“(1 - [Dettagli Ordini].sconto)) AS importo “ & _
“FROM Ordini, [Dettagli Ordini], Impiegati “ & _
“WHERE “ & _
“Ordini.idordine = [Dettagli Ordini].idordine “ & _
“AND “ & _
“Ordini.idimpiegato = Impiegati.idimpiegato “ & _
“GROUP BY “ & _
“Impiegati.cognome, Impiegati.nome”
myCommand = New SqlClient.SqlCommand()
myDataReader = _
EseguiReadParametrica(mySQL, myCommand)
With myDataReader
‘ legge una riga alla volta
‘ fino alla fine dei dati estratti
Do While .Read = True
Console.Write(.Item(“cognome”) &
Console.Write(.Item(“nome”) & “,
Console.Write(.Item(“nr_ordini”)
Console.Write(.Item(“importo”) &
Loop
End With
‘ la connessione può essere chiusa
‘ solo dopo aver letto il DataReader
myCommand.Connection.Close()
“, “)
“)
& “, “)
Chr(13))
Query
143
Catch thisExcept As Exception
‘ se il tentativo di connessione fallisce
‘ viene visualizzata l’eccezione
MessageBox.Show(thisExcept.Message, _
“OrdiniPerImpiegato()”)
End Try
End Sub
Facciamo un altro esempio. Supponiamo di voler interrogare il database Northwind per estrarre gli ordini il cui importo di vendita sia superiore alla media.
Potremmo, con una prima query, calcolare il valore della media degli ordini e
poi, con una seconda query, estrarre i dati dei soli ordini il cui importo è
maggiore del valore della media precedentemente estratta. In alternativa, possiamo ottenere lo stesso risultato con una sola query, che utilizza la clausola
HAVING sull’importo di vendita, associata a una SUBQUERY che estrae il valore
medio dell’importo degli ordini. Vediamo il codice SQL:
SELECT Ordini.idordine,
SUM(([Dettagli ordini].prezzounitario *
[Dettagli ordini].quantità) *
(1 - [Dettagli ordini].sconto)) AS importo
FROM Ordini, [Dettagli ordini]
WHERE
Ordini.idordine = [Dettagli ordini].idordine
GROUP BY Ordini.idordine
HAVING
(SUM(([Dettagli ordini].prezzounitario *
[Dettagli ordini].quantità) *
(1 - [Dettagli ordini].sconto)) >
(SELECT
AVG(([Dettagli ordini].prezzounitario *
[Dettagli ordini].quantità) *
(1 - [Dettagli ordini].sconto)) AS media
FROM [Dettagli ordini] , Ordini
WHERE
[Dettagli ordini].idordine = Ordini.idordine))
Facciamo a meno di scrivere una routine specifica per l’esecuzione della query
e lasciamo al lettore l’esercizio di adattare allo scopo il codice delle funzioni già
viste. Commentiamo, invece, la struttura della query. Nella prima parte abbiamo una semplice clausola SELECT, che estrae il numero d’ordine (il campo
idordine) e l’importo dell’ordine (il campo importo, calcolato con le modalità
che abbiamo visto nella query precedente); la clausola WHERE è utilizzata per
mettere in join le tabelle interessate dalla query (la tabella Ordini e la tabella
Dettagli Ordini); la clausola GROUP BY elenca i campi in cui saranno raggruppati i valori estratti dalla query, nel nostro caso il solo campo idordine; infine,
abbiamo la clausola HAVING che consente di filtrare i dati raggruppati dalla
query scegliendo di estrarre solo gli ordini il cui importo supera il valore estratto da un’ulteriore query; l’ultima query, infatti, ha il solo scopo di restituire il
valore dell’importo medio degli ordini attraverso la funzione AVG, valore che
144
Capitolo 6
sarà confrontato con l’importo dell’ordine per selezionare quelli da restituire al
richiedente.
Facciamo un altro esempio. La fonte dati di riferimento è ancora il database
Northwind. Supponiamo di voler conoscere il fatturato ottenuto nelle varie nazioni e, per ogni nazione, vogliamo conoscere il contributo fornito da ogni
impiegato. Potremmo estrarre i dati aggregati per nazione e impiegato, esportarli in un foglio Excel ed elaborarli con gli strumenti specifici del foglio di
calcolo, per ottenere le aggregazioni e le elaborazioni che desideriamo. Purtroppo, dobbiamo realizzare applicazioni facilmente fruibili e fornire ai nostri
utenti dati già elaborati e organizzati secondo un layout immediatamente comprensibile. Fortunatamente abbiamo a disposizione gli strumenti SQL, che ci
consentono con una sola query di estrarre i dati elaborati e organizzati in modo
da fornirci le informazioni che desideriamo. Vediamo la query:
SELECT
Dettaglio.PaeseDestinatario,
Dettaglio.cognome, Dettaglio.nome,
Dettaglio.importo,
Sintesi.imp_totale
ROUND(
(Dettaglio.importo / Sintesi.imp_totale * 100), 2
) as percentuale
FROM
(SELECT
paesedestinatario,
Impiegati.cognome, Impiegati.nome,
SUM(([Dettagli Ordini].prezzounitario *
[Dettagli Ordini].quantità) *
(1 - [Dettagli Ordini].sconto)) AS importo
FROM Ordini, [Dettagli Ordini], Impiegati
WHERE
Ordini.idordine = [Dettagli Ordini].idordine AND
Ordini.idimpiegato = Impiegati.idimpiegato
GROUP BY PaeseDestinatario,
Impiegati.cognome, Impiegati.nome) Dettaglio,
(SELECT
paesedestinatario,
SUM(([Dettagli Ordini].prezzounitario *
[Dettagli Ordini].quantità) *
(1 - [Dettagli Ordini].sconto)) AS imp_totale
FROM Ordini, [Dettagli Ordini]
WHERE
Ordini.idordine = [Dettagli Ordini].idordine
GROUP BY PaeseDestinatario) Sintesi
where
Dettaglio.paesedestinatario = Sintesi. paesedestinatario
Anche stavolta lasciamo al lettore l’esercizio di scrivere il codice per eseguire la
query. Commentiamone, invece, la struttura. La query è composta da due subquery: la prima, che abbiamo chiamato Dettaglio, estrae il fatturato per impiegato e nazione di destinazione; la seconda, che abbiamo chiamato Sintesi, estrae,
invece, il fatturato raggruppato soltanto per nazione. Nelle tabelle che seguono
possiamo vedere il contenuto delle due subquery.
Query
145
Tabella 6.5
Subquery Dettaglio
paesedestinatario
cognome
nome
importo
Argentina
Argentina
Argentina
Argentina
Argentina
Argentina
Argentina
Argentina
…
Callahan
Davolio
Dodsworth
Fuller
King
Leverling
Peacock
Suyama
…
Laura
Nancy
Anne
Andrew
Robert
Janet
Margaret
Michael
…
4125750
1030050
1416750
715500
2303700
478800
1994100
114000
…
Tabella 6.6
Subquery Sintesi
Paesedestinatario
imp_totale
Argentina
Austria
Belgio
Brasile
Canada
Danimarca
…
12178650
192005757,75
50737282,5
160388664,75
75294435
48991533,75
…
La query principale mette in join le due subquery estraendo la nazione (il
campo paesedestinatario), l’impiegato (i campi nome e cognome), il fatturato
realizzato da ogni impiegato per nazione (il campo importo), il fatturato totale
realizzato per nazione (il campo imp_totale) e la percentuale del fatturato realizzato dall’impiegato per nazione sul totale della nazione (il campo percentuale). Si noti che abbiamo arrotondato il valore del campo percentuale alla seconda cifra decimale usando la funzione ROUND. Vediamo il contenuto della query complessiva:
Tabella 6.7
paesedestinatario
cognome
nome
importo
imp_totale
percentuale
Argentina
Argentina
Argentina
Argentina
Argentina
Callahan
Davolio
Dodsworth
Fuller
King
Laura
Nancy
Anne
Andrew
Robert
4125750
1030050
1416750
715500
2303700
12178650
12178650
12178650
12178650
12178650
33,88
8,46
11,63
5,88
18,92
(continua)
146
Capitolo 6
Tabella 6.7
(seguito)
paesedestinatario
cognome
nome
importo
imp_totale
percentuale
Argentina
Argentina
Argentina
…
Leverling
Peacock
Suyama
…
Janet
Margaret
Michael
…
478800
1994100
114000
…
12178650
12178650
12178650
…
3,93
16,37
0,94
…
Dagli esempi visti in questo capitolo si intuisce che le possibilità di query offerte dal linguaggio SQL standard sono veramente enormi. Si tenga conto, inoltre,
che i DBMS più sofisticati, con le ultime versioni dei loro prodotti, forniscono
funzioni di ricerca e di aggregazione sempre più avanzate. L’invito, quindi, è
quello di non limitarsi all’uso delle funzionalità più comuni dell’SQL, ma di
esplorare anche le funzionalità avanzate fornite dal DBMS, pur tenendo conto
delle possibili incompatibilità con gli altri DBMS in caso di migrazione del
database.