Usare queries per manipolare il database è un mezzo molto efficacie, anche se il processo per creare una query sottoforma di stringa
può risultare alquanto macchinoso in alcuni casi. A questo proposito, vorrei invitarvi a leggere le prime lezioni del tutorial che
ho scritto riguardo a LINQ, il linguaggio di querying integrato disponibile dalla versione 2008 del linguaggio (framework v3.5).
In questo capitlo, invece, inizieremo a passare dalle relazioni, ossia dalle tabelle del database nel loro ambiente, agli oggetti, trasponendo,
quindi, tutte le operazioni a costrutti che già conosciamo. Possiamo rappresentare un database e le sue tabelle in due modi:
- Mediante l'approccio standard, con le classi DataSet e DataTable, che rappresentano esattamente il database, con tutte le sue proprietà e caratteristiche. Queste classi astraggono tutta la struttura relazione e la trasportano nel linguaggio ad oggetti;
- Mediante l'approccio LINQ, con normali classi scritte dal programmatore, artificalmente collegate tramite attributi e metadati, alle relazioni presenti nel database;
Vedremo ora solo il primo approccio, poiché il secondo è trattato già nel tutorial menzionato prima.
La classe DataSet ha lo scopo di rappresentare un database. Mediante un apposito oggetto, detto Adapter (che analizzeremo in seguito), è
possibile travasare tutti i dati del database in un oggetto DataSet e quindi operare su questo senza bisogno di query. Una volta terminate
le elaborazioni, si esegue il processo inverso aggiornando il database con le nuove modifiche apportate al DataSet. Questo è il
principio di base con cui si affronta il passaggio dalle relazioni agli oggetti.
Questa classe espone una gran quantità di membri, tra cui menzioniamo i più importanti:
- AcceptChanges() : conferma tutte le modifiche apportate al DataSet; questo metodo deve essere richiamato obbligatoriamente prima di iniziare la procedura di aggiornamento del database a cui è collegato;
- CaseSensitive : indica se la comparazione di campi di tipo string avviene in modalità case-sensitive;
- Clear() : elimina tutti i dati presenti nel DataSet;
- Clone() : esegue una clonazione deep dell'oggetto DataSet e restituisce la nuova istanza;
- DataSetName : nome del DataSet;
- GetChanges() : restituisce una copia del DataSet in cui sono presenti tutti i dati modificati (come se si fosse richiamato AcceptChanges());
- GetXml() : restituisce una stringa contenente la rappresentazione xml di tutti i dati presenti nel DataSet;
- HasChanges : determina se il DataSet sia stato modificato dall'ultimo salvataggio o caricamento;
- IsInitialized : indica se è inizializzato;
- Merge(D As DataSet) : unisce D al DataSet corrente. Le tabelle e i dati di D vengono aggiunti all'oggetto corrente;
- RejectChanges() : annulla tutte le modifiche apportate dall'ultimo salvataggio o caricamento;
- ReadXml(R) : legge un file XML mediante l'oggetto R (di tipo XmlReader) e trasferisce i dati ivi contenuti nel DataSet;
- Reset() : annulla ogni modifica apportata e riporta il DataSet allo stato iniziale (ossia come era dopo il caricamento);
- Tables : restituisce una collezione di oggetti DataTable, che rappresentano le tabelle presenti nel DataSet (e quindi nel database);
Se un DataSet rappresenta un database, allora un oggetto di tipo DataTable rappresenta un singola tabella, composta di righe e colonne. Nessun nuovo concetto da introdurre, quindi: si tratta solo di una classe che rappresenta ciò che abbiano visto fin ora per una relazione. I suoi membri sono pressoché simili a quelli di DataSet, con l'aggiunta delle proprietà Columns, Rows, PrimaryKey e del metodo AddRow (ho menzionato solo quelli di uso più frequente).
Ora possiamo caricare i dati in un DataSet ed eseguire un binding verso un controllo. Abbiamo già il concetto di binding nei capitoli sulla reflection, in riferimento alla possibilità di legare un identificatore a un valore. In questo caso, pur essendo fondamentalmente la stessa cosa, il concetto è leggermente differente. Noi vogliamo legare un certo insieme di valori ad un controllo, in modo che esso li visualizzi senza dover scrivere un codice particolare per inserirli. Il controllo che, per eccellenza, rende graficamente nel modo migliore le tabelle è DataGridView. Assumendo, quindi, di avere nel form solo un controllo DataGridView1, possiamo scrivere questo codice:
Imports MySql.Data.MySqlClient Class Form1 Private Data As New DataSet Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim Conn As New MySqlConnection("Server=localhost; Database=appdata; Uid=root; Pwd=root;") Dim Adapter As New MySqlDataAdapter Conn.Open() Adapter.SelectCommand = New MySqlCommand("SELECT * FROM Customers;", Conn) Adapter.Fill(Data) Conn.Clone() DataGridView1.DataSource = Data.Tables(0) End Sub End Class
DataGridView1 verrà riempito con tutti i dati presenti nella prima (e unica) tabella del dataset.
Nel prossimo esempio vedremo di accennare alla costruzione di una semplice applicazione per gestire brani musicali con un database, ed
eventualmente introdurrò un po' di codice per la riproduzione audio. In questo esempio, però, non useremo un generico
database, ma uno specifico database di cui conosciamo le proprietà e della cui esistenza siamo certi. Quindi faremo a meno di usare
un generico DataSet, ma ne creeremo uno specifico per quel database, che sia più semplice da manipolare. Useremo, quindi,
un DataSet tipizzato. Non dovremo scrivere alcuna riga di codice per far questo, poiché basterà "disegnare" la struttura del
database con uno specifico strumento che il nostro IDE mette a disposizioni, e che si chiama Table Designer. Mediante quest'ultimo, possiamo
delinare lo schema di un database e delle sue tabelle, e l'ambiente di sviluppo provvederà a scrivere un codice adeguato per creare
un dataset tipizzato specifico per quel database. A livello pratico, un dataset tipizzato non è altro che una classe derivata da
DataSet che definisce alcune proprietà e metodi atti a semplificare la scrittura di codice. Ad esempio, invece di referenziare la
prima cella della prima riga di una tabella, ottenerne il valore e convertirlo in intero, la versione tipizzata del dataset espone direttamente
una proprietà ID (ad esempio) che fa tutto questo. C'è solo un difetto nel codice autogenerato, ma lo illustrerò in
seguito.
Prima di iniziare, bisogna creare effettivamente le tabelle che useremo nel database AppData. Per questo programma, ho ideato tre tabelle:
authors, songs e albums, costruite come segue:
CREATE TABLE `albums` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `Name` char(255) NOT NULL, `Year` int(11) DEFAULT NULL, `Description` text, `Image` text, PRIMARY KEY (`ID`) ); CREATE TABLE `authors` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `Name` char(255) NOT NULL, `Nickname` char(255) DEFAULT NULL, `Description` text, `Image` text, PRIMARY KEY (`ID`) ); CREATE TABLE `songs` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `Path` char(255) NOT NULL, `Title` char(255) DEFAULT NULL, `Author` int(11) DEFAULT NULL, `Album` int(11) DEFAULT NULL, PRIMARY KEY (`ID`) );
[Gli accenti tonici sono stati aggiunti da SQLyog, e sono obbligatori solo se il nome della colonna o della tabella contiene degli spazi.]
Prima di procedere, potrebbe essere utile mostrare la toolbar di gestione delle basi di dati: per far questo, cliccate con il pulsante destro
su uno spazio vuoto della toolbar e spuntate Data Design per far apparire le relative icone:
Per aggiungere un nuovo dataset tipizzato, invece, cliccate sempre col destro sul nome del progetto nel solution explorer, scegliete Add Item
e quindi DataSet. Dovrebbe apparirvi un nuovo spazio vuoto simile a questo:
Spostando il mouse sulla toolbox a fianco, potrete notare che è possibile aggiungere tabelle, relazioni, queries e alcune altre cose.
Dato che dobbiamo ricreare la stessa struttura del database AppData, è necessario creare tre tabelle: Albums, Authors e Songs, ciascuna
con le stesse colonne di quelle sopra menzionate. Trascinate un componente DataTable all'interno della schermata e rinominatelo in Songs,
quindi fate click col destro sull'header della tabella e scegliete Add > Column:
Aggiungete quindi tante colonne quante sono quelle del codice SQL. Ora selezionate la prima colonna (ID) e portate in primo piano la finestra
della proprietà (la stessa usata per i controlli). Essa visualizzerà alcune informazioni sulla colonna ID. Per rispettare
il vincolo con il database reale, essa deve essere dichiarata di tipo intero, deve supportare l'autoincremento e non può essere
NULL:
Ora fate lo stesso con tutte le altre colonne, tenendo conto che char(255) e text sono entrambi contenibili dallo stesso tipo (String).
Prima di passare alla compilazione delle altre tabelle, ricordatevi di impostare ID come chiave primaria: cliccate ancora sull'header della
tabella, scegliendo Add > Key:
Bene. Come avrete sicuramente notate, i campi Author e Album di Songs non sono stringhe, bensì interi. Infatti, come abbiamo visto
qualche capitolo fa, è possibile collegare logicamente due tabelle tramite una relazione (uno-a-uno, uno-a-molti o molti-a-molti). In
questo caso, vogliamo collegare al campo Author di una canzone, la tupla che rappresenta quell'autore nella tabella Authors. Questa è
una relazione uno-a-molti (in questo programma semplificato, assumiamo che tutti coloro che hanno partecipato alla realizzazione siano considerati
"autori", senza le varie distinzioni tra autore dei testi, artist, compositori eccetera...). Mediante l'editor integrato nell'ambiente di
sviluppo possiamo anche aggiungere questa realzione (che andrà a popolare la proprietà Relations del DataSet). Basta
aggiungere un oggetto Relation e compilare i campi relativi:
Una volta completati tutti i passaggi, è possibile iniziare a scrivere qualche riga di codice (non dimenticatevi di riempire il database
con qualche tupla di esempio prima di iniziare il debug).
Ecco l'interfaccia del programma:
Oltre ai controlli che si vedono nell'immagine, ho aggiunto anche il dataset tipizzato creato prima dall'editor, AppDataSet. Dato che nella
listbox sulla sinistra visualizzeremo i titoli delle canzoni, possiamo eseguire un binging di tali dati sulla listbox. Dopo averla selezionata,
andate nella proprietà DataSource e scegliete la tabella Songs:
Dopodiché, impostate il campo DisplayMember su Title e ValueMember su ID: come avevo spiegato nel capitolo sulla listbox, queste
proprietà ci permettono di modificare cosa viene visualizzato coerentemente con gli oggetti immagazzinati nella lista. Se avete fatto
tutto questo, l'IDE creerà automaticamente un nuovo oggetto di tipo BindingSource (il SongsBindingSource dell'immagine precedente).
Esso ha il compito di mediare tra la sorgente dati e l'interfaccia utente, e ci sarà utile in seguito per la ricerca.
Ecco il codice:
Public Class Form1 Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim Conn As New MySqlConnection("Server=localhost; Database=appdata; Uid=root; Pwd=root;") Dim Adapter As New MySqlDataAdapter Conn.Open() 'Carica le tabelle nel dataset. Le tabelle sono ora 'accessibili mediante omonime proprietà dal 'dataset tipizzato Adapter.SelectCommand = New MySqlCommand("SELECT * FROM Songs;", Conn) Adapter.Fill(AppDataSet.Songs) Adapter.SelectCommand = New MySqlCommand("SELECT * FROM Authors;", Conn) Adapter.Fill(AppDataSet.Authors) Adapter.SelectCommand = New MySqlCommand("SELECT * FROM Albums;", Conn) Adapter.Fill(AppDataSet.Albums) Conn.Clone() End Sub Private Sub lstSongs_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lstSongs.SelectedIndexChanged If lstSongs.SelectedIndex < 0 Then Exit Sub End If 'Trova la riga con ID specificato. Il tipo SongsRow è stato 'definito dall'IDE durante la creazione del dataset tipizzato. Dim S As AppDataSet.SongsRow = AppDataSet.Songs.FindByID(lstSongs.SelectedValue) lblName.Text = S.Title tabAuthor.Tag = Nothing 'Anche il metodo IsAuthorNull è stato definito dall'IDE. 'In generale, per ogni proprietà per cui non è stata 'specificata la clausola NOT NULL, l'IDE crea un metodo per 'verificare se quel dato attributo contiene un valore 'nullo. Equivale a chiamare S.IsNull(3). If Not S.IsAuthorNull() Then 'Ottiene un array che contiene tutte le righe della 'tabella Authors che soddisfano la relazione definita 'tra Songs e Authors. Dato che la chiave esterna della 'tabella figlio era un ID, la realzione, pur essendo 'teoricamente uno-a-molti, è in realtà 'uno-a-uno. Perciò, se questo array ha almeno 'un elemento, ne avrà solo uno. Dim Authors() As AppDataSet.AuthorsRow = S.GetAuthorsRows() 'Come IsNull, questo metodo equivale a chiamare 'S.GetChildRows("Songs_Authors") If Authors.Length > 0 Then Dim Author As AppDataSet.AuthorsRow = Authors(0) lblAuthorName.Text = Author.Name If Not Author.IsNicknameNull() Then lblAuthorName.Text &= vbCrLf & Chr(34) & Author.Nickname & Chr(34) End If If Not Author.IsImageNull() Then imgAuthor.Image = Image.FromFile(Author.Image) Else imgAuthor.Image = Nothing End If If Not Author.IsDescriptionNull() Then txtAuthorDescription.Text = Author.Description Else txtAuthorDescription.Text = "" End If tabAuthor.Tag = Author.ID End If End If tabAlbum.Tag = Nothing If Not S.IsAlbumNull() Then Dim Albums() As AppDataSet.AlbumsRow = S.GetAlbumsRows() If Albums.Length > 0 Then Dim Album As AppDataSet.AlbumsRow = Albums(0) lblAlbumName.Text = Album.Name If Not Album.IsYearNull() Then lblAlbumYear.Text = Album.Year Else lblAlbumYear.Text = "" End If If Not Album.IsImageNull() Then imgAlbum.Image = Image.FromFile(Album.Image) Else imgAlbum.Image = Nothing End If If Not Album.IsDescriptionNull() Then txtAlbumDescription.Text = Album.Description Else txtAlbumDescription.Text = "" End If tabAlbum.Tag = Album.ID End If End If End Sub Private Sub btnSearch_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSearch.Click If Not String.IsNullOrEmpty(txtSearch.Text) Then 'La proprietà Filter di un binding source è come 'una condizione SQL. In questo caso cerchiamo tutte le 'canzoni il cui titolo contenga la stringa specificata. SongsBindingSource.Filter = String.Format("title like '%{0}%'", txtSearch.Text) Else SongsBindingSource.Filter = "" End If End Sub End Class