Transazioni
In ADO.NET è stato ampliato il supporto allo sviluppo di applicazioni transazionali, sia che si tratti di transazioni locali ad una singola fonte dati, che per quanto riguarda le transazioni distribuite. Queste ultime, che in questa versione del .NET Framework si appoggiano ancora ai Component Services di Windows (COM+) , verranno trattate dettagliatamente nei prossimi articoli.
Le transazioni locali invece, vengono create a partire da un oggetto Connection attraverso il metodo BeginTransaction() che restituisce un oggetto di tipo Transaction. Attraverso questo oggetto è possibile configurare il livello di isolamento della transazione con la proprietà IsolationLevel (ReadCommitted, ReadUncommitted, Serializable, ecc.), pilotando così la gestione della concorrenza tra le varie connessioni nell’accesso ai dati transazionali e il relativo modello di locking sulla base dati.
// Instanzio un nuovo oggetto Connection using(SqlConnection cnn = new SqlConnection(“connection string”)) { try { // Creo l’oggetto Command SqlCommand cmd1 = new SqlCommand("INSERT INTO Orders (...)", cnn); // Creo l’oggetto Command SqlCommand cmd2 = new SqlCommand("INSERT INTO [Order Details] (...)", cnn); // Apertura della connessione cnn.Open(); // Creo la transazione SqlTransaction myTran = cnn.BeginTransaction() // Associazione comandi - transazione cmd1.Transaction = myTrans; cmd2.Transacrion = myTrans; // Definisco il tipo di comando cmd.CommandType = CommandType.Text; // Eseguo il comando e ottengo un XmlReader cmd.ExecuteNonQuery(); // Commit della transazione myTrans.Commit(); } catch(Exception e) { // gestisco l’eccezione myTrans.Rollback(); } }
Una differenza rispetto a ADO 2.x è che i comandi che vengono eseguiti utilizzando una certa Connection, non vengono automaticamente arruolati in una transazione, ma occorre farlo esplicitamente. In questo modo sono state gettate le basi per una futura espansione di questa area nel momento in cui, nella prossima versione di SQL Server, verranno supportate transazioni nested.
Error & Info Handling
L’apertura della connessione verso una fonte dati e l’esecuzione di comandi sono operazioni naturalmente soggette al verificarsi di errori, che possono andare dalla mancanza di connettività di rete verso il db server fino alla violazione di qualche regola di mantenimento dell’intergrità dei dati codificata all’interno del database (chiave primaria, relazioni, ecc.). Occorre quindi prevedere per le funzioni che eseguono questo tipo di attività, un adeguato meccanismo di intercettazione e gestioni delle condizioni di errore, siano esse applicativa o di sistema. Il .NET Framework mette a disposizione degli sviluppatori un modello di gestione delle eccezioni comune a tutti i linguaggi, integrato nel Framework e costruito sul Structured Exception Handling di Windows. Lo statement try{...} catch{...} finally{...} consente quindi di costruire, nelle parti dell’applicazione che lo richiedono, una struttura di gestione degli errori che consente uno sviluppo molto più lineare di quanto avveniva nel mondo COM/VB6.
Nel blocco try{...} quindi verranno messe le istruzioni che si presume possano generare eccezioni come l’apertura della connessione, l’esecuzione di un comando o la chiamata ad un componente che a sua volta può generare una eccezione applicativa. Se una di queste operazioni fallisce, il controllo dell’applicazione passa al primo blocco catch{...} che si incontra che specifica all’interno della parentesi un tipo di eccezione “compatibile” con quella che si è verificata. Se ad esempio si è tentata l’apertura di una connessione con un db server che in questo momento non era disponibile, il runtime genererà una eccezione di classe SqlException o OleDbException a seconda del provider utilizzato, “farcendola” di tutte le informazioni che riguardano la specifica operazione fallita, che sono quindi rese disponibili per l’interrogazione nel relativo blocco catch{...}. Se il primo blocco catch{...} dello statement specifica una eccezione generica di tipo Exception, questo riceverà come argomento un oggetto di quel tipo, perdendo quindi tutte le caratteristiche specifiche dell’eccezione originata dal provider. Verrà eseguito il codice all’interno di quel blocco e tipicamente l’eccezione verrà gestita in quel modo, salvo che non si voglia ri-invocarla di nuovo utilizzando lo statement throw, per passarla alla funzione chiamante, magari corredata di altre informazioni applicative. Se invece il primo blocco catch{...} incontrato specifica di catturare una eccezione di tipo SqlException tutte le informazioni riguardandi la condizione di errore saranno disponibili all’interno del blocco stesso attraverso l’oggetto passato come argomento.
Per quanto riguarda le connessioni verso SQL Server, gli errori generati che hanno un livello di severità uguale o inferiore a 10 non generano un’eccezione ma possono essere ugualmente catturati utilizzando l’evento InfoMessage e il suo argomento di tipo InfoMessageEventArgs. Questo argomento espone attraverso la collezione Errors, la sequenza degli eventi che si sono verificati sulla connessione con la fonte dati.
Un altro evento che può essere utile è lo StateChange che viene invocato quando lo stato della connessione viene modificato. In particolare attraverso le due proprietà dell’argomento StateChangeEventArgs che si chiamano OriginalState e CurrentState è possibile capire la dinamica del cambio di stato.
// Instanzio un nuovo oggetto Connection SqlConnection conn = new SqlConnection("ConnectionString"); try { // Associo un event handler all’evento di InfoMessage conn.InfoMessage += new SqlInfoMessageEventHandler(OnInfoMessage); // Associo un event handler all’evento di StateChange conn.StateChange += new StateChangeEventHandler(OnStateChange); // Apertura della connessione conn.Open(); } catch (SqlException e) { // Gestione eventuali errori del Data Provider for (int i=0; i < e.Errors.Count; i++) { Console.WriteLine(e.Errors[i].ToString() + "\n"); } } catch (Exception e) { // Gestione errori generici Console.WriteLine(e.Message); } finally { // Chiusura della connessione conn.Close(); } // Gestione del cambiamento di stato protected static void OnStateChange(object sender, StateChangeEventArgs args) { Console.WriteLine("The current Connection state has changed from {0} to {1}.", args.OriginalState, args.CurrentState); } // Gestione degli InfoMessage protected static void OnInfoMessage(object conn, SqlInfoMessageEventArgs e) { Console.WriteLine("caught a SQL warning"); for (int i=0; i < e.Errors.Count; i++) { Console.WriteLine("Index#" + i + "\n" + "Warning:" + e.Errors[i].ToString() + "\n"); } }
Considerazioni sulle performance
Come linea generale riguardo alle performance, occorre sempre cercare suddividere le situazioni che necessitano di accesso ai dati nelle due categorie: situazione connessa o disconnessa. Nel primo caso, per operazioni di accesso e modifica delle informazioni l’utilizzo di comandi diretti verso la base dati attraverso gli oggetti Connection e Command è solitamente la strada più performante. Se poi siamo in presenza di una base dati evoluta come ad esempio SQL Server, l’utilizzo di strumenti di ottimizzazione come le Stored Procedure ed una corretta indicizzazione portano degli indubbi vantaggi. In questo scenario connesso, per ottenere delle informazioni abbiamo solitamente 2 strade:
- il DataReader
- i parametri di ritorno di una Stored Procedure
Il secondo metodo, in caso ovviamente di lettura di un singolo record o di parte di esso è la soluzione più indicata in una situazione di forte carico di lavoro e di utilizzo del connection pooling. In queste condizioni si sono verificate delle performance di circa il 30% migliori rispetto all’utilizzo del DataReader. L’uso di questo componente però permette di eseguire operazioni come la lettura dei metadati (es. utilizzando il metodo GetSchemaTable()) e si rivela invece superiore anche in fatto di performance quando non viene utilizzato il pooling delle conessioni. In questa situazione è vantaggioso utilizzare il valore CommandBehavior.SingleRow come parametro di input della ExecuteReader() per fare in modo che lo stream di record venga automaticamente chiuso dopo che il primo è arrivato client.
Per tenere sott’occhio i risultati in termini di prestazioni, ADO.NET espone un numero di counter di sistema che consentono un preciso monitoraggio. Nell’oggetto .NET Clr Data infatti, sono presenti una serie di counter tipo “Current # Pooled Connections” o “Total # Failed Commands” che possono essere molto utili, insieme con quelli esposti dalle varie basi dati, per tutta la fase di performance tuning delle applicazioni.
Evoluzioni future
La prima versione del .NET Framework è sicuramente una grande rivoluzione rispetto alle varie piattaforme che l’anno preceduto come COM/Windows DNA, ma è solamente l’inizio di una serie che porterà man mano all’integrazione nel Framework stesso di una serie di prodotti e servizi che ora sono realizzati come codice un-managed (Windows-native). Per quanto riguarda la parte di accesso ai dati, grande importanza avrà l’introduzione della prossima versione di SQL Server (nome in codice Yukon), la prima che ospiterà il .NET Framework consentendo, tra le altre cose, la scrittura di Stored Procedure nei vari linguaggi supportati (C#, VB.NET, ecc.) ed altri benefici. Tra i più significativi ci sarà la presenza di un nuovo .NET Data Provider che verrà eseguito in-process con il database server, consentendo ad esempio la gestione di cursori lato-server o di modalità di aggioramento e caricamento dei record in modalità batch non-logged, cioè senza passare per il Transaction Log di SQL Server con ovvi benefici di performance per le operazioni che non ne richiedono l’utilizzo per motivi di sicurezza delle informazioni.
Altre evoluzioni future saranno la nascita di nuovi Data Provider per i più diffusi database server come DB2, Oracle, ecc. mentre è già disponibile, come add-on al Framework quello che supporta lo standard ODBC.
Una delle tecnologie più interessanti verso le quali si stà muovendo Microsoft per l’accesso ai dati è denominate ObjectSpaces. Dietro questa sigla si cela l’idea di accedere alle informazioni relazionali presenti nelle varie fonti dati, utilizzando un paradigma completamente ad oggetti, grazie ad un mapping oggetti-dati eseguito dal Framework a run-time sulla base di definizioni contenute in file XML.
Questo rappresenta il legame tra la tecnologia object based e i motori di database che in passato avevano già tentato questa strada senza molto successo. In questo caso i dati rimarrebbero all’interno di strutture relazionali, ottimizzate e indicizzate a dovere per ottenere le corrette performance, mentre gli sviluppatori potrebbero avere un modo elegante, riutilizzabile e più orientato allo sviluppo di applicazioni Enterprise. Data l’assoluta importanza dello sviluppo di questa tecnologia ne seguiremo l’evoluzione in un successivo articolo, quando uscirà dalla fase attuale di Technology Preview.
Al prossimo articolo...
