Riassunto delle ultime puntate
Nel post [MCAD.23] abbiamo visto i primi rudimenti di SqlConnection e SqlCommand all'interno della form frmConnection creata appositamente. In quel post ho scritto del codice per aprire una connessione verso SQL Server e due tipi diversi di SqlCommand (per eseguire una SELECT e per eseguire una stored-procedure). Lo scopo era quello di creare una tabella ListBirthdays sul server, pronta ad accogliere tutti i dati che inseriamo all'interno della nostra applicazione, ovvero tutte le istanze di Age, inteso come persone con le relative date di compleanno.
Nel post [MCAD.24] ho inserito nel codice qualche miglioramento che mi è stato consigliato da cui mi ha lasciato commenti. Miglioramenti che non hanno riguardato ADO.NET più di tanto, però è stato giusto comunque farli notare ed inserirli nella mia serie di articoli su MCAD. Un po' di buon codice, si sa, non basta mai, per cui...
Scriviamo sulla tabella 'ListBirthdays' i nostri compleanni
Oggi vediamo come andare a scrivere sul database, usando le classi che ADO.NET ci mette a disposizione. Quali sono queste classi? Beh, le solite! SqlConnection, SqlDataAdapter, SqlCommand e il meraviglioso DataSet, vero contenitore dei dati che vogliamo esportare. Cominciamo senza troppi fronzoli.
Ho creato un'ulteriore Windows Form, chiamate frmExportDB, disegnata in questo modo:
In breve: una ProgressBar, un CheckBox e due Button. Non sto a soffermarmi più di tanto su queste cose, ormai parto dal presupposto che sappiamo come creare WF decenti. Seguendo le linee guida di "Windows Forms Programming in C#", ho impostato le seguenti proprietà:
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.HelpButton = true;
this.MaximizeBox = false;
this.MinimizeBox = false;
Vedremo in seguito come gestire l'evento HelpRequested delle Windows Forms e dei Controls. Per adesso non ci interessa, ma è l'unica particolarità che ho voluto far notare adesso. Passiamo velocemente a vedere il codice di btnOk_Click. Le linee guida che dovrà seguire questo handler sono le seguenti:
- La connection string verrà letta dal file app.config
- Se il CheckBox è attivo, bisognerà eseguire una "DELETE FROM ListBirthdays" per svuotare la tabella dei dati eventualmente contenuti
- Poi, bisognerà popolare la tabella ListBirthdays con i dati caricati nell'applicazione
Quindi, vediamo come procedere. Il primo blocco di codice è il seguente:
SqlCommand cmd;
// leggo app.config per leggere la stringa di connessione
System.Configuration.AppSettingsReader reader = new System.Configuration.AppSettingsReader();
// SqlConnection per la connessione al db
SqlConnection conn = new SqlConnection((string) reader.GetValue("DBConnection", typeof(System.String)));
Preparo un oggetto SqlCommand. Usando la classe AppSettingsReader carico da app.config la key DBConnection. Ricordo il suggerimento di Igor nel post [MCAD.13]: per leggere dal file di configurazione dell'applicazione posso scrivere anche System.Configuration.ConfigurationSettings.AppSettings["DBConnection"]. Poi, preparo una SqlConnection con la connection string corretta. Il codice seguente è:
// se la CheckBox è true, eseguo un SqlCommand
// per svuotare la tabella di destinazione
if (this.chkDeleteData.Checked)
{
string sql = "DELETE FROM ListBirthdays";
cmd = new SqlCommand(sql, conn);
cmd.CommandType = CommandType.Text;
// apro la connessione, eseguo il command e chiudo
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
Se chkDeleteData.Checked vale true , procedo con la cancellazione della tabella. Creo un SqlCommand con il comando "DELETE FROM ListBirthdays", specificando che la stringa contiene un comando SQL in testo libero, e non magari il nome di una stored-procedure. Quindi apro la connection, eseguo il SqlCommand ed infine chiudo la connection.
Fatto questo, posso procedere con l'esportazione dei dati vera e propria. Vediamo il codice in un blocco unico per maggior chiarezza.
// 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)
{
DataRow dr_dest = ds.Tables[0].NewRow();
// copio i dati dalla tabella Source
// alla tabella Destination
dr_dest["name"] = dr_source["Name"];
dr_dest["birthday"] = dr_source["BirthDate"];
ds.Tables[0].Rows.Add(dr_dest);
// faccio proseguire la barra
this.barBarra.Value++;
this.Refresh();
}
// invio il DataSet a SQL Server
SqlCommandBuilder bld = new SqlCommandBuilder(da);
da.Update(ds, "ListBirthdays");
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);
Il SqlDataAdaper è un oggetto di ADO.NET che fa da "ponte di collegamento" tra il database fisico reale e gli oggetti disconnessi con i quali poi lavoreremo. Creo un DataSet e, chiamando il metodo Fill del SqlDataAdapter, lo riempio con la struttura ed i dati provenienti dal comando "SELECT * FROM ListBirthdays". Notare il costruttore della classe SqlDataAdapter che ho utilizzato: richiede un SQL e un SqlConnection.
Poi, comincio un ciclo foreach per popolare la ds.Tables[0] del DataSet appena creato. Notare l'uso di this.Tabella: Tabella è un membro della form frmExportDB di tipo DataTable. Questa property della form viene inizializzata al momento dell'apertura della form, usando la vecchia
private DataTable getSaveTable(bool WithData)
{ }
che avevamo creato per realizzare le stampe (anteprima, imposta pagina, stampa). Questa function ritorna una DataTable contenente i dati presenti nella ListBox, per esempio, oppure quelli caricati da file XML. Comunque sia, in questo momento ci interessa sapere che al termine del ciclo foreach, noi abbiamo ds.Tables[0] popolata con tanti DataRow quanti sono i compleanni che abbiamo gestito sulla ListBox (in realtà questo è vero solo che abbiamo svuotato la tabella sul server). All'interno del ciclo, giocherello con la ProgressBar per far vedere all'utente a che punto siamo dell'operazione, nulla di preoccupante!
Precisazione: per molti di noi sarà ovvio, credo, ma io lo preciso ugualmente. Usando un DataSet, noi stiamo lavorando con l'architettura disconnessa di ADO.NET. Questo significa che qualsiasi modifica sui dati e sulla struttura viene fatta esclusivamente in RAM, e non sul database fisico. Proprio questo è il concetto di disconnesso: prelevo i dati dal server, li elaboro, salvo sul server i dati aggiornati.
Quindi, teniamo ben presente che all'uscita del ciclo foreach abbiamo un DataSet completamente popolato (inteso sia come struttura, sia come dati), ma il tutto si trova in memoria, e non sul database. Come facciamo adesso a salvare tutto sul database? Entra nuovamente in scena il SqlDataAdapter. Le chiamate a:
SqlCommandBuilder bld = new SqlCommandBuilder(da);
da.Update(ds, "ListBirthdays");
aggiornano la tabella ListBirthdays del database server specificato nella stringa di connessione. L'aggiornamento viene effettuato obbligatoriamente tramite l'uso implicito della classe SqlCommandBuilder. A cosa serve? Beh, per capire bene lo scopo della classe SqlCommandBuilder credo ci sia bisogno di un po' di teoria. L'importante adesso è sapere che al termine del metodo Update è importante rilasciare tutte le risorse che sono state utilizzate (le chiamate al metodo Dispose e alla Close della connection).
Un po' di teoria sul SqlCommandBuilder per completare il discorso
E' importante sapere che la linea di codice che effettivamente scrive su database è quella che contiene la chiamata al metodo Update dell'oggetto SqlDataAdapter. Come fa .NET a sapere quali records aggiornare, aggiungere e cancellare? Quali sono invece i records che sono rimasti invariati?
Primo concetto: la classe DataRow (che concettualmente rappresente i record di una tabella) dispone della property RowState , che può assumere i valori dell'enum DataRowState, ovvero: Added, Deleted, Detached, Modified e Unchanged (per maggiori dettagli c'è questa pagina su MSDN). Quando eseguo l'Update del SqlDataAdapter, .NET esamina uno ad uno lo RowState di tutte le DataRow contenute in tutte le DataTable contenute nel DataSet. In base allo stato di ciascuna DataRow, il framework usa il SqlCommand più opportuno per aggiornare il database. Vengono ovviamente evitate le DataRow dove RowState = DataRowState.Unchanged.
Quindi, per esempio, se ds.Tables[0].Rows[0].RowState = DataRowState.Added, il framework userà il SqlCommand definito da da.InsertCommand. Se RowState = DataRowState.Modified, verrà usato il SqlCommand definito da.UpdateCommand, e così via.I SqlCommand vengono inizializzati tramite il fantomatico SqlCommandBuilder di cui avevamo parlato prima. Eventualmente, possiamo crearli manualmente, possiamo modificarli se quelli creati automaticamente non ci piacciono, oppure se vogliamo ottimizzarli. Ricordiamoci che comunque tutta questa logica viene implementata dal metodo Update del SqlDataAdapter.
SqlTransaction e prossimi post
Nel prossimo post mi piacerebbe scrivere qualcosa sulla classe SqlTransaction che io ho già utilizzato nella mia applicazione. Ho avuto qualche problema e o tuttora qualche dubbio, che ho espresso sul forum di UGIdotNET questa mattina. Inoltre, ho cominciato a vedere le classi Debug e Trace, nel frattempo, perciò vedrò di continuare ovviamente il discorso su ADO.NET, parlando nel contempo di come fare debug del codice e come attivare TraceListeners per produrre log su file di testo.
Il mio compito è ancora lungo, e la strada potrebbe essere in salita!
powered by IMHO 1.2