A seguito del mio ultimo post di ieri, ho ricevuto diversi comment di Andrea e Lorenzo che mi hanno spinto a rivedere e ad ottimizzare il codice C# scritto, appunto, in quel post.
Motivo scatenante: l'utilizzo della classe SqlCommandBuilder. Come ho detto ieri, questa classe serve per facilitare e a velocizzare la creazione dei 3 Command necessari a ADO.NET per fare le sue operazioni sul database (i Command sono InsertCommand, DeleteCommand e UpdateCommand). Tranne in rarissimi casi, però, fretta e bene non vanno mai bene, ed effettivamente provando a visualizzare la CommandText dei 3 SqlCommand si vede che il codice SQL auto-generato potrebbe (leggere deve) essere migliorato, e non poco. Quindi, morale della favola: è giusto sapere che .NET ci mette a disposizione il SqlCommandBuilder, è giusto conoscerlo in vista dell'esame 70-316, ma quando poi si produce codice .NET serio, i 3 SqlCommand vanno creati a mano. Osservazione: la Study Guide di Lorenzo, nè le altre risorse on-line ci dicono che dobbiamo sapere il SqlCommandBuilder. Per superare l'esame dobbiamo saper conoscere le classi e dobbiamo sapere scrivere C#, per cui mi sono messo subito (già ieri sera) a correggere il codice e a migliorarlo al volo. Quindi, veniamo a noi.
Creazione di un SqlCommand con i suoi parametri
Nel form frmExportDB che abbiamo visto ieri, ho creato un metodo privato, che è il seguente:
private SqlCommand getInsertIntoCommand(SqlConnection conn)
{
// Definisco il SqlCommand
SqlCommand cmd = new SqlCommand("INSERT INTO ListBirthdays (name, birthday) VALUES (@name, @birthday)", conn);
cmd.CommandType = CommandType.Text;
// Definisco i suoi due parametri
cmd.Parameters.Add(new SqlParameter("@name", SqlDbType.VarChar, 100, "name"));
cmd.Parameters.Add(new SqlParameter("@birthday", SqlDbType.SmallDateTime, 4, "birthday"));
return(cmd);
}
Il codice non è complicatissimo, ma una descrizione la voglio fare comunque. Nel codice sono state usate 2 classi diverse: SqlCommand e SqlParameter. La prima descrive il Command nella sua interezza, la classe descrive ogni singolo parametro che il Command ha bisogno per funzionare correttamente.
La property CommandText del SqlCommand cmd (che poi verrà ritornato al chiamante) è la seguente:
INSERT INTO ListBirthdays (name, birthday) VALUES (@name, @birthday)
Trattandosi di una tabella con due soli campi (ricordiamoci che id è un contatore univoco), la sua INSERT INTO ha bisogno di due soli parametri: il nome della persona e la sua data di compleanno. I parametri vengono indicati dalle stringhe @name e da @birthday. Quindi, non dobbiamo fare altro che creare due SqlParameter per indicare a .NET tutte le informazioni del caso. Il costruttore di questa classe è molto comodo, perchè permette di creare e definire un parametro molto velocemente. Nel codice sopra, ho semplicemente 2 linee di codice per definire 2 parametri nell'SQL. Il costruttore prevede il nome del parametro, il tipo di dati, la lunghezza in bytes e il nome della column source.
Notare l'uso dell'enum SqlDbType che mappa all'interno del framework .NET i tipi di dato di SQL Server (varchar, bit, text, float, image, e così via). La Source Column, di tipo string, contiene il nome della colonna (field, DataColumn) all'interno del DataSet: l'engine di ADO.NET sostituisce automaticamente @name con il campo del DataSet, permettendo quindi la creazione del nuovo record. Il SqlCommand cmd viene ritornato al chiamante (l'handler del Button Ok che abbiamo visto ieri).
Riempire una DataTable con tante DataRow di un'altra DataTable
Mi spiego meglio
Supponiamo (come è nel nostro caso) di avere due tabelle strutturalmente identiche (stessi nomi di campi, stessi tipi di dati, etc.). Supponiamo che contengano dati diversi e records. Cosa dobbiamo fare se vogliamo accodare records da una DataTable all'altra?
Nel codice che ho postato ieri, seguivo questa procedura:
- Chiamo il metodo NewRow() sulla DataTable di destinazione
- Valorizzo tutte le DataColumn della DataRow appena ritornata da NewRow()
- Aggiungo alla collection Rows della DataTable la DataRow
- Ricomincio fino a quando non ho finito tutte le DataRow che voglio trasferire
Ieri sera, quasi per caso, ho scoperto l'utilizzo del metodo ImportRow. Ne ho parlato anche in questo posto. Adesso l'handler del Button Ok è il seguente:
private void btnOk_Click(object sender, System.EventArgs e)
{
// SqlConnection per la connessione al db
SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["DBConnection"]);
// se la CheckBox è true, eseguo un SqlCommand
// per svuotare la tabella di destinazione
if (this.chkDeleteData.Checked)
{
string sql = "TRUNCATE TABLE ListBirthdays";
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.CommandType = CommandType.Text;
// apro la connessione, eseguo il command e chiudo
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
// Creo un SqlDataAdapter per creare il DataSet locale
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM ListBirthdays", conn);
// Nel DataSet creo la struttura e i dati della tabella
// ListBirthdays del database: se ho fatto la DELETE, allora
// ds.Tables[0].Rows = 0
DataSet ds = new DataSet();
da.Fill(ds, "ListBirthdays");
foreach (DataRow dr_source in this.Tabella.Rows)
{
// copio i dati dalla tabella Source
// alla tabella Destination
ds.Tables[0].ImportRow(dr_source);
// faccio proseguire la barra
this.barBarra.Value++;
this.Refresh();
}
// invio il DataSet a SQL Server
// specifico solo l'InsertCommand
da.InsertCommand = this.getInsertIntoCommand(conn);
// Per cominciare una transazione sul server, la SqlConnection deve
// essere aperta per forza
conn.Open();
SqlTransaction tran = conn.BeginTransaction(IsolationLevel.ReadCommitted);;
da.InsertCommand.Transaction = tran;
try
{
// tento l'update sul database
// questa riga richiede un SqlCommandBuilder
da.Update(ds, "ListBirthdays");
// se tutto va bene, committo la transazione
tran.Commit();
}
catch
{
// altrimenti, rollback
tran.Rollback();
}
tran.Dispose();
da.Dispose();
ds.Dispose();
conn.Close();
this.Close();
// conferma all'utente di fine esportazione
MessageBox.Show(this, "L'esportazione dei compleanni è stata completata con successo!!",
"Conferma!", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
Gran parte del codice è rimasto invariato. Gli unici cambiamenti degni di nota sono:
- L'uso della classe SqlTransaction (blocco try...catch per fare il rollback)
- La chiamata alla function getInsertIntoCommand descritta prima per valorizzare la property InsertCommand del SqlDataAdapter
- Il miglioramento del ciclo foreach (metodo ImportRow)
Uso della classe SqlTransaction
SqlTransaction è la classe di ADO.NET che implementa la logica delle transazioni sui database. La classe SqlConnection ritorna un oggetto SqlTransaction dopo la chiamata al metodo BeginTransaction. Lavorando con l'oggetto SqlTransaction, possiamo chiamare i metodi Commit() o Rollback(). Nel codice sopra, creo un oggetto tran proprio per questo motivo.
La chiamata a da.Update(...) è stata inclusa in un blocco try...catch. Se tutto va a buon fine, viene chiamato il metodo Commit() e il database risulta aggiornato a tutti gli effetti. Se qualcosa va storto, viene chiamato il Rollback() per annullare tutto.