Tutorial ADO.NET & Oracle

Parte 3: L'uso di ADO.NET




P   a   r   t   e   1          -          P   a   r   t   e       2          -          P   a   r   t   e       3          -          P   a   r   t   e       4



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:
  1. Non dichiarare field o property di tipo Connection (ne pubbliche ne private, ne statiche ne di istanza) in una classe 
  2. Non  aprire connessioni nel costruttore
  3. 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:
  1. Non dovrà essere necessario codificare la chiusura della connessione nel distruttore (il Dispone, il  Finalize o il metodo ~MiaClasse)
  2. 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:
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):
  1. 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);
  2. 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:
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:
  1.  I dati nel DataSet sono destinati alla semplice visualizzazione e quindi il codice non fa accesso a singole colonne oltre l'Oid?
  2. 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:
  1. potrebbe avvenire una perdita di precisione
  2. il valore potrebbe non essere rappresentabile nel dominio del tipo di destinazione
  3. 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.



P   a   r   t   e   1          -          P   a   r   t   e       2          -          P   a   r   t   e       3          -          P   a   r   t   e       4