Tutorial ADO.NET & Oracle
Parte 3: L'uso di ADO.NET
INTRODUZIONE
Le funzionalità fornite da ADO.NET sono
molteplici, quali tra queste sono utili per le nostre esigenze? Questo
tutorial cerca la risposta a tale domanda, indirizza l'apprendimento e
l'utilizzo di ADO.NET secondo le esigenze tipiche di una Software Factory.
Il tutorial perciò descrive l'architettura di ADO.NET, gli obiettivi e
le finalità della tecnologia. Inoltre suggerisce le parti di maggiore
utilità e le linee guida per l'utilizzo di ADO.NET e le specificità legate
ad Oracle. Questa terza parte si concentra sull'uso di ADO.NET.
APERTURA DELLA CONNESSIONE
Il modello di accesso ai dati di tipo disconnesso
richiede che una connessione venga riaperta più volte durante l'esecuzione
di una applicazione. Per ottenere tempi accettabili diventa fondamentale
attivare il Connection Pooling di ADO.NET. Per attivate il connection pooling,
a differenza da quanto accadeva con ADO, non è necessario usare i servizi
di COM+, bensì è sufficiente attivare il servizio tramite la stringa di
connessione. Ecco un esempio per il driver OleDb:
string cnnStr = "Provider=MSDAORA;User ID=GINO;Password=GUATENALBARE;Data Source=HX3_SVI;" + "OLE DB Services=-1"; System.Data.OleDb.OleDbConnection cnn = new System.Data.OleDb.OleDbConnection(cnnStr); cnn.Open(); XXX
|
A tale proposito si veda:
Connection Pooling for the OLE DB .NET Data Provider
Enabling and Disabling OLE DB Services
CHIUSURA DELLA CONNESSIONE
A differenza di quanto avveniva con VB ed ADO, le
connessioni lasciate aperte non vengono chiuse
automaticamente quando l'oggetto viene rilasciato (quando la
variabile viene messa a null, quando non vi sono più oggetti che vi fanno
riferimento o quando l'oggetto che la contiene non viene più referenziato
da nessuno).
Il motivo sta nella gestione automatica della memoria di .NET con
la tecnica del Garbage Collector. La conseguenza è che le connessioni
che non vengono chiuse esplicitamente dal programmatore restano aperte
dopo che l'elaborazione è terminata per un tempo indefinito sprecando
risorse preziose sul Server e perciò minando alla base la scalabilità della
applicazione.
Ogni connessione che viene aperta direttamente
o indirettamente attraverso un DataReader, un DataAdapter o un Command
va sempre chiusa esplicitamente
qualsiasi cosa accada.
|
Per una corretta gestione della apertura e chiusura diretta
od indiretta delle connessioni:
- Non dichiarare field o property di tipo Connection (ne pubbliche
ne private, ne statiche ne di istanza) in una classe
- Non aprire connessioni nel costruttore
- Ogni metodo che apre una connessione dovrà sempre chiuderla prima
di terminare, anche qualora avvenga una eccezione imprevista
Se si programma correttamente l'apertura e la chiusura delle connessioni
queste situazioni non si presenteranno:
- Non dovrà essere necessario codificare la chiusura della connessione
nel distruttore (il Dispone, il Finalize o il metodo ~MiaClasse)
- Nessun metodo che usa una connessione la troverà già aperta da
un altro metodo (se non da lui direttamente richiamato)
Per chiudere una connessione anche in caso di eccezione usare il try-finally:
System.Data.OleDb.OleDbConnection cnn = new System.Data.OleDb.OleDbConnection(cnnStr); System.Data.OleDb.OleDbDataReader drCustomers = null;
try { // Apro la connessione cnn.Open(); // Eseguo un comando e ottengo un'istanza di DataReader drCustomers = cmdCustomers.ExecuteReader(); // ... elaborazioni varie sul db } finally { //Qualsiasi cosa accada... if (drCustomers != null) drCustomers.Close(); if (cnn != null) cnn.Close(); } XXX
|
Oppure, equivalentemente, usare lo statement using di C#:
using(System.Data.OleDb.OleDbConnection cnn = new System.Data.OleDb.OleDbConnection(cnnStr)) using(System.Data.OleDb.OleDbDataReader drCustomers = null) { // Apro la connessione cnn.Open(); // Eseguo un comando e ottengo un'istanza di DataReader drCustomers = cmdCustomers.ExecuteReader(); // ... elaboraziuoni varie sul db } XXX
|
USO DI PARAMETRI TIPIZZATI
Gli oggetti Command permettono l'esecuzione di comandi
SQL siano essi Select, Update, Delete, Insert o Stored Procedure.
Se nelle vecchie applicazioni VB poteva essere conveniente utilizzare
i parametri tipizzati per richiamare Stored Procedure od eseguire query,
nelle nuove applicazioni Web i parametri
tipizzati sono indispensabili. La prima ragione è la sicurezza,
infatti costruire comandi SQL combinando comando e parametri apre un buco
alla sicurezza (uno dei 10 buchi alla sicurezza utilizzati più di frequente
per violare un sito). La seconda ragione è dovuta al fatto che nelle nuove
applicazioni il numero di strati software che attraversano i dati inseriti
dall'utente sono molteplici ed ognuno ha una propria impostazione internazionale:
- Il Browser ha le impostazioni internazionali dipendenti dall'utente,
dalla sua nazionalità e le sue impostazioni personalizzate sulle quali
lo sviluppatore e l'installatore non hanno il benche minimo controllo. Infatti
non vi sarà mai capitato di navigare in internet e trovare un sito che
vi chiede di modificare le vostre impostazioni (del Browser, dell'utente
del sistema operativo, oppure del sistema operativo stesso???)
- Il Web Server e la applicazione Web hanno una loro impostazione
internazionale. In questo .NET ci viene in aiuto in quanto le vecchie applicazioni
ASP hanno le impostazioni internazionali che dipendono dalla combinazione
di diverse variabili (le impostazioni del sistema operativo al momento della
sua installazione, quelle dell'utente System, quelle dell'utente in cui
gira il processo IIS della applicazione e anche quelle dell'utente in cui
gira in Thread della richiesta della pagina qualora sia in uso l'impersonificazione)
- Il client Oracle
- Il Server Oracle
Usare stringhe SQL piuttosto che comandi parametrici tipizzati rende
la soluzione dipendente dalle impostazioni internazionali dei vari strati
e quindi ingestibile. La scrittura di comandi parametrizzati richiede
tuttavia del tempo, ma le capacità di Visual Studio .NET come generatore
di codice sono notevoli e sono disponibili attraverso diversi Wizard.
USO DI DATASET
I DataSet possono essere utilizzati con più tabelle
e relazioni di integrità referenziale tra loro. Tuttavia questo modo di
impiego impone al programmatore di gestire seguendo le dipendenze gerarchiche
delle tabelle e l'ordine delle operazioni di salvataggio dei dati e della
loro cancellazione.
A meno che non si stia realizzano un applicazione che accede a db distribuiti
su diversi server in Internet per ottenere dati tra loro correlati, questa
complessità è eccessiva in relazione alle comuni esigenze di una Software
Factory, infatti per la tipologia di programmi di maggiore interesse è sufficiente
l'impiego di DataSet popolati con una singola DataTable. La DataTable potrà
essere utilizzata per popolare una DataGrid in Binding, per leggere le
righe di un documento (ossia gli oggetti figli in un una relazione UML
di tipo Composizione o per dirla semplicemente le righe di una testata),
per restituire il risultato da una selezione fatta. Quindi è utile concentrare
la formazione sull'uso della DataTable, creazione, popolazione, modifica,
aggiornamento e binding e tralasciare l'uso del DataSet usato con più DataTable.
DATASET FORTEMENTE TIPIZZATI
I criteri di scelta tra l'utilizzo di DataSet fortemente
tipizzati piuttosto che non tipizzati dipende dal contesto di impiego
del DataSet (per quanto detto al punto precedente a noi interessa la singola
DataTable del DataSet):
- DataSet usato internamente per implementare la persistenza del
singolo oggetto o di un documento (ossia il caso di relazione UML di tipo
Composizione , detta anche Aggregazione Forte, con
gli oggetti figli privi di una propria identità forte);
- DataSet usato come parametro di un metodo pubblico per restituire
il risultato di una elaborazione
Uso Interno
Per implementare la persistenza del singolo oggetto,
la scelta è tra
- l'uso di un DataReader per leggere i dati e di Command per eseguire
gli aggiornamenti
- l'uso di un DataSet tipizzato generato automaticamente dal Wizard
di VS.NET
Nel primo caso ho una soluzione più efficiente ma che richiede uno
sforzo al programmatore per implementare tutto il codice di accesso al database
sia in scrittura che in lettura. Nel secondo caso ho una soluzione meno
efficiente a causa della maggiore complessità di strutture mantenute in
memoria, tuttavia il Wizard fornisce l'implementazione di tutto il codice
di accesso e anche del locking ottimistico (che richiede la memorizzazione
dei valori originali e una strutturazione particolare delle query). L'uso
di un DataSet non tipizzato è la scelta che meglio di tutte unisce i difetti
delle due opzioni di cui sopra, infatti è meno efficiente e richiede comunque
l'intervento di codifica da parte dello sviluppatore.
Per implementare la persistenza di un insieme di oggetti (N.B. questo
accade solo nel caso di oggetti figli di una relazione UML di tipo
Composizione in quanto in tutti gli altri casi ogni oggetto deve
assolvere personalmente e singolarmente alla propria persistenza), la scelta
è tra:
- l'uso di un DataSet tipizzato generato automaticamente dal Wizard
di VS.NET
- l'uso di un DataSet non tipizzato
Nel primo caso, come già detto, ho una soluzione più conveniente in
quanto riduce la necessità di codifica, inoltre la tipizzazione forte del
DataSet aumenta l'efficacia del test e riduce i costi di manutenzione ed
evoluzione del codice. Nel secondo caso perdo i benefici di testabilità, manutenibilità
ed evolvibilità del codice ma guadagno in flessibilità in quando risulta
più facile estendere o configurare i dati. E' indispensabile notare che
la flessibilità deve essere scelta solo a seguito di precisi requisiti
e successiva analisi che individua il grado, la direzione ed i confini di
questa flessibilità. Senza opportuna analisi non si chiama più flessibilità
bensì mollezza del codice e ciò che potrebbe a prima vista apparire una scelta
furba diventa invece una scelta ingenua.
Uso Pubblico
Qualora il DataSet (ricordo che a noi interessa in realtà la singola
DataTable) venga restituito come membro di un'interfaccia pubblica, la scelta
plausibile è tra:
- l'uso di un DataSet tipizzato generato automaticamente dal Wizard
di VS.NET
- l'uso di un DataSet non tipizzato
In entrambi i casi è consigliato che la DataTable del DataSet contenga
come prima colonna l'Oid (l'identificativo univoco che rappresenta l'identità
dell'oggetto), conoscendo il valore dell'Oid ed indicandolo sarà possibile
richiedere l'istanza dell'oggetto corrispondente ad una riga del DataTable.
A questo punto resta da capire che uso si vuole fare delle restanti
colonne:
- I dati nel DataSet sono destinati alla semplice visualizzazione
e quindi il codice non fa accesso a singole colonne oltre l'Oid?
- Oppure il codice accede esplicitamente a qualche colonna?
Nel primo caso è possibile usare un DataSet non tipizzato, in questo
caso l'implementatore potrà variare le colonne che verranno passate tramite
il DataSet senza alcun preavviso. Nel secondo caso è necessario usare un
DataSet fortemente tipizzato, in questo caso la forte tipizzazione traspare
dall'interfaccia pubblica e quest'ultima diventa una esplicita dichiarazione
dell'esistenza delle colonne che quindi non potranno essere variate dall'implementatore
senza opportuno avviso. L'utilizzatore del DataSet avrà quindi le garanzie
necessarie a testare, manutenere ed evolvere il proprio codice senza essere
soggetto all'estro occasionale.
TIPI NATIVI DEL DB E DI C#
I tipi dato utilizzati nel Command o nel DataReader
sono specifici del .NET Data Provider impiegato. Quando si leggono o si
scrivono valori per tali tipi dato (per esempio quando si assegna il valore
di un parametro o si legge il valore di una colonna) lo si fa da C# e più
in generale da codice .NET e quindi avviene una conversione tra i tipi nativi
del provider e quelli del C#. Lo stesso accade quando si popolano le DatTable
le cui colonne possono contenere solo i tipi dato nativi di .NET.
A tale propositi si veda:
Mapping .NET Data Provider Data Types to .NET Framework Data Types
.
Ogni volta che avviene questa conversione si corrono tre rischi:
- potrebbe avvenire una perdita di precisione
- il valore potrebbe non essere rappresentabile nel dominio del
tipo di destinazione
- il valore di partenza potrebbe essere null (caso particolare
del punto 2)
Per evitare questi rischi e quindi ridurre l'impedenza tra i tipi dato
nativi del provider e quelli di C# ogni .NET Data Provider mette a disposizione
dei tipi dato che fanno da ponte. Questi tipi dato mantengono le stesse caratteristiche
di efficenza dei tipi dato nativi di C# e la medesima semantica per
valore, ma aggiungono la gestione del valore null e operazioni di conversione
controllate.
Il provider per OleDb da utilizzare temporaneamente (come indicato
in
Scelta del Provider
) non implementa tali tipi, ma gli implementano il provider per Oracle
della MS e anche quello della Oracle. Per prendere comunque visione di tali
tipi per comprenderne l'utilità ed il funzionamento, vedere i rispettivi
Help.
Questi tipi, pur prevedendo la possibilità di gestire il valore null,
devono essere usate nel Data Layer e quindi all'interno dell'implementazione
del Business Layer ma non nell'interfaccia pubblica del Business layer.
Se così non fosse la scelta del Provider da dettaglio implementativo
verrebbe elevata a semantica di interfaccia.