9. Accesso al database
9.1. Connettore ADO.NET
Diamo un'altra occhiata all'architettura a livelli utilizzata in diverse occasioni
![]() |
Negli esempi studiati, il livello [dao] ha finora sfruttato due tipi di origine dati:
- dati hard-coded
- dati provenienti da file di testo
In questo capitolo, studiamo il caso in cui i dati provengono da un database. L'architettura a 3 livelli si evolve quindi in un'architettura multistrato. Esistono vari tipi di architettura multistrato. Studieremo i concetti di base con quanto segue:
![]() |
Nel diagramma sopra riportato, il livello [dao] [1] interagisce con il SGBD [3] tramite una libreria di classi specifica per il SGBD utilizzato e fornita insieme ad esso. Questo livello implementa le funzioni standard note come ADO (Active X Data Objects). Un livello di questo tipo viene chiamato provider (in questo caso, provider di accesso al database) o anche connettore. La maggior parte degli SGBD dispone ora di un connettore ADO.NET, cosa che non avveniva agli albori della piattaforma .NET. I connettori .NET non offrono un'interfaccia standard al livello [dao], pertanto quest'ultimo presenta i nomi delle classi del connettore nel proprio codice. Se si cambia l'SGBD, si cambiano il connettore e le classi, e si deve modificare il livello [dao] . Questo è sia efficiente, poiché il connettore .NET, essendo stato scritto per un particolare SGBD, sa come utilizzarlo al meglio, sia rigido, poiché cambiare l'SGBD significa cambiare il livello [dao]. Questo secondo argomento va relativizzato: le aziende non cambiano il proprio SGBD molto spesso. Vedremo in seguito che, a partire dalla versione 2.0 di .NET, esiste un connettore generico che offre flessibilità senza sacrificare le prestazioni.
9.2. I due modi di utilizzare un'origine dati
La piattaforma .NET consente di sfruttare un'origine dati in due modi diversi:
- modalità connessa
- modalità offline
In modalità connessa, l'applicazione
- apre una connessione alla fonte dati
- opera con la fonte dati in lettura/scrittura
- chiude la connessione
In modalità offline, l'applicazione
- apre una connessione alla fonte dati
- ottiene una copia in memoria di tutti o parte dei dati di origine
- chiude la connessione
- lavora con la copia in memoria dei dati di lettura/scrittura
- al termine del lavoro, apre una connessione, invia i dati modificati alla fonte dati affinché vengano presi in considerazione, chiude la connessione
Qui studieremo solo la modalità connessa.
9.3. Concetti di base sul funzionamento dei database
Dimostreremo i concetti principali dell'utilizzo dei database con un database SQL Server Compact 3.5. Questo SGBD è fornito con Visual Studio Express. Si tratta di un SGBD leggero in grado di gestire un solo utente alla volta. È tuttavia sufficiente per introdurre alla programmazione dei database. In un secondo momento, presenteremo altri SGBD.
L'architettura utilizzata sarà la seguente:
![]() |
Un'applicazione console [1] gestirà un database SqlServer Compact [3,4] tramite il connettore Ado.Net di questo SGBD [2].
9.3.1. Visita il database di esempio
Creeremo il database direttamente in Visual Studio Express. A tal fine, creeremo un nuovo progetto di tipo console.
![]() |
- [1]: il progetto
- [2]: si apre la vista "Database Explorer"
- [3]: crea una nuova connessione
![]() |
- [4]: seleziona il tipo di SGBD
- [5,6]: selezionare SGBD SQL Server Compact
- [7]: crea il database
- [8]: un database SQL Server Compact è contenuto in un unico file .sdf. Indichiamo dove crearlo, in questo caso nella cartella del progetto C#.
- [9]: al nuovo database è stato assegnato il nome [dbarticles.sdf]
- [10]: è stata selezionata la lingua francese. Ciò ha un impatto sulle operazioni di ordinamento.
- [11,12]: il database può essere protetto da password. Qui "dbarticles".
- [13]: confermare la pagina delle informazioni. Il database è ora fisicamente creato:
![]() |
- [14]: il nome del database appena creato
- [15]: seleziona l'opzione "Salva la mia password" in modo da non doverla digitare ogni volta
- [16]: verifica la connessione
- [17]: tutto a posto
- [18]: convalida la pagina delle informazioni
- [19]: la connessione appare nell'esploratore del database
- [20]: per il momento, il database non ha tabelle. Creiamone una. Un articolo avrà i seguenti campi:
- id : un identificatore univoco - chiave primaria
- nome: nome dell'articolo - univoco
- prezzo: prezzo dell'articolo
- stockattuale: la sua disponibilità attuale
- stockminimo: il livello minimo di scorte al di sotto del quale è necessario rifornire l'articolo
![]() |
- [21]: il campo [id] è di tipo intero ed è la chiave primaria della tabella [22].
- [23]: questa chiave primaria è di tipo Identity. Questo concetto, specifico dei sistemi di gestione di database (SGBD) SQL Server, indica che la chiave primaria sarà generata dal sistema stesso. In questo caso la chiave primaria sarà un numero intero che parte da 1 e viene incrementato di 1 per ogni nuova chiave.
![]() |
- [24]: vengono creati gli altri campi. Si noti che il campo [name] ha un vincolo di unicità di tipo " " [25].
- [26]: viene assegnato un nome alla tabella
- [27]: una volta convalidata la struttura della tabella, questa appare nel database.
![]() |
- [28]: richiesta di visualizzazione del contenuto della tabella
- [29]: attualmente vuota
- [30]: questa viene compilata con alcuni dati. Una riga viene convalidata non appena viene inserita la riga successiva. Il campo [id] non viene inserito: viene generato automaticamente quando la riga viene convalidata.
Ora dobbiamo configurare il progetto in modo che questo database, attualmente nella directory principale del progetto, venga copiato automaticamente nella cartella di esecuzione del progetto:
![]() |
- [1]: richiesta di visualizzazione di tutti i file
- [2]: appare la base [dbarticles.sdf]
- [3]: lo includiamo nel progetto
![]() |
- [4]: l'operazione di aggiunta di un'origine dati a un progetto avvia una procedura guidata di cui qui non abbiamo bisogno [5].
- [6]: la base fa ora parte del progetto. Torniamo alla modalità normale [7].
- [8]: il progetto con il suo database
- [9]: nelle proprietà del database, possiamo vedere [10] che il database verrà automaticamente copiato nella cartella di esecuzione del progetto. È lì che il programma che stiamo per scrivere lo cercherà.
Ora che abbiamo un database a disposizione, potremo sfruttarlo. Per prima cosa, diamo un'occhiata a SQL.
9.3.2. I quattro comandi di base del linguaggio SQL
SQL (Structured Query Language) è un linguaggio parzialmente standardizzato per l'interrogazione e l'aggiornamento dei database. Tutti gli SGBD rispettano la parte standardizzata di SQL, ma aggiungono estensioni proprietarie al linguaggio che sfruttano alcune caratteristiche degli SGBD. Abbiamo già visto due esempi: la generazione automatica delle chiavi primarie e i tipi consentiti per le colonne delle tabelle dipendono spesso dall'SGBD.
I quattro comandi di base del linguaggio SQL che presentiamo sono standardizzati e accettati da tutti gli SGBD:
La query che recupera i dati contenuti in un database. Solo le parole chiave nella prima riga sono obbligatorie, le altre sono facoltative. Altre parole chiave non mostrate sono qui.
| |
Inserisce una riga nella tabella. (col1, col2, ...) specifica le colonne della riga da inizializzare con i valori (val1, val2, ...). | |
Aggiorna la tabella verificando la condizione (tutte le righe se non è specificato where). Per queste righe, col1 riceve il valore val1 | |
Elimina tutte le condizioni di verifica della tabella |
Scriveremo un'applicazione console per eseguire comandi SQL sul database [dbarticles] creato in precedenza. Ecco un esempio di esecuzione . Invitiamo il lettore a comprendere i comandi SQL eseguiti e i relativi risultati.
- riga 1: la stringa di connessione: contiene tutti i parametri necessari per connettersi al database.
- riga 3: richiede il contenuto della tabella [articles]
- riga 16: viene inserita una nuova riga. Si noti che l'id non viene inizializzato in questa operazione, poiché è l'SGBD a generare il valore di questo campo.
- riga 19: controllo. Riga 28, la riga è stata aggiunta.
- riga 30: il prezzo dell'articolo appena aggiunto viene aumentato del 10%.
- riga 33: verifica
- riga 42: l'aumento di prezzo è stato applicato
- riga 44: elimina l'articolo aggiunto in precedenza
- riga 47: ok
- righe 53-55: l'articolo non è più presente.
9.3.3. Interfacce ADO.NET di base per la modalità connessa
Torniamo al diagramma di un'applicazione che utilizza un database tramite un connettore ADO.NET:
![]() |
In modalità connessa, il :
- apre una connessione all'origine dati
- opera con l'origine dati in lettura/scrittura
- chiude la connessione
Tre interfacce ADO.NET sono principalmente interessate da queste operazioni:
- IDbConnection, che incapsula le proprietà e i metodi della connessione.
- IDbCommand, che incapsula le proprietà e i metodi del comando SQL in esecuzione.
- IDataReader, che incapsula le proprietà e i metodi del risultato di un'istruzione SQL Select.
Utilizzata per gestire la connessione al database. I metodi M e le proprietà P di questa interfaccia saranno i seguenti:
Nome | Tipo | Ruolo |
P | catena di connessione alla base. Specifica tutti i parametri necessari per stabilire una connessione con una base specifica. | |
M | apre la connessione alla base definita da ConnectionString | |
M | chiude la connessione | |
M | avvia una transazione. | |
P | Stato della connessione: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken |
Se Connection è una classe che implementa l'interfaccia IDbConnection, la connessione può essere aperta come segue:
Utilizzata per eseguire un comando SQL o una procedura memorizzata. I metodi M e le proprietà P di questa interfaccia saranno i seguenti:
Nome | Tipo | Ruolo |
P | indica cosa deve essere eseguito - prende i propri valori da un'enumerazione : - CommandType.Text : esegue il comando SQL definito in CommandText. Questo è il valore predefinito. - CommandType.StoredProcedure : esegue una procedura memorizzata nel | |
P | - testo del comando SQL da eseguire se CommandType= CommandType.Text - il nome della procedura memorizzata da eseguire se CommandType= CommandType.StoredProcedure | |
P | ID della connessione IDbConnection da utilizzare per eseguire il comando SQL | |
P | l'ID della transazione IDbTransaction in cui eseguire il comando SQL | |
P | l'elenco dei parametri di un'istruzione SQL parametrizzata. L'istruzione update articles set price=price*1.1 where id=@id contiene il parametro @id. | |
M | per eseguire un'istruzione SQL SELECT. Il risultato è un oggetto IDataReader che rappresenta il risultato della SELECT. | |
M | per eseguire un comando SQL Update, Insert, Delete. Restituisce il numero di righe interessate dall'operazione (aggiornate, inserite, eliminate). | |
M | per eseguire un comando SQL Select restituisce un singolo risultato come in: select count(*) from articles. | |
M | per creare i parametri IDbParameter dell'istruzione SQL. | |
M | ottimizza l'esecuzione di una query parametrizzata quando viene eseguita più volte con parametri diversi. |
Se Command è una classe che implementa l'interfaccia IDbCommand, l'esecuzione di un'istruzione SQL senza transazione assumerà la seguente forma:
Utilizzata per incapsulare i risultati di un'istruzione SQL SELECT. Un oggetto IDataReader rappresenta una tabella con righe e colonne, che vengono elaborate in sequenza: prima la prima riga, poi la seconda, ... I metodi M e le proprietà P di questa interfaccia saranno i seguenti:
Nome | Tipo | Ruolo |
P | il numero di colonne nella tabella IDataReader | |
M | GetName(i) restituisce il nome della colonna n° i della tabella IDataReader. | |
P | Item[i] rappresenta la colonna n° i nella riga corrente della tabella IDataReader. | |
M | passa alla riga successiva nella tabella IDataReader. Render Booleano True se la lettura è stata possibile, False in caso contrario. | |
M | chiude la tabella IDataReader. | |
M | GetBoolean(i): restituisce il valore booleano della colonna n. i nella riga corrente della tabella IDataReader. Altri metodi simili includono: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString. | |
M | Getvalue(i): restituisce il valore della colonna n. i nella riga corrente della tabella IDataReader come oggetto di tipo. | |
M | IsDBNull(i) restituisce True se la colonna n. i nella riga corrente di IDataReader non ha alcun valore, rappresentato da SQL NULL. |
L'utilizzo di un oggetto IDataReader spesso si presenta in questo modo:
9.3.4. Gestione degli errori
Rivediamo l'architettura di un'applicazione database:
![]() |
Il livello [dao] può incontrare numerosi errori durante il funzionamento del database. Questi saranno generati come eccezioni dal connettore ADO.NET. Il codice del livello [dao] deve gestirli. Qualsiasi operazione con il database deve essere eseguita in modalità try / catch / finally, al fine di intercettare e gestire qualsiasi eccezione e liberare le risorse che devono essere liberate. Ad esempio, il codice visto sopra per sfruttare il risultato di un'istruzione Select diventa il seguente:
Qualunque cosa accada, gli oggetti IDataReader e IDbConnection devono essere chiusi. Ecco perché questa chiusura è inclusa nelle clausole finally.
La chiusura della connessione e dell'oggetto IDataReader può essere automatizzata con un using :
- La riga 3, la clausola using, garantisce che la connessione aperta all'interno di using(...){...} venga chiusa al di fuori di essa, indipendentemente dal modo in cui si esce dal blocco: normalmente o a seguito dell'arrivo di un'eccezione. Ciò consente di risparmiare un finally, ma l'interesse non risiede in questa piccola economia. L'uso di un using impedisce allo sviluppatore di chiudere la connessione da solo. Oppure, dimenticare di chiudere una connessione può passare inosservato e "bloccare" l'applicazione in modo apparentemente casuale, ogni volta che l'SGBD raggiunge il numero massimo di connessioni aperte che può supportare.
- Riga 11: procedere allo stesso modo per chiudere l'oggetto IDataReader.
9.3.5. Esempio di configurazione del progetto
Il progetto finale sarà il seguente:
![]() |
- [1]: il progetto avrà un file di configurazione [App.config]
- [2]: utilizza due classi DLL che non sono referenziate di default e devono quindi essere aggiunte ai riferimenti del progetto:
- [System.Configuration] per utilizzare il file di configurazione [App.config]
- [System.Data.SqlServerCe] per gestire il database Sql Server Compact
- [3, 4]: ricorda come aggiungere riferimenti a un progetto.
- [5, 6]: riepilogo su come aggiungere il file [App.config] a un progetto.
Il file di configurazione [App.config] sarà il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
</connectionStrings>
</configuration>
- righe 3-5: il tag <connectionStrings> definisce le stringhe di connessione al database. Una stringa di connessione ha la forma "parametro1=valore1;parametro2=valore2;...". Definisce tutti i parametri necessari per stabilire una connessione con un particolare database. Queste stringhe di connessione cambiano a seconda del SGBD. La [http://www.connectionstrings.com/] ne fornisce la struttura per i principali SGBD.
- riga 4: definisce una stringa di connessione specifica, in questo caso per il database SQL Server Compact dbarticles.sdf che abbiamo creato in precedenza:
- name = nome della stringa di connessione. È tramite questo nome che una stringa di connessione viene recuperata dal programma C#
- connectionString : la stringa di connessione per un database SQL Server Compact
- DataSource : indica il percorso di base. La sintassi |DataDirectory| indica la cartella di esecuzione del progetto.
- Password : password di base. Questo parametro è assente se non c'è una password.
Il codice C# per recuperare la stringa di connessione precedente è il seguente:
string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
- ConfigurationManager è la classe della DLL [System.Configuration], utilizzata per gestire il file [App.config].
- ConnectionsStrings["nom"].ConnectionString : indica il tag connectionString < add name="name" connectionString="..."> nella sezione <connectionStrings> del file [App.config]
Il progetto è ora configurato. Passiamo ora alla classe [Program.cs], di cui abbiamo visto un esempio in precedenza.
9.3.6. Il programma di esempio
Il programma [program.cs] è il seguente:
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Text;
using System.Text.RegularExpressions;
using System.Configuration;
namespace Chap7 {
class SqlCommands {
static void Main(string[] args) {
// console application - executes SQL requests typed from the keyboard
// on a database whose connection string is obtained from a configuration file
// use of configuration file [App.config]
string connectionString = null;
try {
connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// display connection string
Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
// build a dictionary of accepted sql commands
string[] commandesSQL = new string[] { "select", "insert", "update", "delete" };
Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>();
for (int i = 0; i < commandesSQL.Length; i++) {
dicoCommandes.Add(commandesSQL[i], true);
}
// read-execute SQL commands typed on the keyboard
string requête = nu ll; // query text SQL
string[] cham ps; // query fields
Regex modèle = new Regex(@"\s+ "); // sequence of spaces
// input-execution loop for SQL commands typed on keyboard
while (true) {
// request for query
Console.Write("\nRequête SQL (rien pour arrêter) : ");
requête = Console.ReadLine().Trim().ToLower();
// finished?
if (requête == "")
break;
// the query is broken down into fields
champs = modèle.Split(requête);
// valid request?
if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
// error msg
Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
// following request
continue;
}
// query execution
if (champs[0] == "select") {
ExecuteSelect(connectionString, requête);
} else
ExecuteUpdate(connectionString, requête);
}
}
// execute an update request
static void ExecuteUpdate(string connectionString, string requête) {
...
}
// executing a Select query
static void ExecuteSelect(string connectionString, string requête) {
....
}
}
}
- righe 1-6: spazi dei nomi utilizzati nell'applicazione. La gestione di un database SQL Server Compact richiede lo spazio dei nomi [System.Data.SqlServerCe] alla riga 3. Si tratta di una dipendenza da uno spazio dei nomi proprietario del SGBD. Ciò significa che il programma dovrà essere modificato se il SGBD viene cambiato.
- riga 18: la stringa di connessione al database viene letta dal file [App.config] e visualizzata alla riga 25. Verrà utilizzata per stabilire una connessione con il database.
- righe 28-32: un dizionario che memorizza i nomi dei quattro comandi SQL autorizzati: select, insert, update, delete.
- righe 40-62: il ciclo per l'inserimento dei comandi SQL digitati sulla tastiera e la loro esecuzione sul database
- riga 48: la riga digitata sulla tastiera viene suddivisa in campi per determinare il primo termine, che deve essere: select, insert, update, delete
- righe 50-55: se la query non è valida, viene visualizzato un messaggio di errore e si passa alla query successiva.
- righe 57-61: il comando SQL inserito viene eseguito. L'esecuzione assume una forma diversa a seconda che si tratti di un comando select o di un comando insert, update, delete. Nel primo caso, il comando recupera i dati dal database senza modificarli; nel secondo, aggiorna il database senza recuperare dati. In entrambi i casi, l'esecuzione è delegata a un metodo che richiede due parametri:
- la stringa di connessione che gli consentirà di connettersi al database
- il comando SQL da eseguire su questa connessione
9.3.7. Esecuzione di una query SELECT
L'esecuzione dei comandi SQL richiede i seguenti passaggi:
- Connessione al database
- Invio dei comandi SQL al database
- Elaborazione dei risultati dei comandi SQL
- Chiusura della connessione
I passaggi 2 e 3 vengono eseguiti ripetutamente, con la connessione che viene chiusa solo quando il database non viene più utilizzato. Le connessioni aperte sono risorse limitate di un SGBD. Devono essere conservate. Questo è il motivo per cui cerchiamo sempre di limitare la durata di una connessione aperta. In questo esempio, la connessione viene chiusa dopo ogni comando SQL. Viene aperta una nuova connessione per il comando SQL successivo. L'apertura e la chiusura di una connessione comportano un costo. Per ridurre tale costo, alcuni SGBD offrono il concetto di pool di connessioni aperte: all'avvio dell'applicazione, vengono aperte N connessioni e assegnate al pool. Queste rimangono aperte fino al termine dell'applicazione. Quando l'applicazione apre una connessione, riceve una delle N connessioni già aperte nel pool. Quando chiude la connessione, la restituisce semplicemente al pool. Il vantaggio di questo sistema è che è trasparente per lo sviluppatore: il programma non deve essere modificato per utilizzare il pool di connessioni. La configurazione del pool di connessioni dipende dal SGBD.
Per prima cosa, esaminiamo l'esecuzione dei comandi SQL Select. Il metodo ExecuteSelect del nostro programma di esempio è il seguente:
// execute a Select query
static void ExecuteSelect(string connectionString, string requête) {
// handle any exceptions
try {
using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
// opening connection
connexion.Open();
// executes sqlCommand with select query
SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
SqlCeDataReader reader= sqlCommand.ExecuteReader();
// displaying results
AfficheReader(reader);
}
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
// reader display
static void AfficheReader(IDataReader reader) {
...
}
- riga 2: il metodo riceve due parametri:
- la stringa di connessione [connectionString] che gli consentirà di connettersi al database
- l'istruzione SQL Select [request] da eseguire su questa connessione
- riga 4: qualsiasi operazione con un database può generare un'eccezione che potresti voler gestire. Ciò è tanto più importante qui in quanto i comandi SQL forniti dall'utente potrebbero essere sintatticamente errati. Dobbiamo essere in grado di comunicarglielo. Tutto il codice si trova quindi all'interno di un try / catch.
- riga 5: qui ci sono diverse cose:
- la connessione al database viene inizializzata con la stringa di connessione [connectionString]. Non è ancora aperta. Verrà aperta alla riga 7.
- la clausola using (Resource) {...} è una struttura sintattica che garantisce il rilascio della risorsa Resource, in questo caso una connessione, all'uscita del blocco controllato dal using.
- La connessione è di tipo proprietario: SqlCeConnection, specifico per il SGBD SQL Server Compact.
- riga 7: la connessione viene aperta. È in questo momento che vengono utilizzati i parametri della stringa di connessione.
- riga 9: viene emesso un comando SQL tramite un oggetto SqlCeCommand. La riga 9 inizializza questo oggetto con due informazioni: la connessione da utilizzare e il comando SQL da inviare su di essa. L'oggetto SqlCeCommand può essere utilizzato per eseguire un comando Select, un comando Update, Insert o Delete. Le sue proprietà e i suoi metodi sono stati presentati al paragrafo 9.3.3.
- riga 10: un comando SQL Select viene eseguito tramite l'oggetto ExecuteReader di SqlCeCommand, che crea un oggetto IDataReader i cui metodi e proprietà sono descritti nel paragrafo 9.3.3.
- riga 12: la visualizzazione dei risultati è affidata all'oggetto AfficheReader:
// reader display
static void AfficheReader(IDataReader reader) {
using (reader) {
// exploitation of results
// -- columns
StringBuilder ligne = new StringBuilder();
int i;
for (i = 0; i < reader.FieldCount - 1; i++) {
ligne.Append(reader.GetName(i)).Append(",");
}
ligne.Append(reader.GetName(i));
Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-'));
// -- data
while (reader.Read()) {
// current line operation
ligne = new StringBuilder();
for (i = 0; i < reader.FieldCount; i++) {
ligne.Append(reader[i].ToString()).Append(" ");
}
Console.WriteLine(ligne);
}
}
}
- riga 2: il metodo riceve un oggetto IDataReader. Si noti che qui si tratta di un'interfaccia e non di una classe specifica.
- riga 3: la clausola using serve a gestire automaticamente la chiusura dell'IDataReader.
- righe 8-10: i nomi delle colonne della tabella dei risultati della selezione. Si tratta delle colonne col1, col2, ... della richiesta SELECT col1, col2, ... FROM table ...
- righe 14-21: sfoglia la tabella dei risultati e visualizza i valori per ogni riga della tabella.
- riga 18: non conosciamo il tipo della colonna i nel risultato perché non conosciamo la tabella oggetto della query. La sintassi reader.GetXXX(i) dove XXX è il tipo della colonna n. i, poiché questo tipo non è noto. Utilizziamo quindi la sintassi reader.Item[i].ToString() per ottenere la rappresentazione stringa della colonna n. i. La sintassi reader.Item[i].ToString() può essere abbreviata in reader[i].ToString().
9.3.8. Esecuzione di un ordine di aggiornamento: INSERT, UPDATE, DELETE
Il codice del metodo ExecuteUpdate è il seguente:
// execute an update request
static void ExecuteUpdate(string connectionString, string requête) {
// handle any exceptions
try {
using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
// opening connection
connexion.Open();
// executes sqlCommand with update request
SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
int nbLignes = sqlCommand.ExecuteNonQuery();
// result display
Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
}
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
Abbiamo detto che l'esecuzione di un comando di query Select non era diversa da quella di un comando di aggiornamento Update, Insert, Delete tramite il metodo SqlCeCommand utilizzato: ExecuteReader per Select, ExecuteNonQuery per Update, Insert, Delete. Commentiamo solo quest'ultimo metodo nel codice sopra riportato:
- riga 10: i comandi Update, Insert, Delete vengono eseguiti dall'oggetto SqlCeCommand con ExecuteNonQuery. In caso di esito positivo, questo metodo restituisce il numero di righe aggiornate (update), inserite (insert) o eliminate (delete).
- riga 12: questo numero di righe viene visualizzato sullo schermo
Il lettore è invitato a consultare un esempio di come eseguire questo codice, paragrafo 9.3.2.
9.4. Altri connettori ADO.NET
Il codice che abbiamo studiato è proprietario: dipende da [System.Data.SqlServerCe] per il SGBD SQL Server Compact. Ora realizzeremo lo stesso programma con diversi connettori .NET e vedremo cosa cambia.
9.4.1. Connettore SQL Server 2005
L'architettura utilizzata sarà la seguente:
![]() |
L'installazione di SQL Server 2005 è descritta nelle appendici al paragrafo 1.1.
Creiamo un secondo progetto nella stessa soluzione di prima, quindi creiamo il database SQL Server 2005. Il SGBD SQL Server 2005 deve essere avviato prima delle operazioni seguenti:
![]() |
- [1]: creare un nuovo progetto nella soluzione corrente e impostarlo come progetto corrente.
- [2]: creare una nuova connessione
- [3]: selezionare il tipo di connessione
![]() |
- [4]: selezionare il SGBD SQL Server
- [5]: risultato della scelta precedente
- [6]: utilizzare il pulsante [Sfoglia] per specificare dove creare il database di SQL Server 2005. Il database è contenuto in un file .mdf.
- [7]: selezionare la radice del nuovo progetto e richiamare il file di base [dbarticles.mdf].
- [8]: utilizzare l'autenticazione Windows.
- [9]: convalida la pagina delle informazioni
![]() |
- [11]: database SQL Server
- [12]: creare una tabella. Sarà identica al database SQL Server Compact creato in precedenza.
- [13]: il campo [id]
- [14]: il campo [id] è di tipo Identity.
- [15,16]: il campo [id] è la chiave primaria
![]() |
- [17]: altri campi della tabella
- [18]: assegna alla tabella il nome [articles] quando la salvi (Ctrl+S).
Dobbiamo ancora inserire i dati nel :
![]() | ![]() |
Includiamo il database nel :
![]() |
I riferimenti del progetto sono i seguenti:
![]() |
Il file di configurazione [App.config] è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" />
<add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" />
</connectionStrings>
</configuration>
- riga 4: stringa di connessione al database [dbarticles.mdf] con autenticazione Windows
- riga 5: stringa di connessione al database [dbarticles.mdf] con autenticazione SQL Server. [sa,msde] è la coppia (login,password) dell'amministratore di SQL Server come definita nel paragrafo 1.1.
Il programma [Program.cs] si evolve come segue:
using System.Data.SqlClient;
...
namespace Chap7 {
class SqlCommands {
static void Main(string[] args) {
...
// use of configuration file [App.config]
string connectionString = null;
try {
connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
} catch (Exception e) {
...
}
...
// read-execute SQL commands typed on the keyboard
...
}
// execute an update request
static void ExecuteUpdate(string connectionString, string requête) {
// handle any exceptions
try {
using (SqlConnection connexion = new SqlConnection(connectionString)) {
// opening connection
connexion.Open();
// executes sqlCommand with update request
SqlCommand sqlCommand = new SqlCommand(requête, connexion);
int nbLignes = sqlCommand.ExecuteNonQuery();
// result display
Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
}
} catch (Exception ex) {
....
}
}
// execute a Select query
static void ExecuteSelect(string connectionString, string requête) {
// handle any exceptions
try {
using (SqlConnection connexion = new SqlConnection(connectionString)) {
// opening connection
connexion.Open();
// executes sqlCommand with select query
SqlCommand sqlCommand = new SqlCommand(requête, connexion);
SqlDataReader reader = sqlCommand.ExecuteReader();
// exploitation of results
...
}
} catch (Exception ex) {
...
}
}
}
}
- riga 1: lo spazio dei nomi [System.Data.SqlClient] contiene classi per la gestione di un database SQL Server 2005
- riga 24: la connessione è SQLConnection
- riga 28: l'oggetto che incapsula i comandi SQL è di tipo SQLCommand
- riga 47: l'oggetto che incapsula il risultato di un comando SQL Select è di tipo SQLDataReader
Il codice è identico a quello utilizzato con il SGBD SQL Server Compact, ad eccezione dei nomi delle classi. Per eseguirlo, è possibile utilizzare (riga 11) una delle due stringhe di connessione definite in [App.config].
9.4.2. Connettore MySQL5
L'architettura utilizzata sarà la seguente:
![]() |
L'installazione di MySQL5 è descritta nelle appendici al paragrafo 1.2 Connettore e il connettore Ado.Net al paragrafo 1.2.5.
Creiamo un terzo progetto nella stessa soluzione di prima e aggiungiamo i riferimenti necessari:
![]() |
- [1]: il nuovo progetto
- [2]: a cui aggiungiamo i riferimenti
- [3]: la DLL [MySQL.Data] del connettore Ado.Net di MySql5 e quella di [System.Configuration] [4].
Ora creiamo il database [dbarticles] e la sua tabella [articles]. Il SGBD MySQL5 deve essere avviato. Inoltre, avviamo il client [Query Browser] (vedere paragrafo 1.2.3).
![]() |
- [1]: in [Query Browser], clicchiamo con il tasto destro nella zona [Schemata] [2] per creare [3] un nuovo schema, termine utilizzato per descrivere un database.
- [4]: il database si chiamerà [dbarticles]. In [5], lo vediamo. Per il momento, non ha tabelle. Eseguiremo il seguente script SQL:
- riga 1: il database [dbarticles] diventa il database corrente. I seguenti comandi SQL verranno eseguiti su di esso.
- righe 4-10: definizione della tabella [ARTICLES]. Si noti che SQL è di proprietà di MySQL. I tipi di colonna e la generazione automatica della chiave primaria (attributo AUTO_INCREMENT) differiscono da quelli presenti nei SGBD SQL Server Compact ed Express.
- righe 12-14: inserimento di tre righe
- righe 16-21: aggiunta di vincoli di integrità sulle colonne.
Questo script viene eseguito in [MySQL Query Browser]:
![]() |
- in [MySQL Query Browser] [6], carichiamo lo script [7]. Lo si può vedere in [8]. In [9], viene eseguito.
![]() |
- in [10], è stata creata la tabella [articles]. Fare doppio clic su di essa. Si aprirà la finestra [11] contenente la query [12], pronta per essere eseguita tramite [13]. In [14], il risultato dell'esecuzione. Abbiamo le tre righe previste. Si noti che i valori nel campo [ID] sono stati generati automaticamente (attributo del campo AUTO_INCREMENT).
Ora che il database è pronto, possiamo tornare allo sviluppo dell'applicazione in Visual Studio.
![]() |
In [1], il programma [Program.cs] e il file di configurazione [App.config]. Quest'ultimo è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
</connectionStrings>
</configuration>
Riga 4, gli elementi della stringa di connessione sono i seguenti:
- Server : nome della macchina su cui si trova il SGBD MySQL, qui localhost, ovvero la macchina su cui verrà eseguito il programma.
- Database: il nome del database gestito, qui dbarticles
- Uid: nome utente, qui root
- Pwd: la relativa password, in questo caso root. Queste due informazioni identificano l'amministratore creato nel paragrafo 1.2.
Il programma [Program.cs] è identico a quello delle versioni precedenti, tranne che per i seguenti dettagli:
MySql.Data.MySqlClient | |
MySqlConnection | |
MySqlCommand | |
MySqlDataReader |
Il programma utilizza la stringa di connessione denominata dbArticlesMySql5 nel file [App.config]. L'esecuzione produce i seguenti risultati:
9.4.3. Connettore ODBC
L'architettura utilizzata sarà la seguente:
![]() |
Il vantaggio dei connettori ODBC è che presentano un'interfaccia standard alle applicazioni che li utilizzano. Pertanto, con un unico codice, la nuova applicazione sarà in grado di comunicare con qualsiasi SGBD dotato di un connettore ODBC, c.a.d. o SGBD. Le prestazioni dei connettori ODBC non sono pari a quelle dei connettori "proprietari", che sono in grado di sfruttare tutte le funzionalità di un particolare SGBD. D'altra parte, si ottiene una grande flessibilità applicativa: è possibile cambiare SGBD senza modificare il codice.
Vedremo un esempio in cui l'applicazione utilizza un database MySQL5 o un database SQL Server Express, a seconda della stringa di connessione fornita. Di seguito, si presume che:
- SGBD SQL Server Express e MySQL5 siano stati avviati
- il driver ODBC di MySQL5 sia presente sulla macchina (vedi paragrafo 1.2.6). L'impostazione predefinita è SQL Server 2005.
- i database utilizzati sono quelli del paragrafo 9.4.2 per la base MySQL5, quelli del paragrafo 9.4.1 per il database SQL Server Express.
Il nuovo progetto Visual Studio è il seguente:
![]() |
Sopra, il database SQL Server [dbarticles.mdf] creato al paragrafo 9.4.1 è stato copiato nel file di progetto.
Il file di configurazione [App.config] è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" />
<add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
</connectionStrings>
</configuration>
- riga 4: stringa di connessione sorgente ODBC MySQL5. Si tratta di una stringa già studiata in precedenza, nella quale troviamo un nuovo parametro Driver che definisce il driver ODBC da utilizzare.
- riga 5: stringa di connessione sorgente ODBC SQL Server Express. Si tratta della stringa già utilizzata in un esempio precedente, alla quale è stato aggiunto il parametro Driver.
Il programma [Program.cs] è identico a quello delle versioni precedenti, ad eccezione dei seguenti dettagli:
System.Data.Odbc | |
OdbcConnection | |
OdbcCommand | |
OdbcDataReader |
Il programma utilizza una delle due stringhe di connessione definite nel file [App.config]. L'esecuzione produce i seguenti risultati:
Con la stringa di connessione [dbArticlesOdbcSqlServer2005] :
Con stringa di connessione [dbArticlesOdbcMySql5] :
9.4.4. Connettore OLE DB
L'architettura utilizzata sarà la seguente:
![]() |
Come per i connettori ODBC, sono disponibili anche i connettori OLE DB (Object Linking and Embedding DataBase), driver che forniscono un'interfaccia standard alle applicazioni che li utilizzano. I driver ODBC consentono l'accesso ai database. Le origini dati per i driver OLE DB sono più varie: database, sistemi di messaggistica, directory, ecc. Qualsiasi origine dati può essere oggetto di un driver OLE DB, se un editore lo decide. Ciò fornisce un accesso standard a un'ampia varietà di dati.
Vedremo un esempio in cui l'applicazione utilizza un database ACCESS o SQL Server Express, a seconda della stringa di connessione fornita. Di seguito, supponiamo che l'SGBD SQL Server Express sia stato avviato e che il database utilizzato sia quello dell'esempio precedente.
Il nuovo progetto Visual Studio è il seguente:
![]() |
- in [1]: lo spazio dei nomi richiesto per i connettori OLE DB è [System.Data.OleDb], presente nel riferimento [System.Data] sopra. Il database SQL Server [dbarticles.mdf] è stato copiato dal progetto precedente. Il file di base [dbarticles.mdb] è stato creato con Access.
- in [2]: come il database SQL Server, il database ACCESS ha la proprietà [Copy to Output Directory=Copy Always], in modo da essere copiato automaticamente nella cartella di esecuzione del progetto.
Il database ACCESS [dbarticles.mdb] è il seguente:
![]() |
In [1], la struttura della tabella [articles] e in [2] il suo contenuto.
Il file di configurazione [App.config] è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesOleDbAccess" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/>
<add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
</connectionStrings>
</configuration>
- riga 4: stringa di connessione sorgente OLE DB ACCESS. Contiene il parametro Provider che definisce il driver OLE DB da utilizzare e il percorso del database
- riga 5: stringa di connessione sorgente OLE DB Server Express.
Il programma [Program.cs] è identico a quello delle versioni precedenti, ad eccezione dei seguenti dettagli:
System.Data.OleDb | |
OleDbConnection | |
OleDbCommand | |
OleDbDataReader |
Il programma utilizza una delle due stringhe di connessione definite nel file [App.config]. L'esecuzione fornisce i seguenti risultati con la stringa di connessione [dbArticlesOleDbAccess]:
9.4.5. Connettore generico
L'architettura utilizzata sarà la seguente:
![]() |
Come i connettori ODBC e OLE DB, il connettore generico presenta un'interfaccia standard alle applicazioni che lo utilizzano, ma migliora le prestazioni senza sacrificare la flessibilità. Il connettore generico si basa sui connettori SGBD proprietari. L'applicazione utilizza le classi del connettore generico. Queste classi fungono da intermediari tra l'applicazione e il connettore proprietario.
Nell'esempio sopra riportato, quando l'applicazione richiede una connessione al connettore generico, quest'ultimo restituisce un IDbConnection, l'interfaccia di connessione descritta nel paragrafo 9.3.3, implementata da un MySQLConnection o da un SQLConnection a seconda della natura della richiesta che gli viene fatta. Si dice che il connettore generico abbia classi di tipo factory: usiamo una factory per chiederle di creare oggetti e fornire riferimenti ad essi (puntatori). Da qui il suo nome (factory = fabbrica, impianto di produzione di oggetti).
Non esiste un connettore generico per tutti i SGBD (aprile 2008). Per scoprire quali sono installati su una data macchina, utilizzare il seguente programma:
using System;
using System.Data;
using System.Data.Common;
namespace Chap7 {
class Providers {
public static void Main() {
DataTable dt = DbProviderFactories.GetFactoryClasses();
foreach (DataColumn col in dt.Columns) {
Console.Write("{0}|", col.ColumnName);
}
Console.WriteLine("\n".PadRight(40, '-'));
foreach (DataRow row in dt.Rows) {
foreach (object item in row.ItemArray) {
Console.Write("{0}|", item);
}
Console.WriteLine("\n".PadRight(40, '-'));
}
}
}
}
- riga 8: il metodo statico [DbProviderFactories.GetFactoryClasses()] restituisce un elenco di connettori generici installati, sotto forma di tabella di database memorizzata in memoria (DataTable).
- righe 9-11: visualizza i nomi delle colonne della tabella dt :
- dt.Columns è l'elenco delle colonne della tabella. Una colonna C è di tipo DataColumn
- [DataColumn].ColumnName è il nome della colonna
- righe 13-18: visualizza le righe della tabella dt :
- dt.Rows è l'elenco delle righe della tabella. Una riga L è di tipo DataRow
- [DataRow].ItemArray è un array di oggetti, dove ogni oggetto rappresenta una colonna della riga
Il risultato sul mio computer è il seguente:
- riga 1: la tabella ha quattro colonne. Le prime tre sono le più utili per noi qui.
La schermata seguente mostra che sono disponibili i seguenti connettori generici:
Nome | Identificatore |
System.Data.Odbc | |
System.Data.OleDb | |
System.Data.OracleClient | |
System.Data.SqlClient | |
System.Data.SqlServerCe.3.5 | |
MySql.Data.MySqlClient |
In un programma C# è possibile accedere a un connettore generico tramite il suo identificatore.
Vedremo un esempio in cui l'applicazione sfrutta i vari database che abbiamo creato finora. L'applicazione riceverà due parametri:
- il primo parametro specifica il tipo di SGBD utilizzato in modo che venga utilizzata la libreria di classi corretta
- il secondo parametro specifica il database gestito, tramite una stringa di connessione.
Il nuovo progetto Visual Studio è il seguente:
![]() |
- in [1]: lo spazio dei nomi richiesto per i connettori generici è [System.Data.common], presente nel riferimento [System.Data].
Il file di configurazione [App.config] è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
<add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
<add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
<add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" />
<add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
<add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
<add name="dbArticlesOleDbAccess" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/>
</connectionStrings>
<appSettings>
<add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/>
<add key="factoryMySql" value="MySql.Data.MySqlClient"/>
<add key="factorySqlServer" value="System.Data.SqlClient"/>
<add key="factoryOdbc" value="System.Data.Odbc"/>
<add key="factoryOleDb" value="System.Data.OleDb"/>
</appSettings>
</configuration>
- righe 3-11: stringhe di connessione per i vari database utilizzati.
- righe 13-17: nomi dei connettori generici da utilizzare
Il programma [Program.cs] è il seguente:
...
using System.Data.Common;
namespace Chap7 {
class SqlCommands {
static void Main(string[] args) {
// console application - executes SQL requests typed from the keyboard
// on a database whose connection string is obtained from a configuration file, along with the connector name of the associated SGBD
// checking parameters
if (args.Length != 2) {
Console.WriteLine("Syntaxe : pg factory connectionString");
return;
}
// using the configuration file
string factory = null;
string connectionString = null;
DbProviderFactory connecteur = null;
try {
// factory
factory = ConfigurationManager.AppSettings[args[0]];
// connecting chain
connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
// we retrieve a generic connector for the SGBD
connecteur = DbProviderFactories.GetFactory(factory);
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// displays
Console.WriteLine("Provider factory : [{0}]\n", factory);
Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
...
// query execution
if (champs[0] == "select") {
ExecuteSelect(connecteur,connectionString, requête);
} else
ExecuteUpdate(connecteur, connectionString, requête);
}
}
// execute an update request
static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
// handle any exceptions
try {
using (DbConnection connexion = connecteur.CreateConnection()) {
// connection configuration
connexion.ConnectionString = connectionString;
// opening connection
connexion.Open();
// configuration Command
DbCommand sqlCommand = connecteur.CreateCommand();
sqlCommand.CommandText = requête;
sqlCommand.Connection = connexion;
// request execution
int nbLignes = sqlCommand.ExecuteNonQuery();
// result display
Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
}
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
// execute a Select query
static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
// handle any exceptions
try {
using (DbConnection connexion = connecteur.CreateConnection()) {
// connection configuration
connexion.ConnectionString = connectionString;
// opening connection
connexion.Open();
// configuration Command
DbCommand sqlCommand = connecteur.CreateCommand();
sqlCommand.CommandText = requête;
sqlCommand.Connection = connexion;
// request execution
DbDataReader reader = sqlCommand.ExecuteReader();
// display of results
...
}
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
}
}
- righe 12-14: l'applicazione riceve due parametri: il nome del connettore generico e la stringa di connessione al database sotto forma di chiavi nel file [App.config].
- righe 23, 25: recupero del nome del connettore generico e della stringa di connessione da [App.config]
- riga 27: il connettore generico viene istanziato. Da questo momento in poi, è associato a un SGBD specifico.
- righe 39-43: l'esecuzione del comando SQL digitato sulla tastiera viene delegata a due metodi, ai quali passiamo:
- la richiesta da eseguire
- la stringa di connessione che identifica il database su cui verrà eseguita la query
- il connettore generico che identifica le classi da utilizzare per comunicare con l'SGBD che gestisce il database.
- righe 50-54: si ottiene una connessione utilizzando il metodo CreateConnection (riga 50) del connettore generico, quindi configurata con la stringa di connessione del database da gestire (riga 52). Viene quindi aperta (riga 54).
- righe 56-58: l'oggetto Command necessario per eseguire il comando SQL viene creato con il metodo CreateCommand del connettore generico. Viene quindi configurato con il testo del comando SQL da eseguire (riga 57) e la connessione su cui eseguirlo (riga 58).
- riga 60: viene eseguito il comando SQL di aggiornamento
- righe 74-87: viene utilizzato un codice simile. La novità risiede nella riga 84. L'oggetto Reader ottenuto dall'esecuzione del comando di tipo Select è DbDataReader, che può essere utilizzato allo stesso modo di OleDbDataReader, OdbcDataReader, ... che abbiamo già incontrato.
Ecco alcuni esempi.
Con MySQL5 di base:
![]() |
Aprire la pagina delle proprietà del progetto [1] e selezionare la scheda [Debug] [2]. In [3], la chiave del connettore per la riga 14 di [App.config]. In [4], la chiave della stringa di connessione alla riga 6 di [App.config]. I risultati sono i seguenti:
Con SQL Server Compact:
![]() |
In [1], la chiave del connettore per la riga 13 di [App.config]. In [2], la chiave della stringa di connessione alla riga 4 di [App.config]. I risultati sono i seguenti:
Il lettore è invitato a testare gli altri database.
9.4.6. Quale connettore scegliere?
Torniamo all'architettura di un'applicazione di database:
![]() |
Abbiamo visto vari tipi di connettori ADO.NET:
- i connettori proprietari sono i più efficienti, ma rendono il livello [dao] dipendente da classi proprietarie. Cambiare il SGBD significa cambiare il livello [dao].
- I connettori ODBC o OLE DB consentono di lavorare con più database senza modificare il livello [dao]. Sono meno potenti dei connettori proprietari.
- Il connettore generico si basa sui connettori proprietari pur presentando un'interfaccia standard al livello [DAO].
Sembra quindi che il connettore generico sia il connettore ideale. In pratica, tuttavia, il connettore generico non riesce a nascondere tutte le particolarità di un SGBD dietro un'interfaccia standard. Nel prossimo paragrafo, esamineremo il concetto di query parametrizzate. Con SQL Server, una query parametrizzata ha la seguente forma:
Con MySQL5, la stessa query verrebbe scritta:
C'è quindi una differenza nella sintassi. La proprietà dell'interfaccia IDbCommand descritta nel paragrafo 9.3.3 è la seguente:
l'elenco dei parametri di un'istruzione SQL parametrizzata. L'istruzione update articles set price=price*1.1 where id=@id ha il parametro @id. |
La proprietà Parameters è di tipo IDataParameterCollection, un'interfaccia. Rappresenta tutti i parametri del comando SQL CommandText. La proprietà Parameters dispone di un metodo Add per aggiungere IDataParameter, anch'esso un'interfaccia. Ha le seguenti proprietà:
- ParameterName : nome del parametro
- DbType: il tipo SQL del parametro
- Value : il valore assegnato al
- ...
Il tipo IDataParameter è particolarmente adatto ai parametri dell'ordine SQL
poiché contiene parametri con nome. È possibile utilizzare il ParameterName.
Il tipo IDataParameter non è compatibile con l'ordine SQL
perché i parametri non sono denominati. Viene quindi preso in considerazione l'ordine in cui i parametri vengono aggiunti alla raccolta [IDbCommand.Parameters]. In questo esempio, i 4 parametri dovrebbero essere inseriti nel seguente ordine: nome, prezzo, stockactuel, stockminimum. In una query con parametri denominati, l'ordine in cui i parametri vengono aggiunti è irrilevante. In definitiva, lo sviluppatore non può ignorare completamente il SGBD che utilizza durante l'inizializzazione dei parametri di una query parametrizzata. Questa è una delle attuali limitazioni del connettore generico.
Esistono framework che superano queste limitazioni e aggiungono nuove funzionalità al livello [dao]:
![]() |
Un framework è un insieme di librerie di classi progettate per facilitare un particolare modo di progettare un'applicazione. Esistono numerosi framework di questo tipo, che consentono di scrivere livelli [dao] che sono sia ad alte prestazioni sia insensibili ai cambiamenti nel SGBD :
- Spring.Net [http://www.springframework.net/], già presentato in questo documento, offre l'equivalente del connettore generico studiato, senza le sue limitazioni, oltre a varie funzionalità per semplificare l'accesso ai dati. È disponibile anche una versione Java.
- iBatis.Net [http://ibatis.apache.org] è più vecchio e più ricco di Spring.Net. È disponibile una versione Java.
- NHibernate [http://www.hibernate.org/] è un porting della versione Java di Hibernate, il famoso framework Java. NHibernate permette al livello [dao] di interagire con l'SGBD senza emettere comandi SQL. Il livello [dao] lavora con oggetti Hibernate. Per interrogare gli oggetti gestiti da Hibernate si utilizza un linguaggio di query HBL (Hibernate Query Language). Sono questi oggetti che emettono i comandi SQL. Hibernate è in grado di adattarsi ai dialetti SQL dei sistemi di gestione di database (SGBD).
- LINQ (Language INtegrated Query), integrato nella versione 3.5 di .NET e disponibile in C# 2008. LINQ segue le orme di NHibernate, ma per il momento (maggio 2008) è supportato solo il SGBD SQL Server. Ciò dovrebbe evolversi nel tempo. LINQ va oltre NHibernate: il suo linguaggio di query consente query standard su tre diversi tipi di origine dati:
- raccolte di oggetti (LINQ to Objects)
- un file Xml (LINQ to Xml)
- un database (LINQ to SQL)
Questi framework non saranno trattati in questo documento. Tuttavia, ne raccomandiamo vivamente l'uso nelle applicazioni professionali.
9.5. Query parametriche
Nel paragrafo precedente abbiamo parlato delle query parametrizzate. Le presentiamo qui con un esempio per il SGBD SQL Server Compact. Il progetto è il seguente
![]() |
- in [1], il progetto. Vengono utilizzati solo [App.config], [Article.cs] e [Parametres.cs]. Si noti anche il database SQL Server Ce [dbarticles.sdf].
- in [2], il progetto è configurato per eseguire [Parametres.cs]
- in [3], il progetto fa riferimento a
Il file di configurazione [App.config] definisce la stringa di connessione al database:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
</connectionStrings>
</configuration>
Il file [Article.cs] definisce una classe [Article]. Un oggetto Article verrà utilizzato per incapsulare le informazioni contenute in una riga del database ARTICLES [dbarticles.sdf] :
namespace Chap7 {
class Article {
// properties
public int Id { get; set; }
public string Nom { get; set; }
public decimal Prix { get; set; }
public int StockActuel { get; set; }
public int StockMinimum { get; set; }
// manufacturers
public Article() {
}
public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
Id = id;
Nom = nom;
Prix = prix;
StockActuel = stockActuel;
StockMinimum = stockMinimum;
}
}
}
L'applicazione [Parametres.cs] implementa le richieste parametrizzate:
using System;
using System.Data.SqlServerCe;
using System.Text;
using System.Data;
using System.Configuration;
namespace Chap7 {
class Parametres {
static void Main(string[] args) {
// using the configuration file
string connectionString = null;
try {
// connecting chain
connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// displays
Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
// create a table of items
Article[] articles = new Article[5];
for (int i = 1; i <= articles.Length; i++) {
articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i);
}
// handle any exceptions
try {
// delete existing items from the database
ExecuteUpdate(connectionString, "delete from articles");
// table items are displayed
ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
// insert the table of items into the database
InsertArticles(connectionString, articles);
// table items are displayed
ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
// insert table of items
static void InsertArticles(string connectionString, Article[] articles) {
using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
// opening connection
connexion.Open();
// control configuration
string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30);
sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
// command compilation
sqlCommand.Prepare();
// line insertion
for (int i = 0; i < articles.Length; i++) {
// parameter initialization
sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
// request execution
sqlCommand.ExecuteNonQuery();
}
}
}
// execute an update request
static void ExecuteUpdate(string connectionString, string requête) {
...
}
// execute a Select query
static void ExecuteSelect(string connectionString, string requête) {
...
}
// reader display
static void AfficheReader(IDataReader reader) {
...
}
}
La procedura [InsertArticles] alle righe 51-75 è nuova rispetto a quanto visto in precedenza:
- riga 51: la procedura riceve due parametri:
- la stringa di connessione connectionString che consentirà alla procedura di connettersi al
- un array di oggetti Article da aggiungere al database Articles
- riga 56: la richiesta di inserimento dell'oggetto [Article]. Ha quattro parametri:
- @name : nome dell'articolo
- @price : il suo prezzo
- @its : la sua disponibilità attuale
- @sm : la sua scorta minima
La sintassi di questa query parametrizzata è proprietaria di SQL Server Compact. Abbiamo visto nel paragrafo precedente che con MySQL5 la sintassi sarebbe la seguente:
Con SQL Server Compact, ogni parametro deve essere preceduto dal carattere @. I nomi dei parametri sono liberi.
- righe 58-61: definiscono le caratteristiche di ciascuno dei 4 parametri e li aggiungono, uno per uno, all'elenco dei parametri dell'oggetto SqlCeCommand che incapsula il comando SQL da eseguire.
Qui utilizziamo il metodo [SqlCeCommand].Parameters.Add, che ha sei firme. Ne utilizziamo entrambe qui di seguito:
Add(string parameterName, SQLDbType type)
aggiunge e configura il parametro denominato parameterName. Questo nome deve essere uno di quelli presenti nel parametro della query configurata: (@name, ...). type designa il tipo SQL della colonna interessata dal parametro. Sono disponibili molti tipi, tra cui:
tipo SQL | tipo C# | comment |
Int64 | ||
DateTime | ||
Decimale | ||
Doppio | ||
Int32 | ||
Decimale | ||
Stringa | stringa a lunghezza fissa | |
String | stringa a lunghezza variabile | |
Singolo |
Add(string parameterName, SQLDbType type, int size)
il terzo parametro size imposta la dimensione della colonna. Questa informazione è utile solo per determinati tipi SQL, ad esempio il tipo NVarChar.
- riga 63: la richiesta parametrizzata viene compilata. Si dice anche che viene preparata, da cui il nome del metodo. Questa operazione non è essenziale. Serve a migliorare le prestazioni. Quando un SGBD esegue una richiesta SQL, effettua alcune operazioni di ottimizzazione prima di eseguirla. Una query parametrizzata è pensata per essere eseguita più volte con parametri diversi. Il testo della query, tuttavia, rimane invariato. Il lavoro di ottimizzazione può quindi essere effettuato una sola volta. Alcuni programmi SGBD possono "preparare" o "compilare" le query parametrizzate. Viene quindi definito un piano di esecuzione per questa query. Questa è la fase di ottimizzazione di cui abbiamo parlato. Una volta compilata, la query viene eseguita ripetutamente, ogni volta con nuovi parametri effettivi ma con lo stesso piano di esecuzione.
La compilazione non è l'unico vantaggio delle query parametrizzate. Prendiamo la query che abbiamo studiato:
Potremmo voler costruire il testo della query a livello di programmazione:
string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";
Nell'esempio sopra, se (nome, prezzo, sa, sm) è ("articolo1", 100, 10, 1), la query precedente diventa:
string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";
Ora, se (nome, prezzo, stockattuale, stockminimo) è ("articolo1", 100, 10, 1), la query precedente diventa:
string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";
e diventa sintatticamente errata a causa dell'apostrofo presente nel sostantivo "article1". Se il nome proviene da un input dell'utente, ciò significa che dobbiamo verificare che l'input non contenga apostrofi e, in caso contrario, eliminarli. Questa operazione dipende dal sistema di gestione di database (SGBD). Il vantaggio della query preparata è che svolge autonomamente questo compito. Questa funzionalità da sola giustifica l'uso di una query preparata.
- righe 65-73: gli articoli nella tabella vengono inseriti uno per uno
- righe 67-70: ciascuno dei quattro parametri della query riceve il proprio valore tramite la proprietà Value.
- riga 72: la richiesta di inserimento, ora completa, viene eseguita nel modo consueto.
Ecco un esempio:
- riga 3: messaggio dopo l'eliminazione di tutte le righe della tabella
- righe 5-7: mostrano che la tabella è vuota
- righe 10-18: mostrano la tabella dopo l'inserimento dei 5 articoli
9.6. Transazioni
9.6.1. Informazioni generali
Una transazione è una sequenza di comandi SQL eseguiti in modo "atomico":
- o tutte le operazioni vanno a buon fine
- oppure una di esse fallisce, nel qual caso tutte quelle precedenti vengono annullate
Alla fine, le operazioni di una transazione sono state applicate tutte con successo oppure nessuna. Quando l'utente ha il controllo della transazione, la convalida con un comando COMMIT oppure la annulla con un comando ROLLBACK.
Negli esempi precedenti non abbiamo utilizzato una transazione. Eppure l'abbiamo fatto, perché in un SGBD un'istruzione SQL viene sempre eseguita all'interno di una transazione. Se il client .NET non avvia esplicitamente una transazione, l'SGBD utilizza una transazione implicita. Esistono due casi comuni:
- ogni singolo comando SQL è oggetto di una transazione, avviata dall'SGBD prima del comando e chiusa in seguito. Si dice che si è in modalità autocommit. È quindi come se il client .NET effettuasse transazioni per ogni comando SQL.
- L'SGBD non è in modalità autocommit e avvia una transazione implicita sul primo comando SQL che il client .NET emette al di fuori di una transazione e lascia che sia il client a chiuderla. Tutti i comandi SQL emessi dal client .NET fanno quindi parte della transazione implicita. Questa può essere terminata da vari eventi: il client chiude la connessione, avvia una nuova transazione, ecc., ma ci si trova quindi in una situazione dipendente dall'SGBD. Questa modalità dovrebbe essere evitata.
La modalità predefinita viene solitamente impostata configurando l'SGBD. Alcuni SGBD hanno come impostazione predefinita l'autocommit, altri no. Per impostazione predefinita, SQLServer Compact è in modalità autocommit.
I comandi SQL di utenti diversi vengono eseguiti contemporaneamente in transazioni che funzionano in parallelo. Le operazioni eseguite da una transazione possono influenzare quelle eseguite da un'altra. Esistono quattro livelli di isolamento tra le transazioni di utenti diversi:
- Lettura non confermata
- Lettura con commit
- Lettura ripetibile
- Serializzabile
Lettura non confermata
Questa modalità di isolamento è nota anche come "Lettura sporca". Ecco un esempio di ciò che può accadere in questa modalità:
- un utente U1 avvia una transazione su una tabella T
- un utente U2 avvia una transazione sulla stessa tabella T
- l'utente U1 modifica le righe nella tabella T ma non le convalida ancora
- l'utente U2 "vede" queste modifiche e prende decisioni basandosi su ciò che vede
- l'utente annulla la transazione con un ROLLBACK
Possiamo notare che al punto 4, l'utente U2 ha preso una decisione basandosi su dati che in seguito si riveleranno falsi.
Lettura con commit
Questa modalità di isolamento evita l'inconveniente descritto in precedenza. In questa modalità, l'utente U2 al punto 4 non "vedrà" le modifiche apportate dall'utente U1 alla tabella T. Le vedrà solo dopo che U1 avrà eseguito un COMMIT della propria transazione.
In questa modalità, nota anche come "Unrepeatable Read", possono tuttavia verificarsi le seguenti situazioni:
- un utente U1 avvia una transazione su una tabella T
- un utente U2 avvia una transazione sulla stessa tabella T
- l'utente U2 esegue un SELECT per ottenere la media di una colonna C delle righe in T che soddisfano una certa condizione
- l'utente U1 modifica (UPDATE) determinati valori nella colonna C di T e li convalida (COMMIT)
- l'utente U2 ripete lo stesso SELECT di cui al punto 3. Scoprirà che la media nella colonna C è cambiata a seguito delle modifiche apportate da U1.
Ora l'utente U2 vede solo le modifiche "confermate" da U1. Ma pur rimanendo nella stessa transazione, due operazioni identiche 3 e 5 danno risultati diversi. Questa situazione è nota come "Lettura non ripetibile". È una situazione fastidiosa per chiunque desideri avere un'immagine stabile della tabella T.
Lettura ripetibile
In questa modalità di isolamento, a un utente viene garantito di ottenere gli stessi risultati per le sue letture del database fintanto che rimane nella stessa transazione. Lavora su una foto sulla quale le modifiche apportate da altre transazioni, anche quelle convalidate, non vengono mai riflesse. Le vedrà solo quando terminerà la sua transazione con un COMMIT o un ROLLBACK.
Tuttavia, questa modalità di isolamento non è ancora perfetta. Dopo l'operazione 3 sopra descritta, le righe consultate dall'utente U2 sono bloccate. Durante l'operazione 4, l'utente U1 non potrà modificare (UPDATE) i valori nella colonna C di queste righe. Tuttavia, potrà aggiungere righe (INSERT). Se alcune delle righe aggiunte verificano la condizione testata in 3, l'operazione 5 fornirà una media diversa da quella trovata in 3, a causa delle righe aggiunte. Queste righe sono talvolta indicate come righe fantasma.
Per risolvere questo nuovo problema, dobbiamo passare all'isolamento "Serializable".
Serializable
In questa modalità di isolamento, le transazioni sono completamente isolate l'una dall'altra. Ciò garantisce che il risultato di due transazioni eseguite simultaneamente dia lo stesso risultato che si otterrebbe se fossero eseguite una dopo l'altra. Per ottenere questo risultato, nell'operazione 4, quando l'utente U1 vuole aggiungere righe che modificherebbero il risultato del SELECT dell'utente U1, gli verrà impedito di farlo. Un messaggio di errore gli comunicherà che l'inserimento non è possibile. Diventerà possibile una volta che l'utente U2 avrà convalidato la propria transazione.
I quattro livelli SQL di isolamento delle transazioni non sono disponibili in tutti gli SGBD. Il livello di isolamento predefinito è solitamente la lettura confermata (Committed Read). Il livello di isolamento desiderato per una transazione può essere specificato esplicitamente quando una transazione esplicita viene creata da un client .NET.
9.6.2. Il sistema di gestione delle transazioni API
Una connessione implementa l'interfaccia IDbConnection presentata al paragrafo 9.3.3. Questa interfaccia dispone del seguente metodo:
M | avvia una transazione. |
Questo metodo ha due firme:
- IDbTransaction BeginTransaction() : avvia una transazione e restituisce l'oggetto IDbTransaction per controllarla
- IDbTransaction BeginTransaction(IsolationLevel level) : specifica anche il livello di isolamento richiesto per la transazione. level prende i suoi valori dalla seguente enumerazione:
la transazione può leggere i dati scritti da un'altra transazione che quest'ultima non ha ancora convalidato - evitare questa opzione | |
la transazione non può leggere i dati scritti da un'altra transazione che non ha ancora convalidato. Tuttavia, i dati letti due volte di seguito dalla transazione potrebbero cambiare (letture non ripetibili) perché un'altra transazione potrebbe averli modificati nel frattempo (le righe lette non sono bloccate - solo quelle aggiornate lo sono). Inoltre, un'altra transazione potrebbe aver aggiunto righe (righe fantasma) che saranno incluse nella seconda lettura. | |
le righe lette dalla transazione sono bloccate allo stesso modo delle righe aggiornate. Ciò impedisce ad un'altra transazione di modificarle. Non impedisce l'aggiunta di righe. | |
le tabelle utilizzate dalla transazione sono bloccate, impedendo l'aggiunta di nuove righe da parte di un'altra transazione. Tutto avviene come se la transazione fosse l'unica in esecuzione. Riduce le prestazioni poiché le transazioni non funzionano più in parallelo. | |
la transazione opera su una copia dei dati effettuata al momento T. Utilizzata quando la transazione è di sola lettura. Fornisce lo stesso risultato di serializable evitando i relativi costi. |
Una volta avviata, la transazione è controllata da IDbTransaction, un'interfaccia con le seguenti P proprietà e M metodi:
Nome | Tipo | Ruolo |
P | ID della connessione bConnection che supporta la transazione | |
M | convalida la transazione: i risultati delle istruzioni SQL emesse nella transazione vengono copiati nel database . | |
M | annulla la transazione: i risultati delle istruzioni SQL eseguite nella transazione non vengono salvati nel database. |
9.6.3. Il programma di esempio
Torniamo al progetto precedente e diamo un'occhiata al programma [Transactions.cs] :
![]() |
- in [1], il progetto.
- in [2], il progetto è configurato per eseguire [Transactions.cs]
Il codice per [Transactions.cs] è il seguente:
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlServerCe;
using System.Text;
namespace Chap7 {
class Transactions {
static void Main(string[] args) {
// using the configuration file
string connectionString = null;
try {
// connecting chain
connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// displays
Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
// create a table of 2 items with the same name
Article[] articles = new Article[2];
for (int i = 1; i <= articles.Length; i++) {
articles[i - 1] = new Article(0, "article", i * 100, i * 10, i);
}
// handle any exceptions
try {
Console.WriteLine("Insertion sans transaction...");
// the table of items is first inserted into the database without a transaction
ExecuteUpdate(connectionString, "delete from articles");
try {
InsertArticlesOutOfTransaction(connectionString, articles);
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
// we do the same thing again, but in a transaction this time
Console.WriteLine("\n\nInsertion dans une transaction...");
ExecuteUpdate(connectionString, "delete from articles");
InsertArticlesInTransaction(connectionString, articles);
ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
// insert item table without transaction
static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
....
}
// insert an array of items into a transaction
static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
....
}
// execute an update request
static void ExecuteUpdate(string connectionString, string requête) {
....
}
// execute a Select query
static void ExecuteSelect(string connectionString, string requête) {
...
}
// reader display
static void AfficheReader(IDataReader reader) {
...
}
}
}
}
- righe 12-19: la stringa di connessione al database SQLServer Ce viene letta in [App.config]
- righe 25-28: viene creato un array di due oggetti Article. Questi due articoli hanno lo stesso nome "article". Oppure, il database [dbarticles.sdf] ha un vincolo di unicità sulla colonna [name] (cfr. paragrafo 9.3.1). Questi due articoli non possono quindi essere presenti contemporaneamente nel database. I due articoli con il nome "article" vengono aggiunti alla tabella articles. Si verificherà quindi un problema, ovvero un'eccezione generata dal SGBD e trasmessa dal suo connettore ADO.NET. Per dimostrare l'effetto della transazione, i due articoli verranno inseriti in due contesti diversi:
- innanzitutto senza alcuna transazione. Ricordiamo qui che, in questo caso, SQLServer Compact funziona in modalità autocommit, ovvero inserisce ogni comando SQL in una transazione implicita. Il primo articolo verrà inserito. Il secondo no.
- poi in una transazione esplicita che incapsula i due inserimenti. Poiché il secondo inserimento fallirà, il primo verrà annullato. Alla fine, non verrà effettuato alcun inserimento.
- riga 33: la tabella articles viene svuotata
- riga 35: inserimento dei due articoli senza una transazione esplicita. Poiché sappiamo che il secondo inserimento genererà un'eccezione, questa viene gestita da un try / catch
- riga 46: tabella "display articles"
- righe 44-46: la stessa sequenza viene ripetuta, ma questa volta viene utilizzata una transazione esplicita per eseguire gli inserimenti. L'eccezione riscontrata viene qui gestita da InsertArticlesInTransaction.
- righe 54-56: il metodo InsertArticlesOutOfTransaction corrisponde al programma InsertArticles [Parametres.cs] esaminato in precedenza.
- righe 64-66: il metodo ExecuteUpdate è lo stesso di cui sopra. L'istruzione SQL viene eseguita in una transazione implicita. Ciò è possibile qui perché sappiamo che, in questo caso, SQL Server Compact funziona in modalità autocommit.
- righe 69-71: idem per il metodo ExecuteSelect.
Il metodo InsertArticlesInTransaction è il seguente:
// insert an array of items into a transaction
static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
// opening connection
connexion.Open();
// control configuration
string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
// command compilation
sqlCommand.Prepare();
// transaction
SqlCeTransaction transaction = null;
try {
// start of transaction
transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
// the SQL command must be executed in this transaction
sqlCommand.Transaction = transaction;
// line insertion
for (int i = 0; i < articles.Length; i++) {
// parameter initialization
sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
// request execution
sqlCommand.ExecuteNonQuery();
}
// validate the transaction
transaction.Commit();
Console.WriteLine("transaction validée...");
} catch {
// we undo the transaction
if (transaction != null)transaction.Rollback();
Console.WriteLine("transaction invalidée...");
}
}
}
Ci limiteremo a illustrare le differenze tra questa funzione e la funzione InsertArticles del programma [Parametres.cs] esaminato in precedenza:
- riga 16: viene dichiarata una transazione SqlCeTransaction.
- righe 17, 35: try / catch per gestire l'eccezione che si verificherà alla fine del secondo inserimento
- riga 19: viene creata la transazione. Appartiene alla connessione corrente.
- riga 21: il comando SQL viene impostato nella transazione
- righe 23-31: vengono effettuati gli inserimenti
- riga 33: tutto è andato bene - la transazione è stata convalidata - gli inserimenti saranno ora definitivamente integrati nel database.
- riga 37: si è verificato un problema. La transazione viene annullata, se esisteva.
L'esecuzione fornisce i seguenti risultati:
- riga 4: visualizzata da ExecuteUpdate("delete from articles") - non c'erano righe nella tabella
- riga 5: l'eccezione causata dal secondo inserimento. Il messaggio indica che il vincolo UQ__ARTICLES__0000000000000010 non è stato verificato. È possibile ottenere ulteriori informazioni controllando le proprietà del database:
![]() |
- in [1] nella vista [Database Explorer] di Visual Studio, abbiamo creato una connessione [2] al database [dbarticles.sdf]. Questo ha un indice UQ__ARTICLES__0000000000000010. Fare clic con il tasto destro sull'indice per accedere alle sue proprietà
- in [3,4], possiamo vedere che l'indice UQ__ARTICLES__0000000000000010 corrisponde a un vincolo di unicità sulla colonna [NOM]
- righe 7-11: la tabella mostra gli articoli dopo i due inserimenti. Non è vuota: il primo articolo è stato inserito.
- riga 15: visualizzata da ExecuteUpdate("delete from articles") - c'era una riga nella tabella
- riga 16: messaggio visualizzato da InsertArticlesInTransaction se la transazione fallisce.
- righe 18-20: mostrano che non è stato effettuato alcun inserimento. Il rollback della transazione ha annullato il primo inserimento.
9.7. Il metodo ExecuteScalar
9.7.1. Tra i metodi di IDbCommand descritti nel paragrafo 9.3.3, era presente il seguente metodo:
M | per eseguire un comando SQL Select restituisce un singolo risultato come in: select count(*) from articles. |
Qui mostriamo un esempio di come utilizzare questo metodo. Torna a:
![]() |
- in [1], il progetto.
- in [2], il progetto è configurato per eseguire [ExecuteScalar.cs]
Il programma [ExecuteScalar.cs] è il seguente:
...
namespace Chap7 {
class Scalar {
static void Main(string[] args) {
// using the configuration file
string connectionString = null;
...
// displays
Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
// creation of a 5-item table
Article[] articles = new Article[5];
for (int i = 1; i <= articles.Length; i++) {
articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i);
}
// handle any exceptions
try {
// insert the item table into a transaction
ExecuteUpdate(connectionString, "delete from articles");
InsertArticlesInTransaction(connectionString, articles);
ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
// average item prices
decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
// or the number of items
int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
Console.WriteLine("Nombre d'articles={0}", nbArticles);
} catch (Exception ex) {
// error msg
Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
}
}
// insert an array of items into a transaction
static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
...
}
// execute an update request
static object ExecuteScalar(string connectionString, string requête) {
using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
// opening connection
connexion.Open();
// request execution
return new SqlCeCommand(requête, connexion).ExecuteScalar();
}
}
// execute an update request
static void ExecuteUpdate(string connectionString, string requête) {
...
}
// execute a Select query
static void ExecuteSelect(string connectionString, string requête) {
...
}
// reader display
static void AfficheReader(IDataReader reader) {
...
}
}
}
- righe 14-17: creazione di un array di 5 articoli
- riga 22: la tabella articles viene svuotata
- riga 23: viene riempita con i 5 articoli
- riga 24: viene visualizzata
- riga 26: viene richiesto il prezzo medio degli articoli
- riga 29: richiede il numero di articoli
- riga 49: utilizzo del metodo [IDbCommand].ExecuteScalar() per calcolare ciascuno di questi valori.
I risultati sono i seguenti:
Le righe 15 e 16 mostrano i due valori restituiti da ExecuteScalar.
9.8. Applicazione di esempio - versione 7
Prendiamo l'applicazione di esempio IMPOTS. L'ultima versione è stata esaminata nel paragrafo 7.6. Si trattava della seguente applicazione a tre strati:
![]() |
- il livello [ui] era un'interfaccia grafica [A] e il livello [dao] era un file di testo [B].
- L'istanziazione dei livelli e l'integrazione nell'applicazione sono state gestite da Spring.
![]() |
Modifichiamo il livello [dao] in modo che recuperi i dati da un database.
9.8.1. Visita il database
Il contenuto del precedente file di testo [B] viene inserito in un database MySQL5. Vi mostreremo come farlo:
![]() |
- [1] :MySQL Administrator è stato avviato
- [2,3]: nell'area [Schemi], clicca con il tasto destro del mouse e seleziona l'opzione [Crea schema] per creare un nuovo database
- [4]: il database si chiamerà [bdimpots]
- [5]: è stato aggiunto alle basi della zona [Schemata].
![]() |
- [6,7]: clicca con il tasto destro sulla tabella e seleziona l'opzione [Crea nuova tabella] per creare una tabella
- [8]: la tabella si chiamerà [slices]. Avrà le colonne [id, limit, coeffR, coeffN].
- [9,10]: [id] è una chiave primaria di tipo INTEGER e ha l'attributo AUTO_INCREMENT [10]: il SGBD riempirà questa colonna quando verranno aggiunte righe.
- Le colonne [limit, coeffR, coeffN] sono di tipo DOUBLE.
- [11,12]: la nuova tabella appare nella scheda [Schema Tables] del database.
![]() |
- [13,14]: per aggiungere dati alla tabella
- [15]: [Query Browser] è stato avviato
- [16]: dati inseriti e convalidati per le colonne [limit, coeffR, coeffN]. La colonna [id] è stata compilata dall'SGBD. La convalida è stata eseguita con [17].
![]() |
- Sempre in [Query Browser] [18], eseguiamo la query [20] [19]. Questo crea un utente 'admimpots' con password 'mdpimpots' e gli assegna tutti i privilegi (concedi tutti i privilegi) su tutti gli oggetti nel database bdimpots (su bdimpots.*). Questo ci permetterà di lavorare sul database [bdimpots] con l'utente [admimpots] anziché con l'amministratore [root].
9.8.2. La soluzione Visual Studio
![]() |
Seguiremo lo stesso approccio utilizzato per la versione 5 dell'applicazione di esempio (vedere il paragrafo 6.4). Realizzeremo gradualmente la seguente soluzione Visual Studio:
![]() |
- in [1]: la soluzione ImpotsV7 è composta da tre progetti, uno per ciascuno dei tre livelli dell'applicazione
- in [2]: il progetto [dao] nel livello [dao], che ora utilizzerà un database
- in [3]: il progetto [metier] per il livello [metier]. Riprendiamo qui il livello [metier] della versione 5, descritto al paragrafo 6.4.4.
- in [4]: progetto [ui] del livello [ui]. Prendiamo qui il livello [ui] della versione 6, descritto nel paragrafo 7.6.
Ci affidiamo a ciò che abbiamo imparato per recuperare due livelli già scritti, i livelli [ui] e [metier]. Ciò è reso possibile dall'architettura a livelli che abbiamo scelto. Tuttavia, avremo bisogno del codice sorgente per i livelli [ui] e [metier]. Non è possibile accontentarsi delle DLL dei livelli. Quando, nella versione 5, è stata creata la DLL del livello [metier], questa presentava una dipendenza dalla DLL del livello [dao]. Tale dipendenza era hard-coded nella DLL del livello [metier] (nome della DLL del livello [dao], versione, token di identità, ecc.). Ad esempio, la DLL della versione 5 [ImpotsV5-metier.dll] funzionerà solo con la DLL [ImpotsV5-dao.dll] con cui è stata compilata. Se la DLL del livello [dao] viene modificata, il livello [metier] deve essere ricompilato per creare una nuova DLL. Lo stesso vale per il livello [ui]. I livelli [ui] e [metier] non saranno quindi modificati, ma saranno ricompilati per funzionare con la DLL del nuovo livello [dao].
9.8.3. Il livello [dao]
![]() |
![]() |
Riferimenti del progetto (vedi [1] nel progetto)
- nunit.framework : per i test NUnit
- System.Configuration: per utilizzare il file di configurazione [App.config]
- System.Data: poiché utilizziamo un database.
Le entità (vedi [2] nel progetto)
Le classi [TrancheImpot] e [ImpotException] sono quelle delle versioni precedenti.
Il livello [dao] (vedi [3] nel progetto)
L'interfaccia [IImpotDao] non è cambiata:
using Entites;
namespace Dao {
public interface IImpotDao {
// tax brackets
TrancheImpot[] TranchesImpot{get;}
}
}
La classe di implementazione [DataBaseImpot] per questa interfaccia è la seguente:
using System;
using System.Collections.Generic;
using System.Data.Common;
using Entites;
namespace Dao {
public class DataBaseImpot : IImpotDao {
// tax brackets
private TrancheImpot[] tranchesImpot;
public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }
// manufacturer
public DataBaseImpot(string factory, string connectionString, string requête) {
// factory: the factory of the target SGBD
// connectionString: connection string to tax bracket base
// handle any exceptions
try {
// we retrieve a generic connector for the SGBD
DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
using (DbConnection connexion = connecteur.CreateConnection()) {
// connection configuration
connexion.ConnectionString = connectionString;
// opening connection
connexion.Open();
// configuration Command
DbCommand sqlCommand = connecteur.CreateCommand();
sqlCommand.CommandText = requête;
sqlCommand.Connection = connexion;
// request execution
List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
using (DbDataReader reader = sqlCommand.ExecuteReader()) {
while (reader.Read()) {
// a new tax trance is created
listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
}
}
// put the tax brackets in your instance
tranchesImpot = listTrancheImpot.ToArray();
}
} catch (Exception ex) {
// encapsulate the exception in a ImpotException type
throw new ImpotException("Erreur de lecture des tranches d'impôt", ex) { Code = 101 };
}
}
}
}
- riga 7: la classe [DataBaseImpot] implementa l'interfaccia [IImpotDao].
- riga 10: implementazione del metodo dell'interfaccia [TranchesImpot]. Restituisce semplicemente un riferimento alla tabella delle fasce di imposta della riga 9. Questa tabella verrà costruita dal costruttore della classe.
- riga 13: il builder. Utilizza un connettore generico (vedi paragrafo 9.4.5) per valutare il database delle fasce di imposta. Il builder riceve tre parametri:
- il nome della "fabbrica" alla quale richiederà le classi per connettersi al database, emettere comandi SQL e valutare il risultato di un Select.
- la stringa di connessione che deve utilizzare per connettersi al database
- l'ordine SQL Select che deve eseguire per ottenere le fasce di imposta.
- riga 19: richiede un connettore factory
- riga 20: crea una connessione con questo connettore. Viene creata ma non è ancora operativa
- riga 22: la stringa di connessione viene inizializzata. Ora è possibile connettersi.
- riga 24: connettersi
- riga 26: richiede un oggetto [DbCommand] dal connettore per eseguire un comando SQL
- riga 27: imposta il comando SQL da eseguire
- riga 28: imposta la connessione su cui eseguirlo
- riga 30: viene creato un elenco [listTrancheImpot] di oggetti di tipo [TrancheImpot] vuoto.
- riga 31: viene eseguito il comando SQL Select
- righe 32-35: viene utilizzato l'oggetto [DbDataReader] risultante da Select. Ogni riga della tabella dei risultati di Select viene utilizzata per istanziare un oggetto di tipo [TrancheImpot], che viene aggiunto all'elenco [listTrancheImpot].
- riga 38: l'elenco di oggetti di tipo [TrancheImpot] viene trasferito alla tabella alla riga 9.
- righe 40-43: qualsiasi eccezione viene incapsulata nel tipo [ImpotException] e le viene assegnato il codice di errore 101 (arbitrario).
Il test [Test1] (vedere [4] nel progetto)
La classe [Test1] visualizza semplicemente le fasce di imposta sullo schermo. Si tratta della classe già utilizzata nella versione 5 (paragrafo 6.4.3) ad eccezione dell'istruzione che istanzia il livello [dao] (riga 14).
using System;
using Dao;
using Entites;
using System.Configuration;
namespace Tests {
class Test1 {
static void Main() {
// create the [dao] layer
IImpotDao dao = null;
try {
// layer creation [dao]
dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
} catch (ImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
// display tax brackets
TrancheImpot[] tranchesImpot = dao.TranchesImpot;
foreach (TrancheImpot t in tranchesImpot) {
Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
}
}
}
}
La riga 14 utilizza il seguente file di configurazione [App.config]:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" />
</connectionStrings>
<appSettings>
<add key="requete" value="select limite, coeffr, coeffn from tranches"/>
<add key="factoryMySql5" value="MySql.Data.MySqlClient"/>
</appSettings>
</configuration>
- riga 4: stringa di connessione al database MySQL5. Si noti che è l'utente [admimpots] a stabilire la connessione.
- riga 8: la "factory" per lavorare con il SGBD MySQL5
- riga 7: query SQL Select per ottenere le fasce di imposta.
Il progetto è configurato per eseguire [Test1.cs] :

L'esecuzione del test fornisce i seguenti risultati:
Il test NUnit [NUnit1] (vedere [4] nel progetto)
Il test unitario [NUnit1] è quello già utilizzato nella versione 5 (paragrafo 6.4.3) ad eccezione dell'istruzione che istanzia il livello [dao] (riga 16).
using System;
using System.Configuration;
using Dao;
using Entites;
using NUnit.Framework;
namespace Tests {
[TestFixture]
public class NUnit1 : AssertionHelper{
// layer [dao] to be tested
private IImpotDao dao;
// manufacturer
public NUnit1() {
// dao] layer initialization
dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
}
// test
[Test]
public void ShowTranchesImpot(){
// display tax brackets
TrancheImpot[] tranchesImpot = dao.TranchesImpot;
foreach (TrancheImpot t in tranchesImpot) {
Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
}
// some tests
Expect(tranchesImpot.Length,EqualTo(7));
Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6));
Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6));
Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6));
}
}
}
Per eseguire questo test unitario, il progetto deve essere di tipo [Class Library] :
![]() |
- in [1]: la natura del progetto è stata modificata
- in [2]: la DLL generata si chiamerà [ ImpotsV7-dao.dll]
- in [3]: dopo la generazione del progetto (F6), la cartella [dao/bin/Release] contiene la DLL [ImpotsV7-dao.dll]. Contiene anche il file di configurazione [App.config] rinominato [nome DLL].config. Questo è standard in Visual Studio.
La DLL [ImpotsV7-dao.dll] viene quindi caricata nel framework NUnit ed eseguita:
![]() |
- in [1]: test superati. Consideriamo ora operativo il livello [dao]. La sua DLL contiene tutte le classi del progetto, comprese le classi di test. Queste non sono più necessarie. Ricostruiamo la DLL per escludere le classi di test.
- in [2]: la cartella [tests] viene esclusa dal progetto
- in [3]: il nuovo progetto. Questo viene rigenerato premendo F6 per generare una nuova DLL. Questa DLL sarà utilizzata dai livelli [metier] e [ui] dell'applicazione.
9.8.4. Il [lavoro di e del livello]
![]() |
![]() |
- in [1], il progetto [metier] è diventato il progetto attivo della soluzione
- in [2]: riferimenti del progetto. Si noti il riferimento alla DLL del livello [dao] creato in precedenza. Questa procedura di aggiunta dei riferimenti è stata descritta nella versione 5, paragrafo 6.4.4.
- in [3]: il livello [metier]. Si tratta del livello della versione 5, descritto nel paragrafo 6.4.4.
Il progetto [metier] è configurato per generare una DLL:
![]() |
- [1]: il progetto è di tipo "libreria di classi"
- [2]: la generazione del progetto produrrà la DLL [ImpotsV7-metier.dll] [3].
Il progetto viene generato (F6).
9.8.5. Il livello [ui]
![]() |
![]() |
- in [1], il progetto [ui] è diventato il progetto attivo per la soluzione
- in [2]: riferimenti del progetto. Si notino i riferimenti alla DLL dei livelli [dao] e [metier].
- in [3]: il livello [ui]. Si tratta del livello della versione 6 descritto nel paragrafo 7.6.
- in [4], il file di configurazione [App.config] è simile a quello della versione 6, differendo solo nel modo in cui il livello [dao] viene istanziato da Spring:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="MySql.Data.MySqlClient"/>
<constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
<constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- righe 11-25: configurazione Spring
- righe 15-24: oggetti istanziati da Spring
- righe 16-20: istanziamento del livello [dao]
- riga 16: il livello [dao] viene istanziato dalla classe [Dao.DataBaseImpot], che si trova nella DLL [ImpotsV7-Dao]
- righe 17-19: i tre parametri (factory del SGBD utilizzato, stringa di connessione, richiesta SQL) da fornire al costruttore della classe [Dao.DataBaseImpot]
- righe 21-23: istanziazione del livello [metier]. Si tratta della stessa configurazione della versione 6.
Test
Il progetto [ui] è configurato come segue:
![]() |
- [1]: il progetto è di tipo "Applicazione Windows"
- [2]: la generazione del progetto produrrà l'eseguibile [ImpotsV7-ui.exe]
Un esempio è riportato in [3].
9.8.6. Modifica database
![]() |
Il livello [dao] sopra riportato è stato scritto utilizzando un connettore generico e un database MySQL5. Proponiamo qui di passare a una base SQL Server Compact per dimostrare che cambierà solo la configurazione.
L' i di base di SQL Server Compact sarà la seguente:
![]() |
- [1]: il database [dbimpots.sdf] nella vista [DataBase Explorer] di Visual Studio [2]. È stato creato senza password.
- [3]: la tabella [data] contenente i dati. Abbiamo scelto deliberatamente nomi diversi per la tabella e le colonne rispetto a quelli utilizzati con il database MySQL5, per ribadire l'importanza di inserire tali dettagli nel file di configurazione piuttosto che nel codice.
- [4]: la colonna [id] è la chiave primaria e ha l'attributo Identity: sarà il SGBD ad assegnarne i valori.
- [5]: contenuto della tabella [data].
![]() |
- [6]: il database [dbimpots.sdf] è stato inserito nella cartella del progetto [ui] e integrato in questo progetto.
- [7]: il database [dbimpots.sdf] verrà copiato nella cartella di esecuzione del progetto.
Il file di configurazione [App.config] per il nuovo database è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<!--
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="MySql.Data.MySqlClient"/>
<constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
<constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
</object>
-->
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
<constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
<constructor-arg index="2" value="select data1, data2, data3 from data"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- righe 23-27: configurazione del livello [dao] per l'utilizzo del database [dbimpots.sdf].
I risultati sono identici a quelli precedenti. Si noti il vantaggio di utilizzare un connettore generico per rendere il livello [dao] insensibile alle modifiche nel SGBD. Abbiamo visto, tuttavia, che questo connettore non è adatto a tutte le situazioni, in particolare quelle che coinvolgono query parametrizzate. In tali casi, esistono altre soluzioni come quella menzionata sopra, framework di accesso ai dati di terze parti (Spring, iBatis, NHibernate, LINQ, ...).
9.9. Per saperne di più...
- LINQ è trattato in numerose pubblicazioni, tra cui il libro: C# 3.0 in a Nutshell, di Joseph e Ben Albahari, edito da O'Reilly, già citato nell'introduzione a questo documento.
- iBatis è presentato nel libro: iBatis in Action, di Clinton Begin, edito da Manning Editions
- Nhibernate in Action, pubblicato da Manning, è previsto in uscita a luglio 2008
Spring, iBatis e NHibernate dispongono di manuali di riferimento disponibili sui siti web dei vari framework.
































































