Skip to content

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:

  1. modalità connessa
  2. modalità offline

In modalità connessa, l'applicazione

  1. apre una connessione alla fonte dati
  2. opera con la fonte dati in lettura/scrittura
  3. chiude la connessione

In modalità offline, l'applicazione

  1. apre una connessione alla fonte dati
  2. ottiene una copia in memoria di tutti o parte dei dati di origine
  3. chiude la connessione
  4. lavora con la copia in memoria dei dati di lettura/scrittura
  5. 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:

select col1, col2,...
da tabella1, tabella2,...
where condizione
order by espressione
...
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.
  1. Viene eseguito un join con tutte le tabelle dietro la parola chiave from
  2. Vengono mantenute solo le colonne dietro la parola chiave select
  3. Vengono mantenute solo le righe che soddisfano il where
  4. Le righe risultanti ordinate dalla parola chiave expression order by costituiscono il risultato della query. Questo risultato è una tabella.
insert into table(col1,col2, ...)
values (val1,val2, ...)
Inserisce una riga nella tabella. (col1, col2, ...) specifica le colonne della riga da inizializzare con i valori (val1, val2, ...).
aggiorna tabella
imposta col1=val1, col2=val2
where condizione
Aggiorna la tabella verificando la condizione (tutte le righe se non è specificato where). Per queste righe, col1 riceve il valore val1
cancella da tabella
dove condizione
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.

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Requête SQL (rien pour arrêter) : insert into articles(nom,prix,stockactuel,stockminimum) values('x',100,10,1)
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 100 10 1

Requête SQL (rien pour arrêter) : update articles set prix=prix*1.1 where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 110 10 1

Requête SQL (rien pour arrêter) : delete from articles where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
  • 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 :

  1. apre una connessione all'origine dati
  2. opera con l'origine dati in lettura/scrittura
  3. 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.

L'interfaccia IDbConnection

Utilizzata per gestire la connessione al database. I metodi M e le proprietà P di questa interfaccia saranno i seguenti:

Nome
Tipo
Ruolo
Stringa di connessione
P
catena di connessione alla base. Specifica tutti i parametri necessari per stabilire una connessione con una base specifica.
Apri
M
apre la connessione alla base definita da ConnectionString
Chiudi
M
chiude la connessione
BeginTransaction
M
avvia una transazione.
Stato
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:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

L'interfaccia e IDbCommand

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
TipoComando
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
CommandText
P
- testo del comando SQL da eseguire se CommandType= CommandType.Text
- il nome della procedura memorizzata da eseguire se CommandType= CommandType.StoredProcedure
Connection
P
ID della connessione IDbConnection da utilizzare per eseguire il comando SQL
Transazione
P
l'ID della transazione IDbTransaction in cui eseguire il comando SQL
Parametri
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.
ExecuteReader
M
per eseguire un'istruzione SQL SELECT. Il risultato è un oggetto IDataReader che rappresenta il risultato della SELECT.
ExecuteNonQuery
M
per eseguire un comando SQL Update, Insert, Delete. Restituisce il numero di righe interessate dall'operazione (aggiornate, inserite, eliminate).
ExecuteScalar
M
per eseguire un comando SQL Select restituisce un singolo risultato come in: select count(*) from articles.
CreateParameter
M
per creare i parametri IDbParameter dell'istruzione SQL.
Prepare
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:

// opening connection 
IDbConnection connexion=...
connexion.Open();
// order preparation
IDbCommand commande=new Command();
commande.Connection=connexion;
// select order execution
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// execute update, insert, delete commands
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// locking connection
connexion.Close();

L'interfaccia a IDataReader

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
FieldCount
P
il numero di colonne nella tabella IDataReader
GetName
M
GetName(i) restituisce il nome della colonna n° i della tabella IDataReader.
Item
P
Item[i] rappresenta la colonna n° i nella riga corrente della tabella IDataReader.
Read
M
passa alla riga successiva nella tabella IDataReader. Render Booleano True se la lettura è stata possibile, False in caso contrario.
Chiudi
M
chiude la tabella IDataReader.
GetBoolean
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.
Getvalue
M
Getvalue(i): restituisce il valore della colonna n. i nella riga corrente della tabella IDataReader come oggetto di tipo.
IsDBNull
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:

// opening connection 
IDbConnection connexion=...
connexion.Open();
// order preparation
IDbCommand commande=new Command();
commande.Connection=connexion;
// order execution select
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// operation results
while(reader.Read()){
    // operate current line
        ...
}
// lock reader
reader.Close();
// locking connection
connexion.Close();

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:

// connection initialization
IDbConnection connexion=...
// operation connection
try{
     // opening
    connexion.Open();
     // order picking
    IDbCommand commande=new Command();
    commande.Connection=connexion;
     // order execution select
    commande.CommandText="select ...";
    IDbDataReader reader=commande.ExecuteReader();
     // operation results
    try{
        while(reader.Read()){
        // operate current line
        ...
    }finally{
         // lock reader
        reader.Close();
    }
}catch(Exception ex){
     // management exception
    ...
}finally{
     // locking connection
    connexion.Close();
}
...

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 :

// operation connection
try{
    using(IDbConnection connexion=...){
         // opening
        connexion.Open();
         // order preparation
        IDbCommand commande=new Command();
        commande.Connection=connexion;
         // order execution select
        commande.CommandText="select ...";
        using(IDbDataReader reader=commande.ExecuteReader()){
             // operation results
            while(reader.Read()){
            // operate current line
            ...
         }// using IData
     }//using IDbConnection
}catch(Exception ex){
     // management exception
    ...
}
..
  • 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:

  1. Connessione al database
  2. Invio dei comandi SQL al database
  3. Elaborazione dei risultati dei comandi SQL
  4. 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:
/* choice of current database */
USE dbarticles;
/* create an item table */
CREATE TABLE ARTICLES (
    ID            INTEGER PRIMARY KEY AUTO_INCREMENT,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          DOUBLE PRECISION NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* table data insertion */
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3);
/* adding constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  • 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:

spazio dei nomi
MySql.Data.MySqlClient
classe Connection
MySqlConnection
classe Command
MySqlCommand
classe DataReader
MySqlDataReader

Il programma utilizza la stringa di connessione denominata dbArticlesMySql5 nel file [App.config]. L'esecuzione produce i seguenti risultati:

Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

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:

spazio dei nomi
System.Data.Odbc
classe Connection
OdbcConnection
classe Command
OdbcCommand
classe DataReader
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] :

Chaîne de connexion à la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 500,0000 10 5
2 pompe 10,0000 10 2
3 arc 610,0000 4 1
4 flèches - lot de 6 100,0000 12 20
5 combinaison de plongée 300,0000 8 2
6 Bouteilles d'oxygène 120,0000 10 5

Con stringa di connessione [dbArticlesOdbcMySql5] :

Chaîne de connexion à la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

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:

spazio dei nomi
System.Data.OleDb
classe Connection
OleDbConnection
classe Command
OleDbCommand
classe DataReader
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]:

Chaîne de connexion à la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 1202 5 2
2 arc 5000 10 2
3 canoé 1502 12 6
4 fusil 3000 10 2
5 skis nautiques 1800 5 2
6 essai3 3 3 3
7 cachalot 200000 1 0
8 léopard 500000 1 1
9 panthère 800000 1 1

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:

Name|Description|InvariantName|AssemblyQualifiedName|
---------------------------------------
Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc|System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb|System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient|System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient|System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91|
---------------------------------------
MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient|MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|
  • 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
Provider dati ODBC
System.Data.Odbc
Provider dati OleDb
System.Data.OleDb
Provider dati OracleClient
System.Data.OracleClient
Provider dati SqlClient
System.Data.SqlClient
Provider dati Microsoft SQL Server Compact
System.Data.SqlServerCe.3.5
Provider dati MySQL
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:

Provider factory : [MySql.Data.MySqlClient]
Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

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:

Provider factory : [System.Data.SqlServerCe.3.5]
Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

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:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Con MySQL5, la stessa query verrebbe scritta:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

C'è quindi una differenza nella sintassi. La proprietà dell'interfaccia IDbCommand descritta nel paragrafo 9.3.3 è la seguente:

Parametri
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

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

poiché contiene parametri con nome. È possibile utilizzare il ParameterName.

Il tipo IDataParameter non è compatibile con l'ordine SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

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:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

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
BigInt
Int64
 
DateTime
DateTime
 
Decimale
Decimale
 
Float
Doppio
 
Int
Int32
 
Moneta
Decimale
 
NChar
Stringa
stringa a lunghezza fissa
NVarChar
String
stringa a lunghezza variabile
Reale
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:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

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:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------


------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

117 article1 100 10 1
118 article2 200 20 2
119 article3 300 30 3
120 article4 400 40 4
121 article5 500 50 5
  • 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:

  1. 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.
  2. 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à:

  1. un utente U1 avvia una transazione su una tabella T
  2. un utente U2 avvia una transazione sulla stessa tabella T
  3. l'utente U1 modifica le righe nella tabella T ma non le convalida ancora
  4. l'utente U2 "vede" queste modifiche e prende decisioni basandosi su ciò che vede
  5. 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:

  1. un utente U1 avvia una transazione su una tabella T
  2. un utente U2 avvia una transazione sulla stessa tabella T
  3. l'utente U2 esegue un SELECT per ottenere la media di una colonna C delle righe in T che soddisfano una certa condizione
  4. l'utente U1 modifica (UPDATE) determinati valori nella colonna C di T e li convalida (COMMIT)
  5. 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:

BeginTransaction
M
avvia una transazione.

Questo metodo ha due firme:

  1. IDbTransaction BeginTransaction() : avvia una transazione e restituisce l'oggetto IDbTransaction per controllarla
  2. IDbTransaction BeginTransaction(IsolationLevel level) : specifica anche il livello di isolamento richiesto per la transazione. level prende i suoi valori dalla seguente enumerazione:
ReadUncommitted
la transazione può leggere i dati scritti da un'altra transazione che quest'ultima non ha ancora convalidato - evitare questa opzione
ReadCommitted
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.
RepeatableRead
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.
Serializable
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.
Snapshot
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
Connessione
P
ID della connessione bConnection che supporta la transazione
Commit
M
convalida la transazione: i risultati delle istruzioni SQL emesse nella transazione vengono copiati nel database .
Rollback
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:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Insertion sans transaction...
Il y a eu 0 ligne(s) modifiée(s)
Erreur d'accès à la base de données (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ])

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

126 article 100 10 1


Insertion dans une transaction...
Il y a eu 1 ligne(s) modifiée(s)
transaction invalidée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------
  • 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:

ExecuteScalar
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:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)
transaction validée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

145 article1 100 10 1
146 article2 200 20 2
147 article3 300 30 3
148 article4 400 40 4
149 article5 500 50 5
Prix moyen des articles=300
Nombre d'articles=5

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] :

Image

L'esecuzione del test fornisce i seguenti risultati:

1
2
3
4
5
6
7
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

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.