Introduzione a LINQ: LINQ-To-SQL
a cura di Alessandro Del Sole (requisiti: conoscenze approfondite di .NET Framework)

Introduzione
In questo articolo riprendiamo il discorso introduttivo a LINQ, una delle più importanti novità introdotte da Microsoft .NET Framework 3.5. Stavolta faremo un approccio a LINQ-To-SQL, la parte di questa tecnologia dedicata all'interrogazione di database basati su SQL Server, utilizzando Visual Basic 2008.
La particolarità dell'utilizzo di LINQ-To-SQL consiste nella possibilità di interfacciarsi ai dati in maniera fortemente tipizzata, poiché, come vedremo tra breve, è possibile lavorare su classi che "mappano" le varie parti del database e le espongono allo sviluppatore sotto forma di oggetti .NET, mantenendo, nella fase di interrogazione vera e propria, l'utilizzo della medesima sintassi LINQ.
Nel corso dell'articolo prenderemo in considerazione tre diversi scenari, che avranno lo scopo principale di mostrare le modalità di approccio a questa tecnica di programmazione:

  1. Un'applicazione Windows Forms in cui verrà interrogata una tabella esposta da un database;
  2. Un'applicazione Windows Forms in cui verrà creata una relazione tra due tabelle, che verranno poi interrogate;
  3. Un'applicazione Windows Presentation Foundation in cui verrà popolato un controllo a partire da un'origine dati presente nel database.

Il codice sorgente che accompagna l'articolo è disponibile nell'Area Download di VB T&T, sebbene, come di consueto, sia opportuno seguire i passaggi illustrati secondo una logica step-by-step.

Requisiti di sistema
Prima di proseguire nella lettura è necessario che sul sistema siano installati i seguenti software:

Va precisato che Northwind è un database dimostrativo realizzato da Microsoft, che ben si presta a scenari didattici come questo, poiché si tratta di un database molto completo, la cui struttura si rifà ad una tipica situazione aziendale. Pertanto vi potrà tornare utile anche per approfondimenti personali sull'accesso ai dati basato su SQL Server. Nel codice allegato all'articolo non ho accluso il database stesso per motivi di dimensioni e ridistribuzione, potete comunque scaricare il database liberamente dal sito Microsoft sopra indicato.

Creazione del primo progetto di esempio
Iniziamo con il predisporre il primo progetto di esempio, che ci consentirà di capire i primi concetti importanti di LINQ-To-SQL. Dopo aver aperto Visual Basic 2008, bisogna in primo luogo stabilire una connessione al database SQL con le modalità già note, quindi mediante la finestra Esplora database.
Fatto questo, create un nuovo progetto Windows Forms (il nome del progetto non riveste particolare importanza). A questo punto, la prima operazione da compiere è quella di aggiungere al progetto una classe LINQ-To-SQL, che sarà in grado di ricevere una o più tabelle del nostro database. Per farlo, è sufficiente fare clic destro sul progetto nella finestra Esplora soluzioni e selezionare il comando Aggiungi|Nuovo elemento (disponibile anche nel menu Progetto). Quando compare la finestra Aggiungi nuovo elemento, bisogna selezionare il modello chiamato Classi LINQ to SQL, come si osserva in figura:

Nell'esempio proposto il nome assegnato al file riprende quello del database e la descrizione visualizzata nella finestra ci ricorda che si tratta di classi che eseguono la mappatura, sotto forma di oggetto .NET, di elementi del database.
Una volta aggiunta la classe al progetto, noterete che ad essa viene associato uno specifico designer. Sulla superficie di questo designer vanno quindi trascinate le tabelle di interesse del database dalla finestra Esplora database (si noti, inoltre, come l'IDE aggiunga automaticamente un file di configurazione contenente, tra l'altro, la stringa di connessione al database).
Supponiamo, quindi, di voler interrogare la tabella relativa agli impiegati della nostra ipotetica azienda. Dopo aver espanso la struttura ad albero del database, trascinate la tabella Employees sulla superficie della classe, di modo che appaia come nella figura seguente:

Quello che a noi appare a video nel designer è ora, in realtà, implementato da una nuova classe .NET. Infatti, al file Northwind.dbml (che rappresenta il layout grafico) è associato un file di code-behind chiamato Northwind.designer.vb. Attivando la funzionalità di visualizzazione di tutti i file nella finestra Esplora soluzioni, ed espandendo la struttura ad albero relativa alla nostra classe LINQ-To-SQL, potrete osservare la presenza di questo file di codice.
Per capire il concetto di "mappatura" e cosa significa parlare di interazione col database in maniera fortemente tipizzata, attivate l'editor di codice proprio sul file Northwind.designer.vb, che contiene due classi, della prima delle quali riporto un significativo estratto su cui focalizzare l'attenzione:

<System.Data.Linq.Mapping.DatabaseAttribute(Name:="NORTHWND")> _
Partial Public Class NorthwindDataContext
  Inherits System.Data.Linq.DataContext
  
  Private Shared mappingSource As System.Data.Linq.Mapping.MappingSource = New AttributeMappingSource
  
  Public Sub New()
    MyBase.New(Global.Esempio1.My.MySettings.Default.NORTHWNDConnectionString, mappingSource)
    OnCreated()
  End Sub
  
  Public Sub New(ByVal connection As String)
    MyBase.New(connection, mappingSource)
    OnCreated
  End Sub

  'Seguono altri overload del costruttore

End Class

In merito a questa prima classe facciamo subito alcune riflessioni. In primo luogo si osserva come il namespace che espone le funzionalità relative a LINQ-To-SQL sia quello chiamato System.Data.Linq. La classe in questione costituisce un cosiddetto DataContext. Una classe DataContext consiste in un vero e proprio riferimento managed al database ed è con questa che noi andremo a rapportarci, piuttosto che intervenire direttamente sul database stesso. Ogni DataContext eredita dalla classe System.Data.Linq.DataContext e l'IDE ne genera una ogni qual volta si aggiunge una classe LINQ-To-SQL al progetto, assegnando automaticamente l'identificatore in base al nome assegnato al file .dbml (quindi, in questo caso, il nostro DataContext si chiama NorthwindDataContext). Ovviamente, lo sviluppatore non avrà bisogno di modificare o conoscere a menadito il codice sopra esposto, poiché si lavorerà a design-time. Ad ogni buon conto, è bene conoscere l'esistenza del codice che sottende al DataContext.

La seconda classe presente nel file di codice è chiamata, in questo frangente, Employees e rappresenta, come si può facilmente intuire, la tabella degli impiegati precedentemente aggiunta a design-time sulla superficie della rappresentazione grafica della classe. Questo ci porta a capire che ciascun elemento del database che viene associato alla classe LINQ-To-SQL viene tradotto sotto forma di oggetto .NET, con i vantaggi che ne conseguono (in particolare la gestione da parte del Common Language Runtime). La rappresentazione di una tabella fatta in questo modo viene definita, in gergo LINQ, come Entity. Il codice seguente mostra un estratto della classe Employees:

<Table(Name:="dbo.Employees")>  _
Partial Public Class Employees
  Implements System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
  
  Private Shared emptyChangingEventArgs As PropertyChangingEventArgs = _
                                                              New PropertyChangingEventArgs(String.Empty)
  
  Private _EmployeeID As Integer
  
  <Column(Storage:="_EmployeeID", AutoSync:=AutoSync.OnInsert, DbType:="Int NOT NULL IDENTITY", _
          IsPrimaryKey:=True, IsDbGenerated:=True)> _
  Public Property EmployeeID() As Integer
    Get
      Return Me._EmployeeID
    End Get
    Set(ByVal value As Integer)
      If ((Me._EmployeeID = value) _
         = False) Then
        Me.OnEmployeeIDChanging(value)
        Me.SendPropertyChanging()
        Me._EmployeeID = value
        Me.SendPropertyChanged("EmployeeID")
        Me.OnEmployeeIDChanged()
      End If
    End Set
  End Property

  'Segue implementazione altre proprietà

End Class

La classe è decorata da un attributo Table, che sta ad indicare il fatto che la classe stessa rappresenta una tabella. Le proprietà pubbliche sono, a loro volta, decorate dall'attributo Column che contraddistingue la proprietà come colonna della tabella. Quest'ultimo attributo gode di ulteriori proprietà che permettono di configurare i campi delle colonne, come ad esempio Storage che indica il campo privato utilizzato per memorizzare il valore della proprietà (quindi della colonna) o dbType che indica il tipo di dato utilizzato nella colonna secondo la sintassi SQL. Anche in questo caso non è necessario per lo sviluppatore conoscere ogni aspetto della scrittura del codice di queste classi, poiché l'IDE pensa a tutto automaticamente. È comunque opportuno sapere come avviene la mappatura del database attraverso la generazione di classi .NET.

La fase successiva riguarda il collegamento dell'applicazione alla sorgente dati desiderata, utilizzando il comando Aggiungi origine dati del menu Dati. Quando compare la finestra Configurazione guidata origine dati, anziché selezionare un database come si faceva in altre occasioni, in questo caso si seleziona l'opzione Oggetto, proprio in conseguenza dei passaggi appena visti. La figura seguente rappresenta tale selezione:

Andando avanti, si passa alla selezione della classe che costituisce la sorgente dati. Espandendo la struttura ad albero relativa al namespace di primo livello della nostra applicazione, compare, come potete vedere, la classe Employees precedentemente generata dall'IDE. Nel caso in cui la classe non dovesse comparire nell'elenco, provate prima a compilare il tutto.

Al termine della selezione, la finestra Origini dati si presenta così:

L'aspetto assunto dalla finestra risulterà ora sicuramente familiare. Per completare il passaggio di data-binding sarà sufficiente, tramite drag'n'drop, trascinare la tabella Employees dalla finestra Origini dati sul form, come già sapete fare. Visual Basic genera così un controllo di tipo BindingSource, chiamato EmployeesBindingSource, e un controllo BindingNavigator che consente di sfogliare i dati (chiamato EmployeesBindingNavigator). La figura seguente mostra il risultato delle operazioni svolte sinora:

Ora che abbiamo predisposto la base strutturale della nostra applicazione, iniziamo a scrivere un po' di codice per capire come utilizzare LINQ nell'interrogare il nostro database SQL.

Utilizzo del DataContext per le query LINQ
Passiamo quindi alla visualizzazione dell'editor di codice. In primo luogo, va istanziato un nuovo oggetto NorthwindDataContext (a livello di classe) al fine di ottenere un riferimento managed al database:

  Private ndc As New NorthwindDataContext

Supponiamo ora di voler popolare la nostra griglia con i dati relativi ai soli impiegati per i quali la proprietà Region della tabella originaria non sia vuota eseguendo, inoltre, l'ordinamento alfabetico in base al cognome. Per praticità scriveremo la query all'interno dell'evento Load del form:

  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                         Handles MyBase.Load

    Dim risultato = From impiegato In ndc.Employees _
                    Order By impiegato.LastName _
                    Where impiegato.Region <> "" _
                    Select impiegato

    EmployeesBindingSource.DataSource = risultato

  End Sub

Come si può osservare, la query viene eseguita sulla proprietà Employees dell'istanza ndc della classe NorthwindDataContext. La variabile risultato diventa, in conseguenza dell'inferenza dei tipi, una collection di oggetti Employees. La clausola Order By esegue l'ordinamento in base al cognome dell'impiegato (proprietà LastName) mentre l'aggiunta di ciascun impiegato al risultato della query avviene solo qualora la proprietà Region consista in una stringa non vuota (clausola Where). L'utilizzo dei simboli <> per determinare che la stringa non sia vuota si rende necessario dal momento che, all'interno delle query LINQ, non è possibile utilizzare il metodo String.IsNullOrEmpty (che restituisce Boolean). Se ora provate ad avviare l'applicazione, si presenterà un risultato simile a quello in figura:

In sintesi, con questo primo esempio si sono raggiunti i seguenti obiettivi:

Potete quindi salvare il progetto, bere un caffè e prendere un po' di fiato prima di passare all'esempio successivo!

Un secondo esempio: relazioni tra tabelle e successive query
Ci proponiamo ora di mettere in relazione due tabelle del database Northwind per poi eseguire una query il cui risultato verrà associato ai rispettivi controlli BindingSource. Nel corso dell'illustrazione degli esempi vedremo anche come attivare la possibilità di salvare le modifiche nel database e osserveremo l'utilizzo di appositi log delle classi DataContext.
Potete creare un nuovo progetto Windows Forms ripetendo l'associazione del database Northwind vista in precedenza oppure aggiungere un nuovo progetto, sempre Windows Forms, alla soluzione esistente.
Aggiungete, poi, una nuova classe LINQ-To-SQL, utilizzando le modalità viste in precedenza.
Nella finestra Esplora database selezionate due tabelle (CTRL+Clic), Employees e EmployeeTerritories. Le due tabelle hanno in comune la proprietà EmployeeID, che permetterà di metterle in relazione.
Da ultimo, trascinate le due tabelle sulla superficie del designer della classe LINQ-To-SQL, in modo che a video appaia quanto riportato in figura:

L'IDE genera automaticamente una relazione tra le due tabelle e ciò è riprovato dalla freccia visibile nel centro della figura.
A questo punto è necessario aggiungere una nuova origine dati, ripetendo la procedura vista nel primo esempio, al fine di collegare l'applicazione all'oggetto di mapping generato mediante la classe LINQ-To-SQL. Pertanto si utilizzerà ancora il comando Aggiungi nuova origine dati del menu Dati. Dal wizard si selezionerà dapprima il collegamento a un oggetto, quindi ancora la classe Employees, sebbene siano visibili due classi, Employees e EmployeeTerritories, come risulta dalle seguenti figure:

E' possibile selezionare la sola classe Employees perché, in questo scenario, l'IDE riconosce automaticamente la dipendenza tra le due tabelle. Infatti, se osservate la risultante finestra Origini dati, potete facilmente rilevare come la seconda classe sia mostrata a un livello di dipendenza dalla prima:

Ora trascinate i due oggetti Employees ed EmployeeTerritories dalla finestra Origini dati al form, di modo che appaia così:

L'IDE ha generato, in sequenza, due oggetti BindingSource per le tabelle e un BindingNavigator collegato a entrambe le sorgenti dati, che permetterà una navigazione sincronizzata tra i dati esposti dalle tabelle stesse. Passiamo ora alla scrittura di un breve frammento di codice per il popolamento della prima griglia che, grazie al data-binding, avrà come conseguenza il popolamento sincronizzato della seconda.

Popolamento della prima griglia
Si rende ora necessario attivare la visualizzazione dell'editor di codice. Anche in questo frangente la prima operazione da svolgere è ottenere un'istanza del DataContext, da dichiarare a livello di classe:

  Private myDataContext As New NorthwindDataContext

Il passaggio successivo può essere, come esempio, quello di popolare la prima griglia con tutti gli impiegati. Questo ci permetterà di capire tramite osservazione a video, come vedremo tra pochi passaggi, come sia possibile filtrare i risultati tramite LINQ. L'azione può essere svolta nell'evento Load del form:

  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                         Handles MyBase.Load

    EmployeesBindingSource.DataSource = myDataContext.Employees

  End Sub

Quest'azione collega l'oggetto BindingSource relativo alla tabella Employees alla classe LINQ-To-SQL che mappa i dati contenuti nella tabella omonima esposta dal database. Avviando l'applicazione, il risultato a video è questo:

Potete fare clic sui valori delle colonne EmployeeID per vedere in azione la relazione tra le due tabelle.

Abilitare la modifica e il salvataggio dei dati
Le classi DataContext, oltre a costituire un riferimento managed agli oggetti esposti dal database, permettono di salvare nel database stesso le modifiche ai dati fatte tramite i controlli Windows Forms. In primo luogo va abilitato il pulsante di salvataggio implementato nel BindingNavigator, facendo clic destro sul pulsante stesso e selezionando il comando Enabled del menu contestuale. Facendo doppio clic sul pulsante, l'IDE genera automaticamente il gestore dell'evento, che può essere manipolato in questo modo:

    Private Sub EmployeesBindingNavigatorSaveItem_Click(ByVal sender As System.Object, _
                                                     ByVal e As System.EventArgs) _
                                                     Handles EmployeesBindingNavigatorSaveItem.Click

        'Verifica il valore del controllo
        Me.Validate()

        'Prima di salvare i dati, è necessario terminare
        'la modalità di modifica in entrambi i BindingSource
        Me.EmployeesBindingSource.EndEdit()
        Me.EmployeeTerritoriesBindingSource.EndEdit()

        Try
            'Salva le modifiche nel database
            myDataContext.SubmitChanges()
        Catch ex As Exception
            MessageBox.Show(ex.ToString)
        End Try

    End Sub

I commenti nel codice dovrebbero aiutare nella comprensione, ad ogni buon conto i vari passaggi possono essere così schematizzati:

Dopo aver visto con quale modalità il DataContext (quindi l'oggetto .NET) consenta di memorizzare i dati nel database SQL, possiamo passare alla scrittura di una query LINQ aggiungendo una particolarità: l'utilizzo di un apposito Log.

Filtrare i dati con LINQ
Tornate per un momento al designer ed aggiungete un pulsante al form, quindi fate doppio clic sul pulsante stesso affinché Visual Basic 2008 aggiunga un gestore per tale evento. All'interno di tale gestore digitate il seguente codice:

  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                            Handles Button1.Click

    'Mostra nella finestra di output le istruzioni SQL
    'inviate dal compilatore a SQL Server
    myDataContext.Log = Console.Out

    Dim risultato = From impiegato In myDataContext.Employees _
                    Order By impiegato.LastName _
                    Where impiegato.Region <> "" _
                    Select impiegato

    EmployeesBindingSource.DataSource = risultato

  End Sub

Innanzitutto, consideriamo la prima riga di codice. La proprietà Log della classe DataContext consente di mostrare in una finestra esterna (in questo caso la finestra di Output dell'IDE stabilita dalla proprietà Console.Out) le effettive istruzioni SQL inviate dal compilatore a SQL Server per eseguire l'interrogazione sul database. Le righe di codice successive, invece, riprendono la query formulata nel primo esempio, ossia ordinano tutti gli impiegati della tabella in base al cognome, quindi selezionano tutti gli impiegati per i quali la proprietà Region non sia vuota. Avviando l'applicazione e facendo clic sul pulsante appena aggiunto, il primo risultato visivo da considerare è il seguente:

Appare evidente come i dati siano stati filtrati, su entrambe le tabelle, dalla query expression impostata nel codice. Il secondo aspetto visivo da considerare è costituito dalla finestra di Output di Visual Basic:

Come vedete, la finestra di output mostra l'elenco sequenziale delle istruzioni SQL "pure" che il compilatore ha inviato a SQL Server per eseguire l'interrogazione sul database e delle quali la query LINQ costituisce una rappresentazione managed.

Dopo aver visto due esempi dedicati a Windows Forms, vedremo ora un piccolo esempio introduttivo di data-binding con LINQ-To-SQL nell'ambito di un'applicazione Windows Presentation Foundation.

Esempio di LINQ-To-SQL in WPF
Finora abbiamo visto l'utilizzo di LINQ nell'ambito di applicazioni Windows Forms. Ma l'accesso ai dati è parte importante anche nell'ambito di applicazioni Windows Presentation Foundation. A differenza di Windows Forms, WPF non mette a disposizione controlli specifici per il binding dei dati, come ad esempio il BindingSource e il BindingNavigator. Questo perché i controlli utente di WPF espongono modalità specifiche per il data-binding, che consentono il completo accesso ai dati senza l'utilizzo di ulteriori controlli.
In questa parte dell'articolo creeremo un 'applicazione WPF di esempio in cui un controllo ListView verrà popolato con un elenco di impiegati, estratto e filtrato con LINQ.

Cominciamo a realizzare un progetto di tipo Applicazione WPF, creandone uno da zero oppure aggiungendolo alla soluzione esistente. Anche in questo caso è necessario eseguire il passaggio visto nel primo esempio, quindi l'aggiunta di una classe LINQ-To-SQL (che costituirà il DataContext) sulla quale va trascinata una tabella, che nel nostro esempio sarà sempre la Employees. Ora, a differenza di quanto avviene in Windows Forms, in cui si aggiunge una origine dati e la si collega all'applicazione, in WPF non c'è questa possibilità ma il data-binding si può comunque fare sul singolo controllo.

A questo punto implementiamo, via XAML, una semplice ListView nella finestra principale dell'applicazione. Per comodità riporto il solo codice da aggiungere, saltando quello relativo alla definizione dell'oggetto Window:

    <Grid>
        <ListView Name="List1"/>
    </Grid>

L'utilizzo dell'attributo Name per assegnare un identificatore al controllo è necessario, poiché ci occorre nel fare riferimento al controllo stesso tramite codice Visual Basic. A tal proposito è ora necessario passare all'editing del file di code-behind, all'interno del quale dichiareremo, in primo luogo, l'immancabile DataContext:

  Private myDataContext As New NorthwindDataContext

Scopo dell'esempio è, come detto, popolare la nostra ListView con un elenco filtrato di impiegati. A titolo esemplificativo possiamo eseguire questa operazione nell'evento Loaded dell'oggetto Window:

  Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) _
                             Handles Me.Loaded

    'Seleziona il cognome degli impiegati per i quali
    'la propriet\u224 ? Region non sia vuota
    Dim risultato = From impiegato In myDataContext.Employees _
                    Order By impiegato.LastName _
                    Where impiegato.Region <> "" _
                    Select impiegato.LastName

    'Esegue il popolamento del controllo
    List1.ItemsSource = risultato

  End Sub

Avviando l'applicazione, dopo aver atteso qualche secondo necessario al collegamento dei dati, la finestra si presenta così:

Quindi la ListView è stata popolata con i cognomi degli impiegati per i quali la proprietà Region prevista nella tabella non sia vuota.
Come è facile intuire, anche nelle applicazioni WPF è possibile utilizzare il metodo SubmitChanges della classe DataContext per inviare le modifiche al database, ad esempio in casi in cui gli elementi di una ListView/ListBox siano modificabili o in qualunque altro controllo data-bound che accetti la modifica dei valori.

Qualche spunto di studio
Vi segnalo i seguenti link a risorse dedicate a LINQ-To-SQL, nell'ambito dello sviluppo con Visual Basic 2008:

Conclusioni
Anche in questo caso abbiamo solo grattato la superficie di un aspetto molto importante della programmazione basata su .NET 3.5, utilizzando Visual Basic 2008. Tuttavia abbiamo imparato i passaggi fondamentali per collegare un database SQL alle nostre applicazioni, a interrogarlo mediante LINQ e i vantaggi di utilizzare delle classi gestite di mapping. Non di meno, abbiamo visto come, ancora una volta, l'ambiente di lavoro di Visual Basic permetta di risparmiare molto tempo, scrivendo automaticamente il codice necessario all'implementazione delle classi LINQ-To-SQL, lasciandoci il solo compito di agire tramite un comodo designer.

Vi ricordo, infine, che sul mio blog è disponibile uno screen-cast che ho realizzato in merito a LINQ-To-SQL e che potete scaricare liberamente. Per ulteriori informazioni potete contattarmi al mio indirizzo visitare il mio blog.