Skip to content

2. Parte 2

2.1. Introduction

Inizieremo ricordando ciò che è stato fatto nella parte 1 e, in particolare, l’architettura a tre livelli [web, domain, dao] utilizzata. Nella soluzione proposta, il livello [dao] era un livello di test: la fonte dei dati era implementata da un oggetto [ArrayList]. In questo articolo ci soffermeremo sul livello [dao], presentando diverse possibili implementazioni dello stesso quando i dati si trovano in un SGBD.

Strumenti utilizzati:

  • il SGBD Firebird - cfr. allegato, paragrafo 3.5.
  • il SGBD MSDE (Microsoft Data Engine) - vedi allegato, paragrafo 3.12.
  • IBExpert, edizione personale per l’amministrazione grafica di SGBD Firebird - cfr. allegato, paragrafo 3.6.
  • EMS MS SQL Manager per l'amministrazione grafica di SGBD MSDE - cfr. allegato, paragrafo 3.14.
  • Ibatis SqlMap per il livello di accesso ai dati del SGBD - cfr. paragrafo 2.5.6.2.

In una scala che va da principiante a intermedio ad avanzato, questo documento rientra nella sezione [intermédiaire-avancé]. Per comprenderlo sono necessari diversi prerequisiti. Alcuni di essi possono essere acquisiti nei documenti che ho scritto. In tal caso, li cito. È ovvio che si tratta solo di un suggerimento e che il lettore può utilizzare i propri documenti preferiti.

  • linguaggio VB.net: [Introduction au langage VB.NET par l'exemple ]
  • programmazione web in VB.net: [Développement WEB avec ASP.NET 1.1 ]
  • utilizzo dell’aspect IoC di Spring: [Spring IoC pour .NET ]
  • Documentazione Ibatis SqlMap: [http://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Documentazione Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf]
  • Documentazione Spring.net: [http://www.springframework.net/documentation.html]

Qui presentiamo gli elementi dell’applicazione web semplificata di e-commerce analizzata nella parte 1. Questa consente ai clienti del web di:

  • di consultare un elenco di articoli provenienti da un database
  • di inserirne alcuni in un carrello elettronico
  • confermare il carrello. Tale conferma ha come unico effetto l’aggiornamento, nel database, delle scorte degli articoli acquistati.

2.2.1. Le viste dell'applicazione

Le diverse viste presentate all’utente sono le seguenti:

  • la schermata [ERREURS] che segnala eventuali errori dell'applicazione

Image

2.2.2. Architettura generale dell'applicazione

L'applicazione realizzata nella parte 1 presenta un'architettura a tre livelli:

  • i tre livelli sono stati resi indipendenti grazie all’utilizzo di interfacce
  • l'integrazione dei diversi livelli è stata realizzata con Spring
  • ogni livello è associato a spazi dei nomi distinti: web (livello UI), domain (livello business) e dao (livello di accesso ai dati).

L'applicazione segue un'architettura MVC (Modello - Vista - Controller). Se riprendiamo lo schema a livelli riportato sopra, l'architettura MVC si integra in esso nel modo seguente:

L'elaborazione di una richiesta da parte di un cliente si svolge secondo le seguenti fasi:

  1. il cliente invia una richiesta al controller. In questo caso, il controller è una pagina .aspx a cui viene assegnato un ruolo specifico. Essa riceve tutte le richieste dei clienti. È la porta d’ingresso dell’applicazione. È la C di MVC.
  2. il controller elabora questa richiesta. A tal fine, potrebbe aver bisogno dell’aiuto del livello di business, ovvero il modello M nella struttura MVC.
  3. Il controller riceve una risposta dal livello di business. La richiesta del cliente è stata elaborata. Ciò può comportare diverse possibili risposte. Un esempio classico è
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
  4. il controller sceglie la risposta (= vista) da inviare al cliente. Si tratta, nella maggior parte dei casi, di una pagina contenente elementi dinamici. Il controller fornisce tali elementi alla vista.
  5. La vista viene inviata al cliente. È la V di MVC.

2.2.3. Il modello

Il modello M di MVC è costituito qui dai seguenti elementi:

  1. le classi di business
  2. le classi di accesso ai dati
  3. il database

2.2.3.1. Il database

Il database contiene solo una tabella denominata ARTICLES, generata con i seguenti comandi SQL:

CREATE TABLE ARTICLES (
    ID            INTEGER NOT NULL,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          NUMERIC(15,2) NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* vincoli */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
/* chiave primaria */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chiave primaria che identifica in modo univoco un articolo
nom
nome dell'articolo
prix
il suo prezzo
stockactuel
disponibilità attuale
stockminimum
la quantità di magazzino al di sotto della quale è necessario effettuare un ordine di rifornimento

2.2.3.2. Gli spazi dei nomi del modello

Il modello M è fornito sotto forma di due spazi dei nomi:

  • istia.st.articles.dao: contiene le classi di accesso ai dati del livello [dao]
  • istia.st.articles.domain: contiene le classi di business del livello [domain]

Ciascuno di questi spazi dei nomi è contenuto in un proprio file "assembly":

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: l'interfaccia di accesso al livello [dao]. È l'unica interfaccia visibile al livello [domain]. Non ne vede altre.
- [Article]: classe che definisce un articolo
- [ArticlesDaoArrayList]: classe di implementazione dell’interfaccia [IArticlesDao] con una classe [ArrayList]
livello di accesso ai dati - si trova interamente nel livello [dao] dell'architettura a tre livelli dell'applicazione web
webarticles-domain
- [IArticlesDomain]: l'interfaccia di accesso al livello [domain]. È l'unica interfaccia visibile al livello web. Non ne vede altre.
- [AchatsArticles]: una classe che implementa [IArticlesDomain]
- [Achat]: classe che rappresenta l’acquisto di un cliente
- [Panier]: classe che rappresenta l'insieme degli acquisti di un cliente
rappresenta il modello degli acquisti sul web - si trova interamente nel livello [domain] dell’architettura a tre livelli dell’applicazione web

2.2.4. Implementazione e test dell’applicazione [webarticles]

2.2.4.1. Distribuzione

Distribuiamo l’applicazione sviluppata nella parte 1 dell’articolo in una cartella denominata [runtime]:

 

Note:

La cartella [runtime] contiene tre file e due sottocartelle:

  • i controller [global.asax] e [main.aspx]
  • il file di configurazione [web.config]
  • la cartella [bin] che contiene:
    • i file DLL dei tre livelli [webarticles-dao.dll], [webarticles-domain.dll], [webarticles-web.dll]
    • i file necessari per Spring [Spring-Core.*], [log4net.dll]
  • la cartella [vues] che contiene il codice di presentazione delle diverse viste.
  • la presenza dei file di codice .vb è superflua poiché la loro versione compilata si trova in DLL.

2.2.4.2. Test

Configuriamo il server web [Cassini] nel modo seguente:

Image

con:

Percorso fisico: D:\data\serge\lavoro\2004-2005\aspnet\webarticles-010405\runtime\

Percorso virtuale: /webarticles

Con un browser accediamo a URL [http://localhost/webarticles/main.aspx]

Image

Ricordiamo che il livello [dao] è implementato da una classe che memorizza gli articoli in un oggetto [ArrayList]. Questa classe crea un elenco iniziale di quattro articoli. Partendo dalla vista sopra riportata, utilizziamo i collegamenti del menu per eseguire alcune operazioni. Eccone alcune. La colonna di sinistra rappresenta la richiesta del cliente e quella di destra la risposta che gli viene fornita.

2.2.5. Il livello [dao] rivisitato

Nella nostra prima implementazione del livello [dao], l’interfaccia [IArticlesDao] per l’accesso ai dati era stata implementata tramite una classe che memorizzava gli articoli in un oggetto [ArrayList]. Ciò ci ha permesso di non soffermarci troppo su questo livello e di dimostrare che ciò che contava era solo la sua interfaccia e non la sua implementazione. Abbiamo così potuto realizzare un’applicazione web operativa. Questa presenta tre livelli: [web], [domain] e [dao]. Proporremo qui diverse implementazioni del livello [dao]. Ciascuna di esse potrà sostituire l’attuale livello [dao] senza alcuna modifica ai livelli [domain] e [web]. Questa flessibilità è garantita dal fatto che:

  • il livello [domain] non si rivolge a una classe concreta ma a un'interfaccia [IArticlesDao]
  • grazie a Spring, siamo riusciti a nascondere al livello [domain] il nome della classe di implementazione dell’interfaccia [IArticlesDao].

2.2.5.1. Elementi del livello [dao]

Ricordiamo alcuni degli elementi del livello [dao] che saranno mantenuti nelle nuove implementazioni:

  • - [IArticlesDao]: l’interfaccia di accesso al livello [dao]
  • - [Article]: classe che definisce un articolo

2.2.5.2. La classe [Article]

La classe che definisce un articolo è la seguente:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

         ' campi privati
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

         ' ID articolo
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                If Value <= 0 Then
                    Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
                End If
                Me._id = Value
            End Set
        End Property

         ' nome articolo
        Public Property nom() As String
            Get
                Return _nom
            End Get
            Set(ByVal Value As String)
                If Value Is Nothing OrElse Value.Trim.Equals("") Then
                    Throw New Exception("Le champ nom [" + Value + "] est invalide")
                End If
                Me._nom = Value
            End Set
        End Property

         ' prezzo articolo
        Public Property prix() As Double
            Get
                Return _prix
            End Get
            Set(ByVal Value As Double)
                If Value < 0 Then
                    Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
                End If
                Me._prix = Value
            End Set
        End Property

         ' stock attuale articolo
        Public Property stockactuel() As Integer
            Get
                Return _stockactuel
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
                End If
                Me._stockactuel = Value
            End Set
        End Property

         ' scorte minime articolo
        Public Property stockminimum() As Integer
            Get
                Return _stockminimum
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
                End If
                Me._stockminimum = Value
            End Set
        End Property

         ' produttore predefinito
        Public Sub New()
        End Sub

         ' produttore con proprietà
        Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
            Me.id = id
            Me.nom = nom
            Me.prix = prix
            Me.stockactuel = stockactuel
            Me.stockminimum = stockminimum
        End Sub

         ' metodo di identificazione dell'articolo
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Questa classe offre:

  1. un costruttore che consente di impostare le 5 informazioni relative a un articolo: [id, nom, prix, stockactuel, stockminimum]
  2. proprietà pubbliche che consentono di leggere e scrivere le 5 informazioni.
  3. una verifica dei dati inseriti nell'articolo. In caso di dati errati, viene generata un'eccezione.
  4. un metodo toString che consente di ottenere il valore di un articolo sotto forma di stringa di caratteri. Ciò è spesso utile per il debug di un'applicazione.

2.2.5.3. L'interfaccia [IArticlesDao]

L'interfaccia [IArticlesDao] è definita come segue:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' elenco di tutti gli articoli
        Function getAllArticles() As IList
         ' aggiunge un articolo
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' elimina un articolo
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' modifica un articolo
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' cerca un articolo
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' elimina tutti gli articoli
        Sub clearAllArticles()
         ' modifica le scorte di un articolo
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

Il ruolo dei diversi metodi dell'interfaccia è il seguente:

getAllArticles
restituisce tutti gli articoli presenti nella fonte dati
clearAllArticles
svuota la fonte dati
getArticleById
restituisce l'oggetto [Article] identificato dal suo numero
ajouteArticle
consente di aggiungere un articolo alla fonte dati
modifieArticle
consente di modificare un articolo della fonte dati
supprimerArticle
consente di eliminare un articolo dalla fonte dati
changerStockArticle
consente di modificare lo stock di un articolo nella fonte dati

L'interfaccia mette a disposizione dei programmi client una serie di metodi definiti esclusivamente dalle loro firme. Non si occupa del modo in cui tali metodi saranno effettivamente implementati. Ciò garantisce flessibilità all'applicazione. Il programma client effettua le chiamate su un'interfaccia e non su una sua specifica implementazione.

La scelta di una specifica implementazione avviene tramite un file di configurazione Spring.

2.3. La classe di implementazione [ArticlesDaoPlainODBC]

Proponiamo una nuova implementazione del livello [dao] che presuppone che i dati si trovino in una fonte ODBC. È noto che su Windows, quasi tutti i SGBD presenti sul mercato dispongono di un driver ODBC. Il vantaggio di questa soluzione è che è possibile cambiare SGBD in modo trasparente per l’applicazione. Lo svantaggio è che un driver ODBC che sfrutta solo le caratteristiche comuni a tutti i SGBD è in genere meno performante di un driver scritto specificamente per sfruttare tutto il potenziale di un particolare SGBD. Si può consultare il paragrafo 3.7 per scoprire un esempio di creazione di un codice sorgente ODBC.

2.3.1. Il codice

2.3.1.1. Lo scheletro

La classe [ArticlesDaoPlainODBC] implementa l’interfaccia [IArticlesDao] nel modo seguente:

Imports System
Imports System.Collections
Imports System.Data.Odbc

Namespace istia.st.articles.dao

    Public Class ArticlesDaoPlainODBC
        Implements istia.st.articles.dao.IArticlesDao

         ' campi privati
        Private connexion As OdbcConnection = Nothing
        Private DSN As String
        Private insertCommand As OdbcCommand
        Private updatecommand As OdbcCommand
        Private deleteSomeCommand As OdbcCommand
        Private selectSomeCommand As OdbcCommand
        Private updateStockCommand As OdbcCommand
        Private deleteAllCommand As OdbcCommand
        Private selectAllCommand As OdbcCommand

         ' produttore
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN: nome della fonte ODBC
             ' uid: identificativo dell'utente
             ' password: la sua password
...
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
...
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
...
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
...
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
...
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
...
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
....
        End Function

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
             ' esecuzione di una richiesta SELECT 
....
        End Function

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
....
    End Class

End Namespace

Commenti:

  • riga 3: si importa lo spazio dei nomi contenente le classi .NET per l'accesso alle fonti ODBC
  • riga 11: memorizza la connessione alla fonte ODBC
  • riga 12: memorizza il nome DSN della fonte dati
  • righe 13-19 - variabili private di tipo [OdbcCommand] che definiscono le query SQL utilizzate dai diversi metodi della classe
  • righe 22-27 - il costruttore. Riceve gli elementi che gli consentono di costruire l'oggetto [OdbcConnection] che collegherà il codice alla fonte dati ODBC
  • righe 29-31 - il metodo per l'aggiunta di un articolo
  • righe 33-35 - il metodo per modificare le scorte di un articolo
  • righe 37-39 - il metodo che elimina tutti gli articoli dalla fonte dati ODBC
  • righe 41-43 - il metodo che recupera l'elenco di tutti gli articoli dalla fonte ODBC
  • righe 45-47 - il metodo che consente di recuperare un articolo specifico
  • righe 49-51 - il metodo che consente di modificare alcuni campi di un articolo di cui si conosce il numero
  • righe 53-55 - il metodo che consente di eliminare un articolo di cui si conosce il numero
  • righe 57-60 - metodo di utilità che consente di eseguire un [SELECT] sulla fonte di dati e di restituirne il risultato
  • righe 62-64 - metodo di utilità che consente di eseguire un [INSERT, UPDATE, DELETE] sulla fonte dati e di restituirne il risultato

2.3.1.2. Il costruttore

         ' costruttore
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN: nome della fonte ODBC
             ' uid: identità dell'utente
             ' password: la sua password

             'si recupera il nome del database passato come argomento
            Me.DSN = DSN
            Dim connectString As String = String.Format("DSN={0};UID={1};PASSWORD={2}", DSN, uid, password)
             'si instanzia la connessione
            connexion = New OdbcConnection(connectString)
             ' si preparano le query SQL
            insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
            updatecommand = New OdbcCommand("update ARTICLES set nom=?, prix=?, stockactuel=?, stockminimum=? where id=?", connexion)
            deleteSomeCommand = New OdbcCommand("delete from ARTICLES where id=?", connexion)
            selectSomeCommand = New OdbcCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=?", connexion)
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)
            selectAllCommand = New OdbcCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New OdbcCommand("delete from ARTICLES", connexion)
        End Sub

Commenti:

  • riga 2 - il costruttore riceve le tre informazioni necessarie per connettersi a una fonte ODBC: il nome DSN della fonte, l'identità con cui effettuare la connessione e la password associata.
  • riga 8 - si memorizza il nome DSN della sorgente per poterlo riportare nei messaggi di errore.
  • riga 9 - viene istanziato l'oggetto [OdbcConnection]. Una connessione istanziata non è una connessione aperta. È il metodo [open] che provvede all'apertura.
  • righe 12-19 - si preparano le query SQL negli oggetti [OdbcCommand]. In questo modo non sarà necessario ricostruirle ogni volta che se ne avrà bisogno. I parametri formali ? delle query saranno sostituiti al momento dell'esecuzione della query con valori reali.

2.3.1.3. Il metodo executeQuery

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
             ' esecuzione di una query SELECT 
             ' dichiarazione dell'oggetto che consente l'accesso a tutte le righe della tabella dei risultati
            Dim myReader As OdbcDataReader = Nothing
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la query
                myReader = query.ExecuteReader()
                 'si dichiara un elenco di articoli per restituirlo in seguito
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'si prepara un articolo con i valori del lettore
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     ': si aggiunge l'articolo all'elenco
                    articles.Add(unArticle)
                End While
                 'si restituisce il risultato
                Return articles
            Finally
                 ' liberazione delle risorse
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

Commenti:

  • il metodo [executeQuery] è un metodo di utilità che:
    • esegue una query [SELECT id, nom, prix, stockactuel, stockminimum from ARTICLES ...] sulla fonte dati
    • restituisce il risultato sotto forma di un elenco di oggetti [Article]
  • riga 1 - l'unico parametro del metodo è l'oggetto [OdbcCommand] contenente la query [Select] da eseguire.
  • riga 7 - la connessione è aperta. Verrà chiusa alla riga 29, indipendentemente dal fatto che si verifichi o meno un errore.
  • riga 9 - viene istanziato l'oggetto [OdbcDataReader] necessario per elaborare il risultato di [Select]
  • righe 13-23 - ogni riga risultante da [Select] viene inserita in un oggetto [Article] che si unirà agli altri articoli in un oggetto [ArrayList]
  • l'elenco degli articoli viene restituito alla riga 25
  • Non viene gestita alcuna eccezione. Dovrà essere gestita dal codice che chiama questo metodo.

2.3.1.4. Il metodo executeUpdate

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
             ' esecuzione di una richiesta di aggiornamento
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la richiesta
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' rilascio delle risorse
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

Commenti:

  • il metodo riceve un oggetto [OdbcCommand] che contiene una richiesta SQL di tipo [Insert, Update, Delete].
  • La connessione viene aperta alla riga 5. Verrà chiusa alla riga 10, indipendentemente dal fatto che si sia verificata o meno un'eccezione.
  • La richiesta di aggiornamento viene eseguita alla riga 7. Il risultato, ovvero il numero di righe della tabella ARTICLES modificate dalla richiesta, viene restituito immediatamente.

2.3.1.5. Il metodo ajouteArticle

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la richiesta di inserimento
                With insertCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", unArticle.id))
                    .Add(New OdbcParameter("nom", unArticle.nom))
                    .Add(New OdbcParameter("prix", unArticle.prix))
                    .Add(New OdbcParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OdbcParameter("stockminimum", unArticle.stockminimum))
                End With
                Try
                     'si esegue
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'errore nella query
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

Commenti:

  • riga 1 - il metodo riceve l'articolo da aggiungere alla fonte dati ODBC. Restituisce il numero di righe interessate da questa operazione, c.a.d. 1 o 0
  • righe 3 e 20 - il metodo è sincronizzato. Questo vale per tutti i metodi di accesso ai dati. Ciò implica che un solo thread alla volta potrà operare sulla fonte di dati. Probabilmente si tratta di un approccio troppo conservativo. Esistono alternative migliori, in particolare quella di includere queste operazioni all’interno di transazioni. In questo caso, è il SGBD a gestire gli accessi concorrenti. Non abbiamo voluto introdurre il concetto di transazione in questa fase. Spring ci offre la possibilità di introdurle nel livello [domain]. Forse avremo occasione di tornarci sopra in un altro articolo.
  • Nelle righe 5-12, vengono assegnati i valori ai parametri formali della richiesta dell’oggetto [insertCommand] inizializzato dal costruttore. Ricordiamo la richiesta:
insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)

I 5 valori necessari per la query sono forniti dalle righe 7-11.

  • Nelle righe 13-19, la richiesta viene eseguita. Se va a buon fine, viene restituito il risultato. In caso contrario, viene generata un’eccezione generica con un messaggio di errore esplicito

2.3.1.6. Il metodo modifieArticle

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di aggiornamento
                With updatecommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("nom", unArticle.nom))
                    .Add(New OdbcParameter("prix", unArticle.prix))
                    .Add(New OdbcParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OdbcParameter("stockminimum", unArticle.stockminimum))
                    .Add(New OdbcParameter("id", unArticle.id))
                End With
                 ' si esegue
                Try
                     'si esegue la query di inserimento
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

Commenti:

  • riga 1 - il metodo riceve l’articolo da modificare dalla fonte dati ODBC. Restituisce il numero di righe interessate da questa operazione, c.a.d. 1 o 0
  • i commenti del metodo [ajouteArticle] possono essere riportati qui

2.3.1.7. Il metodo supprimeArticle

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di cancellazione
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 'si esegue
                Try
                     'si esegue la query di cancellazione
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

Commenti:

  • riga 1 - il metodo riceve il numero dell'articolo da eliminare dalla fonte dati ODBC. Restituisce il numero di righe interessate da questa operazione, c.a.d. 1 o 0
  • i commenti del metodo [ajouteArticle] possono essere riportati qui

2.3.1.8. Il metodo getAllArticles

            Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si esegue la query SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'si restituisce l'elenco
                    Return articles
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

Commenti:

  • riga 1 - il metodo non riceve alcun parametro. Restituisce l'elenco di tutti gli articoli della fonte dati ODBC
  • la query [Select], che richiede tutti gli articoli, viene inviata al metodo [executeQuery] - riga 6
  • l'elenco ottenuto viene restituito alla riga 8
  • nelle righe 9-12 viene gestita un'eventuale eccezione

2.3.1.9. Il metodo getArticleById

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 'la si esegue
                Try
                     'si esegue la query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'si verifica se l'articolo è stato trovato
                    If articles.Count = 0 Then Return Nothing
                     'si restituisce l'articolo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

Commenti:

  • riga 1 - il metodo riceve come parametro il numero dell'articolo desiderato. Restituisce tale numero se lo trova nella fonte ODBC, altrimenti restituisce il riferimento [nothing].
  • La richiesta [Select] relativa all'articolo viene inizializzata alle righe 5-8
  • viene eseguita alla riga 12: si ottiene un elenco di articoli
  • se l'elenco è vuoto, viene restituito il riferimento [nothing] alla riga 14
  • altrimenti, l’unico articolo dell’elenco viene restituito alla riga 16
  • nelle righe 17-20 viene gestita un'eventuale eccezione

2.3.1.10. Il metodo clearAllArticles

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si esegue la richiesta di cancellazione
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

Commenti:

  • riga 1 - il metodo non riceve alcun parametro e non restituisce nulla
  • riga 6 - viene eseguita la richiesta di eliminazione di tutti gli articoli
  • righe 7-10: viene gestita un'eventuale eccezione

2.3.1.11. Il metodo changerStockArticle

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si sta preparando la richiesta di aggiornamento delle scorte
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("mvt1", mouvement))
                    .Add(New OdbcParameter("id", idArticle))
                    .Add(New OdbcParameter("mvt2", mouvement))
                End With
                 'si esegue
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'errore di richiesta
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

Commenti:

  • riga 1 - il metodo riceve come parametri il numero dell'articolo di cui si deve modificare la giacenza e il relativo incremento (positivo o negativo). Restituisce il numero di righe modificate dall'operazione c.a.d. 0 o 1.
  • righe 5-10: viene inizializzata la query [updateStockCommand]. Ricordiamo il testo della query SQL:
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)

si noti che lo stock viene modificato solo se, una volta modificato, rimane >=0.

  • La richiesta di aggiornamento delle scorte dell’articolo viene eseguita alla riga 13 e il risultato restituito
  • righe 14-18, si gestisce un'eventuale eccezione

2.3.2. Generazione dell'assembly del livello [dao]

Il progetto Visual Studio di questa nuova versione del layer [dao] presenta la seguente struttura:

Image

Il progetto è configurato per generare un DLL denominato [webarticles-dao.dll]:

2.3.3. Test NUnit del livello [dao]

2.3.3.1. Creazione di una fonte ODBC-Firebird

Per testare il nostro nuovo livello [dao] abbiamo bisogno di una fonte di dati ODBC e quindi di un database. Utilizziamo Firebird SGBD (paragrafo 3.5). Con IBExpert (paragrafo 3.6), creiamo il seguente database degli articoli:

L’amministratore di questo database sarà l’utente [SYSDBA] con la password [masterkey]. Creiamo alcuni articoli:

Image

Ora creiamo la seguente sorgente Firebird ODBC (cfr. paragrafo 3.7):

Il file sorgente ODBC creato presenta le seguenti caratteristiche:

  • nome DSN: odbc-firebird-articoli
  • ID di connessione: SYSDBA
  • password associata: masterkey

2.3.3.2. La classe di test NUnit

Abbiamo già scritto una classe di test per il livello [dao] inizialmente creato. Se il lettore se lo ricorda, questa classe non testava una classe specifica, ma l’interfaccia [IArticlesDao]:

Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO

Namespace istia.st.articles.tests

    <TestFixture()> _
    Public Class NunitTestArticlesArrayList
         ' l'oggetto da testare
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' si recupera un'istanza del generatore di oggetti Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' si richiede l'istanziazione dell'oggetto articlesdao
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

....

Si nota che nel metodo dell'attributo <Setup()> si richiede a Spring un riferimento al singleton denominato [articlesdao] di tipo [IArticlesDao], ovvero del tipo dell'interfaccia. Il singleton [articlesdao] era definito dal seguente file di configurazione [spring-config.xml]:

<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">

<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao"/>
</objects>

Dimostriamo che la classe di test iniziale ci permette di testare il nostro nuovo livello [dao] senza modifiche né ricompilazioni.

  • Creiamo nella cartella Visual Studio del nostro nuovo livello [dao] la cartella [tests] (in basso a destra) copiando la cartella [bin] dal progetto di test del livello iniziale [dao] (in basso a sinistra). Se necessario, il lettore è invitato a rivedere il progetto di test della prima versione del livello [dao] nella prima parte dell’articolo.
  • nella cartella [tests] sostituiamo il file DLL [webarticles-dao.dll] proveniente dalvecchio livello [dao] con DLL e [webarticles-dao.dll] provenienti dal nuovo livello [dao]
  • Modifichiamo il file di configurazione [spring-config.xml] per istanziare la nuova classe [ArticlesDaoPlainODBC]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">

<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, webarticles-dao">
        <constructor-arg index="0">
            <value>odbc-firebird-articles</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>SYSDBA</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>masterkey</value>
        </constructor-arg>
    </object>
</objects>

Commenti:

  • riga 6, l'oggetto [articlesdao] è ora associato a un'istanza della classe [ ArticlesDaoPlainODBC]
  • questa classe ha un costruttore a tre argomenti:
  • il nome della fonte DSN - riga 8
  • l'identità con cui si accederà al database - riga 11
  • la password associata a tale identità - riga 14

Qui riprendiamo le informazioni della sorgente ODBC-Firebird che abbiamo creato in precedenza.

2.3.3.3. Test

Ora siamo pronti per i test. Utilizzando l’applicazione [Nunit-Gui], carichiamo il file DLL [test-webarticles-dao.dll] dalla cartella [tests] sopra indicata ed eseguiamo il test [testGetAllArticles]:

Image

Osservando la schermata sopra riportata, si potrebbe rimpiangere il nome [NUnitTestArticlesDaoArrayList] assegnato inizialmente alla classe di test. Ciò crea confusione. È infatti la classe [ArticlesDaoPlainODBC] ad essere qui testata. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che avevamo inserito nella tabella [ARTICLES]. Ora eseguiamo tutti i test:

Image

Nella finestra a sinistra è riportato l'elenco dei metodi testati. Il colore del puntino che precede il nome di ciascun metodo indica se il metodo ha superato il test (verde) o meno (rosso). Il lettore che visualizza questo documento sullo schermo potrà constatare che tutti i test sono stati superati.

2.3.3.4. Conclusione

Abbiamo appena dimostrato che:

  • poiché la classe di test NUnit faceva riferimento non a una classe ma a un'interfaccia;
  • perché il nome esatto della classe di istanziazione dell’interfaccia era fornito in un file di configurazione e non nel codice;
  • poiché Spring si occupava di istanziare la classe e di fornirne un riferimento al codice di test;

quindi il codice di test scritto per il livello iniziale [dao] rimaneva valido per una nuova implementazione di quello stesso livello. Non abbiamo avuto bisogno di accedere al codice della classe di test. Abbiamo utilizzato solo la sua versione compilata, quella generata durante il test del livello iniziale [dao]. Trarremo conclusioni analoghe quando sarà necessario integrare il nuovo livello [dao] nell’applicazione [webarticles].

2.3.4. Integrazione del nuovo livello [dao] nell’applicazione [webarticles]

2.3.4.1. I test di integrazione

Ricordiamo che la versione iniziale dell’applicazione [webarticles] era stata distribuita nella cartella [runtime] seguente:

 

Si invita il lettore a consultare, se necessario, il paragrafo 2.2.4 che illustra in dettaglio le modalità di distribuzione dell’applicazione [webarticles]. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • nella cartella [bin], il file DLL del vecchio livello [dao] viene sostituito dal file DLL del nuovo livello [dao]
  • in [runtime], il file di configurazione [web.config] viene sostituito da un file che tiene conto della nuova classe di implementazione del livello [dao]:

Il nuovo file di configurazione [web.config] è il seguente:

<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
    <spring>
        <context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, webarticles-dao">
                <constructor-arg index="0">
                    <value>odbc-firebird-articles</value>
                </constructor-arg>
                <constructor-arg index="1">
                    <value>SYSDBA</value>
                </constructor-arg>
                <constructor-arg index="2">
                    <value>masterkey</value>
                </constructor-arg>
            </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

Commenti:

  • le righe 14-24 associano al singleton [articlesDao] un'istanza della nuova classe [ArticlesDaoPlainODBC]. Questa è l'unica modifica. L'abbiamo già riscontrata durante i test del nuovo livello [dao].

Siamo pronti per i test. Configuriamo il server web [Cassini] allo stesso modo descritto nel paragrafo 2.2.4. Inizializziamo la tabella degli articoli [Firebird] con i seguenti valori:

Image

Assicurarsi che il server web Cassini e i server SGBD e [Firebird] siano in esecuzione. Con un browser, accediamo all’URL [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES] nel database [Firebird]:

Image

Gli articoli [parapluie] e [bottes] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [chapeau] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.3.4.2. Conclusione

Cosa abbiamo fatto?

  • abbiamo ripristinato la versione di distribuzione della versione precedente;
  • abbiamo sostituito l'articolo DLL del livello [dao] con una nuova versione. I file DLL dei livelli [web] e [domain] sono rimasti invariati;
  • abbiamo modificato il file di configurazione [web.config] affinché tenga conto della nuova classe di implementazione del livello [dao]

Tutto ciò garantisce un codice pulito e offre una grande flessibilità di evoluzione all’applicazione web. Queste importanti caratteristiche ci vengono fornite da due scelte architetturali:

  • l’accesso ai livelli tramite interfacce
  • l’integrazione e la configurazione dei livelli tramite Spring.

Proponiamo ora una nuova implementazione del livello [dao].

2.4. La classe di implementazione [ArticlesDaoSqlServer]

La seconda implementazione del livello [dao] presuppone che i dati si trovino in un database SQL Server. Microsoft mette a disposizione un SGBD denominato MSDE, che è una versione limitata di SQL Server. In allegato, al paragrafo 3.12, è indicato come ottenerlo e installarlo.

2.4.1. Il codice

La classe [ArticlesDaoSqlServer] è molto simile alla classe [ArticlesDaoPlainODBC] esaminata in precedenza. Pertanto, indicheremo solo le modifiche apportate rispetto alla versione precedente:

  • le classi necessarie si trovano nello spazio dei nomi [System.Data.SqlClient] anziché nello spazio dei nomi [System.Data.Odbc]
  • la connessione di tipo [OdbcConnection] ha ora il tipo [SqlConnection]
  • gli oggetti [OdbcCommand] hanno ora il tipo [SqlCommand]
  • la sintassi delle query SQL configurate cambia. La query di inserimento diventa quindi:
insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)

mentre in precedenza era:

insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
  • il metodo [ajouteArticle] diventa quindi il seguente:
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di inserimento
                With insertCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", unArticle.id))
                    .Add(New SqlParameter("@nom", unArticle.nom))
                    .Add(New SqlParameter("@prix", unArticle.prix))
                    .Add(New SqlParameter("@sa", unArticle.stockactuel))
                    .Add(New SqlParameter("@sm", unArticle.stockminimum))
                End With
                Try
                     'la si esegue
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'errore nella query
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function
  • viene modificato anche il costruttore:
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' server: nome dell'istanza SQL server da raggiungere
             ' databaseName: nome del database da raggiungere
             ' uid: identità dell'utente
             ' password: la sua password

             'si recupera il nome del database passato come argomento
            Me.databaseName = databaseName
             ': si instanzia la connessione
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' si preparano le query SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
...
        End Sub

Il costruttore ora ammette quattro parametri:

             ' server: nome dell'istanza SQL server da raggiungere
             ' databaseName: nome del database da raggiungere
             ' uid: identificativo dell'utente
             ' password: la sua password

Il codice completo della classe [ArticlesDaoSqlServer] è il seguente:

Imports System
Imports System.Collections
Imports System.Data.SqlClient

Namespace istia.st.articles.dao

    Public Class ArticlesDaoSqlServer
        Implements istia.st.articles.dao.IArticlesDao

         ' campi privati
        Private connexion As SqlConnection = Nothing
        Private databaseName As String
        Private insertCommand As SqlCommand
        Private updatecommand As SqlCommand
        Private deleteSomeCommand As SqlCommand
        Private selectSomeCommand As SqlCommand
        Private updateStockCommand As SqlCommand
        Private deleteAllCommand As SqlCommand
        Private selectAllCommand As SqlCommand

         ' costruttore
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' server: nome dell'istanza SQL da raggiungere
             ' databaseName: nome del database da raggiungere
             ' uid: identità dell'utente
             ' password: la sua password

             'si recupera il nome del database passato come argomento
            Me.databaseName = databaseName
             ': si instanzia la connessione
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' si preparano le query SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
            updatecommand = New SqlCommand("update ARTICLES set nom=@nom, prix=@prix, stockactuel=@sa, stockminimum=@sm where id=@id", connexion)
            deleteSomeCommand = New SqlCommand("delete from ARTICLES where id=@id", connexion)
            selectSomeCommand = New SqlCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=@id", connexion)
            updateStockCommand = New SqlCommand("update ARTICLES set stockactuel=stockactuel+@mvt where id=@id and (stockactuel+@mvt)>=0", connexion)
            selectAllCommand = New SqlCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New SqlCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di inserimento
                With insertCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", unArticle.id))
                    .Add(New SqlParameter("@nom", unArticle.nom))
                    .Add(New SqlParameter("@prix", unArticle.prix))
                    .Add(New SqlParameter("@sa", unArticle.stockactuel))
                    .Add(New SqlParameter("@sm", unArticle.stockminimum))
                End With
                Try
                     'si esegue
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'errore nella query
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la richiesta di aggiornamento delle scorte
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@mvt", mouvement))
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 'in esecuzione
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si esegue la richiesta di inserimento
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si sta eseguendo la query SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'viene restituito l'elenco
                    Return articles
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 ': viene eseguita
                Try
                     'si esegue la query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'si verifica se l'articolo è stato trovato
                    If articles.Count = 0 Then Return Nothing
                     'si restituisce l'articolo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di aggiornamento
                With updatecommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@nom", unArticle.nom))
                    .Add(New SqlParameter("@prix", unArticle.prix))
                    .Add(New SqlParameter("@sa", unArticle.stockactuel))
                    .Add(New SqlParameter("@sm", unArticle.stockminimum))
                    .Add(New SqlParameter("@id", unArticle.id))
                End With
                 ' la si esegue
                Try
                     'si esegue la query di inserimento
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di cancellazione
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 'si esegue
                Try
                     'si esegue la query di cancellazione
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Private Function executeQuery(ByVal query As SqlCommand) As IList
             ' esecuzione di una query SELECT 
             ' dichiarazione dell'oggetto che consente l'accesso a tutte le righe della tabella dei risultati
            Dim myReader As SqlDataReader = Nothing
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la query
                myReader = query.ExecuteReader()
                 'si dichiara un elenco di articoli per restituirlo in seguito
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'si prepara un articolo con i valori del lettore
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     ': si aggiunge l'articolo all'elenco
                    articles.Add(unArticle)
                End While
                 'si restituisce il risultato
                Return articles
            Finally
                 ' liberazione delle risorse
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

        Private Function executeUpdate(ByVal updateCommand As SqlCommand) As Integer
             ' esecuzione di una richiesta di aggiornamento
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la richiesta
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' liberazione delle risorse
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Si invita il lettore a interpretare questo codice alla luce dei commenti relativi alla classe [ArticlesDaoPlainODBC] forniti in precedenza.

2.4.2. Generazione dell’assembly del livello [dao]

Il nuovo progetto Visual Studio presenta la seguente struttura:

Image

Il progetto è configurato per generare un DLL denominato [webarticles-dao.dll]:

2.4.3. Test NUnit del livello [dao]

2.4.3.1. Creazione di un server di dati sorgente SQL

Per testare il nostro nuovo livello [dao] abbiamo bisogno di una fonte dati SQL Server e quindi del SGBD SQL Server. Utilizzeremo infatti il SGBD MSDE (MicroSoft Data Engine) (paragrafo 3.12), che è una versione del SQL Server limitata semplicemente dal numero di utenti simultanei ammessi. Con [EMS MS SQL Manager] (paragrafo 3.14) creiamo il seguente database di articoli in un'istanza MSDE denominata [portable1_tahe\msde140405]:

Image

La tabella è di proprietà dell'utente [mdparticles] con password [admarticles]. Il comando Transact-SQL per la creazione della tabella [ARTICLES] è il seguente:

CREATE TABLE [ARTICLES] (
  [id] int NOT NULL,
  [nom] varchar(20) COLLATE French_CI_AS NOT NULL,
  [prix] float(53) NOT NULL,
  [stockactuel] int NOT NULL,
  [stockminimum] int NOT NULL,
  CONSTRAINT [ARTICLES_uq] UNIQUE ([nom]),
  PRIMARY KEY ([id]),
  CONSTRAINT [ARTICLES_ck_id] CHECK ([id] > 0),
  CONSTRAINT [ARTICLES_ck_nom] CHECK ([nom] <> ''),
  CONSTRAINT [ARTICLES_ck_prix] CHECK ([prix] >= 0),
  CONSTRAINT [ARTICLES_ck_stockactuel] CHECK ([stockactuel] >= 0),
  CONSTRAINT [ARTICLES_ck_stockminimum] CHECK ([stockminimum] >= 0)
)
ON [PRIMARY]
GO

Creiamo alcuni articoli:

Image

2.4.3.2. La classe di test NUnit

La classe di test NUnit della classe di implementazione [ArticlesDaoSqlServer] è la stessa di quella della classe [ArticlesDaoPlainODBC] (cfr. paragrafo 2.3.3.2). Seguiamo una procedura analoga per preparare il test NUnit della classe:

  • creiamo nella cartella Visual Studio del progetto [dao-sqlserver] la cartella [tests] (a destra) copiando la cartella [tests] dal progetto [dao-odbc] (a sinistra):
  • nella cartella [tests] del progetto [dao-sqlserver], sostituiamo DLL [webarticles-dao.dll] con DLL [webarticles-dao.dll] generati dal progetto [dao-sqlserver]
  • modifichiamo il file di configurazione [spring-config.xml] per istanziare la nuova classe [ArticlesDaoSqlServer]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoSqlServer, webarticles-dao">
        <constructor-arg index="0">
            <value>portable1_tahe\msde140405</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>dbarticles</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>admarticles</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>mdparticles</value>
        </constructor-arg>
    </object>
</objects>

Commenti:

  • alla riga 7, l'oggetto [articlesdao] è ora associato a un'istanza della classe [ ArticlesDaoSqlServeur]
  • questa classe ha un costruttore a quattro argomenti:
  • il nome dell'istanza MSDE utilizzata - riga 9
  • il nome del database - riga 12
  • l'identità con cui si accederà al database - riga 15
  • la password associata a tale identità - riga 18

Qui riprendiamo le informazioni della sorgente MSDE che abbiamo creato in precedenza.

2.4.3.3. Test

Siamo pronti per i test. Utilizzando l’applicazione [Nunit-Gui], carichiamo il file DLL [test-webarticles-dao.dll] dalla cartella [tests] sopra indicata ed eseguiamo il test [testGetAllArticles]:

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] assegnato inizialmente alla classe di test e che è stato mantenuto poiché utilizziamo i file DLL e [tests-webarticles-dao.dll] derivati da questa classe, è proprio la classe [ArticlesDaoSqlserver] quella qui sottoposta a test. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che avevamo inserito nella tabella [ARTICLES]. Ora eseguiamo tutti i test:

Image

Nella finestra a sinistra è visibile l’elenco dei metodi testati. Il colore del punto che precede il nome di ciascun metodo indica il superamento (verde) o il fallimento (rosso) del metodo. Il lettore che visualizza questo documento sullo schermo potrà constatare che tutti i test sono stati superati.

2.4.4. Integrazione del nuovo livello [dao] nell’applicazione [webarticles]

Seguiamo la procedura illustrata al paragrafo 2.3.4. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • nella cartella [bin], il file DLL delvecchio livello [dao] viene sostituito dal DLL del nuovo livello [dao] implementato dalla classe [ArticlesDaoSqlServer]
  • in [runtime], il file di configurazione [web.config] viene sostituito da un file che tiene conto della nuova classe di implementazione:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
    <spring>
        <context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoSqlServer, webarticles-dao">
        <constructor-arg index="0">
            <value>portable1_tahe\msde140405</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>dbarticles</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>admarticles</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>mdparticles</value>
        </constructor-arg>
    </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

Commenti:

  • le righe 15-33 associano al singleton [articlesDao] un'istanza della nuova classe [ArticlesDaoSqlServer]. Questa è l'unica modifica. L'abbiamo già riscontrata durante i test del nuovo livello [dao]

Siamo pronti per i test. Manteniamo la stessa configurazione del server web [Cassini] di prima. Inizializziamo la tabella degli articoli [MSDE] con i seguenti valori:

Image

Assicurarsi che il server web Cassini e SGBD MSDE (in questo caso l’istanza portable1_tahe\msde140405) siano in esecuzione. Con un browser, accediamo a URL e [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES] nel database [MSDE]:

Image

Gli articoli [ballon foot] e [raquette tennis] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [rollers] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.4.5. La classe di implementazione [ArticlesDaoOleDb]

2.4.5.1. Gli i di origine dati OleDb

La terza implementazione del livello [dao] presuppone che i dati si trovino in un database accessibile tramite un driver OleDb. Il principio delle fonti OleDb è analogo a quello delle fonti ODBC. Un programma che utilizza una sorgente OleDb lo fa tramite un'interfaccia standard comune a tutte le sorgenti OleDb. Cambiare la sorgente OleDb equivale semplicemente a cambiare il driver OleDb. Il codice non viene modificato.

È possibile verificare quali driver OleDb sono disponibili sul proprio computer tramite Visual Studio:

  • visualizzare l’esploratore dei server tramite [Affichage/Explorateur de serveurs]:

Image

  • per aggiungere una nuova connessione, fare clic con il tasto destro su [Connexion de données] e selezionare l’opzione [Ajouter une connexion]. Si aprirà quindi una procedura guidata con cui è possibile definire le caratteristiche della connessione:

Image

  • Il pannello [Fournisseur] mostra l'elenco dei driver OLEDB disponibili. Per il nuovo livello [dao] utilizzeremo un driver [Microsoft Jet 4.0 OLE DB Provider] che consente l’accesso ai database ACCESS.
  • Usciamo momentaneamente da Visual Studio per creare la base ACCESS [articles.mdb] contenente la seguente tabella:

Image

  • La struttura della tabella è la seguente:
id
numerico - intero - chiave primaria
nom
testo - 20 caratteri -
prix
numerico - reale doppio
stockactuel
numerico - intero
stockminimum
numerico - intero
  • Torniamo a Visual Studio e creiamo una nuova connessione come spiegato in precedenza:

Image

  • Scegliamo il driver [Microsoft Jet 4.0] e passiamo al pannello [Connexion]:

Image

  • con il pulsante [1], selezioniamo il database ACCESS appena creato, quindi completiamo la definizione della connessione con il pulsante [Terminer]. La connessione creata appare ora nell’elenco delle connessioni disponibili:

Image

  • un doppio clic sulla tabella [ARTICLES] ci consente di accedere al suo contenuto:

Image

  • È quindi possibile aggiungere, modificare o eliminare righe nella tabella.
  • Selezionare nell’Explorer dei server la nuova connessione per accedere alla sua scheda delle proprietà:

Image

  • È utile conoscere la stringa di connessione. Ci servirà per collegarci al database:
Provider=Microsoft.Jet.OLEDB.4.0;User ID=Admin;Data Source=D:\data\serge\databases\access\articles\articles.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database="";Jet OLEDB:Registry Path="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False
  • Di questa stringa, terremo a mente solo i seguenti elementi:
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;

2.4.5.2. Il codice della classe [ArticlesDaoOleDb]

La classe [ArticlesDaoOleDb] è molto simile alla classe [ArticlesDaoPlainODBC] esaminata in precedenza. Indicheremo quindi solo le modifiche apportate rispetto alla versione precedente:

  • le classi necessarie si trovano nello spazio dei nomi [System.Data.OleDb] anziché nello spazio dei nomi [System.Data.Odbc]
  • la connessione di tipo [OdbcConnection] ha ora il tipo [OleDbConnection]
  • gli oggetti [OdbcCommand] hanno ora il tipo [OleDbCommand]

Il costruttore della classe accetta come unico parametro la stringa di connessione al database:

         ' costruttore
        Public Sub New(ByVal connectString As String)
             ' connectString: stringa di connessione alla fonte OleDb
             'si istanzia la connessione
            connexion = New OleDbConnection(connectString)
             ' si preparano le richieste SQL
...
        End Sub

Il codice completo della classe [ArticlesDaoOleDb] è il seguente:

Imports System
Imports System.Collections
Imports System.Data.OleDb

Namespace istia.st.articles.dao

    Public Class ArticlesDaoOleDb
        Implements istia.st.articles.dao.IArticlesDao

         ' campi privati
        Private connexion As OleDbConnection = Nothing
        Private insertCommand As OleDbCommand
        Private updatecommand As OleDbCommand
        Private deleteSomeCommand As OleDbCommand
        Private selectSomeCommand As OleDbCommand
        Private updateStockCommand As OleDbCommand
        Private deleteAllCommand As OleDbCommand
        Private selectAllCommand As OleDbCommand

         ' costruttore
        Public Sub New(ByVal connectString As String)
             ' connectString: stringa di connessione alla fonte OleDb
             'si istanzia la connessione
            connexion = New OleDbConnection(connectString)
             ' si preparano le richieste SQL
            insertCommand = New OleDbCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
            updatecommand = New OleDbCommand("update ARTICLES set nom=?, prix=?, stockactuel=?, stockminimum=? where id=?", connexion)
            deleteSomeCommand = New OleDbCommand("delete from ARTICLES where id=?", connexion)
            selectSomeCommand = New OleDbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=?", connexion)
            updateStockCommand = New OleDbCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)
            selectAllCommand = New OleDbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New OleDbCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la richiesta di inserimento
                With insertCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", unArticle.id))
                    .Add(New OleDbParameter("nom", unArticle.nom))
                    .Add(New OleDbParameter("prix", unArticle.prix))
                    .Add(New OleDbParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OleDbParameter("stockminimum", unArticle.stockminimum))
                End With
                Try
                     'si esegue la richiesta
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'errore nella query
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di aggiornamento delle scorte
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("mvt1", mouvement))
                    .Add(New OleDbParameter("id", idArticle))
                    .Add(New OleDbParameter("mvt2", mouvement))
                End With
                 'in esecuzione
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si esegue la richiesta di inserimento
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si sta eseguendo la query SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     ': viene restituito l'elenco
                    Return articles
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 'la si esegue
                Try
                     ': si esegue la query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'si verifica se l'articolo è stato trovato
                    If articles.Count = 0 Then Return Nothing
                     'si restituisce l'articolo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'errore nella richiesta
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di aggiornamento
                With updatecommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("nom", unArticle.nom))
                    .Add(New OleDbParameter("prix", unArticle.prix))
                    .Add(New OleDbParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OleDbParameter("stockminimum", unArticle.stockactuel))
                    .Add(New OleDbParameter("id", unArticle.id))
                End With
                 ' la si esegue
                Try
                     'si esegue la query di inserimento
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di cancellazione
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 'si esegue
                Try
                     'si esegue la query di cancellazione
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Private Function executeQuery(ByVal query As OleDbCommand) As IList
             ' esecuzione di una query SELECT 
             ' dichiarazione dell'oggetto che consente l'accesso a tutte le righe della tabella dei risultati
            Dim myReader As OleDbDataReader = Nothing
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la query
                myReader = query.ExecuteReader()
                 'si dichiara un elenco di articoli per restituirlo in seguito
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'si prepara un articolo con i valori del lettore
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     'si aggiunge l'articolo all'elenco
                    articles.Add(unArticle)
                End While
                 'si restituisce il risultato
                Return articles
            Finally
                 ' liberazione delle risorse
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

        Private Function executeUpdate(ByVal sqlCommand As OleDbCommand) As Integer
             ' esecuzione di una richiesta di aggiornamento
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la richiesta
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' liberazione delle risorse
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Si invita il lettore a interpretare questo codice alla luce dei commenti relativi alla classe [ArticlesDaoPlainODBC] forniti in precedenza.

2.4.5.3. Generazione dell'assembly del livello [dao]

Il nuovo progetto Visual Studio presenta la seguente struttura:

Image

Il progetto è configurato per generare un DLL denominato [webarticles-dao.dll]:

2.4.5.4. Test NUnit del livello [dao]

2.4.5.4.1. La classe di test NUnit

La classe di test NUnit della classe di implementazione [ArticlesDaoOleDb] è la stessa di quella della classe [ArticlesDaoPlainODBC] (cfr. paragrafo 2.3.3.2). Seguiamo una procedura analoga per preparare il test NUnit della classe:

  • creiamo nella cartella Visual Studio del progetto [dao-oledb] la cartella [tests] (a destra) copiando la cartella [tests] dal progetto [dao-odbc] (a sinistra):
  • nella cartella [tests] del progetto [dao-oledb] sostituiamo DLL [webarticles-dao.dll] con DLL [webarticles-dao.dll] derivante dalla generazione del progetto [dao-oledb]
  • modifichiamo il file di configurazione [spring-config.xml] per istanziare la nuova classe [ArticlesDaoOleDb]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoOleDb, webarticles-dao">
        <constructor-arg index="0">
            <value>Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;</value>
        </constructor-arg>
    </object>
</objects>

Commenti:

  • riga 7, l'oggetto [articlesdao] è ora associato a un'istanza della classe [ ArticlesDaoOleDb]
  • questa classe ha un costruttore con un argomento: la stringa di connessione al database OleDb ACCESS - riga 9
2.4.5.4.2. Tests

Siamo pronti per i test. Utilizzando l’applicazione [Nunit-Gui], carichiamo il file DLL [test-webarticles-dao.dll] dalla cartella [tests] sopra indicata ed eseguiamo il test [testGetAllArticles]:

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] assegnato inizialmente alla classe di test, è proprio la classe [ArticlesDaoOleDb] ad essere qui testata. La schermata mostra che abbiamo recuperato correttamente gli articoli che avevamo inserito nella tabella [ARTICLES]. Ora eseguiamo tutti i test:

Image

Il lettore che visualizza questo documento sullo schermo potrà vedere che tutti i test sono stati superati (colore verde).

2.4.5.5. Integrazione del nuovo livello [dao] nell’applicazione [webarticles]

Seguiamo la procedura illustrata al paragrafo 2.3.4. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • nella cartella [bin], il file DLL delvecchio livello [dao] viene sostituito dal file DLL del nuovo livello [dao] implementato dalla classe [ArticlesDaoOleDb]
  • in [runtime], il file di configurazione [web.config] viene sostituito da un file che tiene conto della nuova classe di implementazione:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
    <spring>
        <context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoOleDb, webarticles-dao">
                <constructor-arg index="0">
                    <value>Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;</value>
                </constructor-arg>
            </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

Commenti:

  • le righe 14-18 associano al singleton [articlesDao] un'istanza della nuova classe [ArticlesDaoOleDb]. Questa è l'unica modifica.

Manteniamo la stessa configurazione del server web [Cassini] di prima. Inizializziamo la tabella degli articoli con i seguenti valori:

Image

Assicurarsi che il database degli articoli non sia in uso da parte di un programma come Visual Studio o ACCESS. Tramite un browser richiediamo l'URL [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES] con ACCESS:

Image

Gli articoli [pantalon] e [jupe] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [manteau] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.5. La classe di implementazione [ArticlesDaoFirebirdProvider]

2.5.1. Il provider di accesso Firebird-net-provider

Abbiamo già utilizzato una fonte di dati [Firebird] tramite un driver ODBC. Sebbene garantiscano una grande riutilizzabilità del codice che li impiega, i driver ODBC sono tuttavia meno performanti rispetto ai driver scritti specificamente per il SGBD di destinazione. Il SGBD [Firebird] può essere utilizzato tramite una libreria di classi specifiche scaricabile dal sito di Firebird [http://firebird.sourceforge.net/]. La pagina dei download offre i seguenti link (aprile 2005):

Image

Il link [firebird-net-provider] è quello da utilizzare per scaricare le classi .NET di accesso a SGBD Firebird. L'installazione del pacchetto crea una cartella simile alla seguente:

Image

Ci interessano due elementi:

  • [FirebirdSql.Data.Firebird.dll]: l'assembly contenente le classi .NET per l'accesso a SGBD Firebird
  • [FirebirdNETProviderSDK.chm]: la documentazione relativa a queste classi

Successivamente, affinché un progetto Visual Studio possa utilizzare queste classi, faremo due cose:

  • inseriremo l’assembly [FirebirdSql.Data.Firebird.dll] nella cartella [bin] del progetto
  • aggiungeremo lo stesso assembly ai riferimenti del progetto

2.5.2. Il codice della classe [ArticlesDaoFirebirdProvider]

La classe [ArticlesDaoFirebirdProvider] è molto simile alla classe [ArticlesDaoSqlServer] esaminata in precedenza. Indicheremo quindi solo le modifiche apportate rispetto a quella versione:

  • le classi necessarie si trovano nello spazio dei nomi [FirebirdSql.Data.Firebird] anziché nello spazio dei nomi [System.Data.SqlClient]
  • la connessione di tipo [SqlConnection] ha ora il tipo [FbConnection]
  • gli oggetti [SqlCommand] hanno ora il tipo [FbCommand]
  • gli oggetti [SqlParameter] hanno ora il tipo [FbParameter]

Il costruttore della classe accetta quattro parametri, con i quali costruisce la stringa di connessione al database:

         ' costruttore
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' server: nome del computer host di SGBD
             ' databaseName: percorso di accesso al database
             ' uid: identificativo dell'utente che effettua l'accesso
             ' password: la sua password
...
        End Sub

Il codice completo della classe [ArticlesDaoFirebirdProvider] è il seguente:

Imports System
Imports System.Collections
Imports FirebirdSql.Data.Firebird

Namespace istia.st.articles.dao

    Public Class ArticlesDaoFirebirdProvider
        Implements istia.st.articles.dao.IArticlesDao

         ' campi privati
        Private connexion As FbConnection = Nothing
        Private databasePath As String
        Private insertCommand As FbCommand
        Private updatecommand As FbCommand
        Private deleteSomeCommand As FbCommand
        Private selectSomeCommand As FbCommand
        Private updateStockCommand As FbCommand
        Private deleteAllCommand As FbCommand
        Private selectAllCommand As FbCommand

         ' produttore
        Public Sub New(ByVal serveur As String, ByVal databasePath As String, ByVal uid As String, ByVal password As String)
             ' server: nome del computer host di SGBD Firebird
             ' databaseName: percorso di accesso al database da utilizzare
             ' uid: identificativo dell'utente che si connette al database
             ' password: la sua password

             'si recupera il nome del database passato come argomento
            Me.databasePath = databasePath
             'si instanzia la connessione
            Dim connectString As String = String.Format("DataSource={0};Database={1};User={2};Password={3}", serveur, databasePath, uid, password)
            connexion = New FbConnection(connectString)
             ' si preparano le query SQL
            insertCommand = New FbCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
            updatecommand = New FbCommand("update ARTICLES set nom=@nom, prix=@prix, stockactuel=@sa, stockminimum=@sm where id=@id", connexion)
            deleteSomeCommand = New FbCommand("delete from ARTICLES where id=@id", connexion)
            selectSomeCommand = New FbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=@id", connexion)
            updateStockCommand = New FbCommand("update ARTICLES set stockactuel=stockactuel+@mvt where id=@id and (stockactuel+@mvt)>=0", connexion)
            selectAllCommand = New FbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New FbCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di inserimento
                With insertCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", unArticle.id))
                    .Add(New FbParameter("@nom", unArticle.nom))
                    .Add(New FbParameter("@prix", unArticle.prix))
                    .Add(New FbParameter("@sa", unArticle.stockactuel))
                    .Add(New FbParameter("@sm", unArticle.stockminimum))
                End With
                Try
                     'si esegue
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'errore nella query
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la richiesta di aggiornamento delle scorte
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@mvt", mouvement))
                    .Add(New FbParameter("@id", idArticle))
                End With
                 'in esecuzione
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si esegue la richiesta di inserimento
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' sezione esclusiva
            SyncLock Me
                Try
                     'si sta eseguendo la query SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'viene restituito l'elenco
                    Return articles
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 ': viene eseguita
                Try
                     'si esegue la query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'si verifica se l'articolo è stato trovato
                    If articles.Count = 0 Then Return Nothing
                     'si restituisce l'articolo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di aggiornamento
                With updatecommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@nom", unArticle.nom))
                    .Add(New FbParameter("@prix", unArticle.prix))
                    .Add(New FbParameter("@sa", unArticle.stockactuel))
                    .Add(New FbParameter("@sm", unArticle.stockminimum))
                    .Add(New FbParameter("@id", unArticle.id))
                End With
                 ' la si esegue
                Try
                     'si esegue la query di inserimento
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' sezione esclusiva
            SyncLock Me
                 ' si prepara la query di cancellazione
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 'si esegue
                Try
                     'si esegue la query di cancellazione
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'errore di query
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Private Function executeQuery(ByVal query As FbCommand) As IList
             ' esecuzione di una query SELECT 
             ' dichiarazione dell'oggetto che consente l'accesso a tutte le righe della tabella dei risultati
            Dim myReader As FbDataReader = Nothing
            Try
                 'si crea una connessione a BDD
                connexion.Open()
                 'si esegue la query
                myReader = query.ExecuteReader()
                 'si dichiara un elenco di articoli per restituirlo in seguito
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'si prepara un articolo con i valori del lettore
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     'si aggiunge l'articolo all'elenco
                    articles.Add(unArticle)
                End While
                 'si restituisce il risultato
                Return articles
            Finally
                 ' liberazione delle risorse
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

        Private Function executeUpdate(ByVal updateCommand As FbCommand) As Integer
             ' esecuzione di una richiesta di aggiornamento
            Try
                 ': viene stabilita una connessione a BDD
                connexion.Open()
                 'si esegue la richiesta
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' liberazione delle risorse
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Si invita il lettore a interpretare questo codice alla luce dei commenti relativi alla classe [ArticlesDaoSqlServer] forniti in precedenza.

2.5.3. Generazione dell’assembly del livello [dao]

Il nuovo progetto Visual Studio presenta la seguente struttura:

Image

Si noti la presenza dell’assembly [FirebirdSql.Data.Firebird.dll] nei riferimenti del progetto. Questo DLL è stato inserito nella cartella [bin] del progetto. Il progetto è configurato per generare un DLL denominato [webarticles-dao.dll]:

2.5.4. Test Nunit del livello [dao]

2.5.4.1. La classe di test NUnit

La classe di test Nunit della classe di implementazione [ArticlesDaoFirebirdProvider] è la stessa di quella della classe [ArticlesDaoPlainODBC] (cfr. paragrafo 2.3.3.2). Seguiamo una procedura analoga per preparare il test Nunit della classe [ArticlesDaoFirebirdProvider]:

  • nella cartella Visual Studio del progetto [dao-firebird-provider] creiamo la cartella [tests] (a destra) copiando la cartella [bin] dal progetto di test del livello [dao-odbc] (a sinistra):
  • nella cartella [tests] sostituiamo DLL e [webarticles-dao.dll] con DLL e [webarticles-dao.dll] generati dal progetto [dao-firebird-provider]
  • modifichiamo il file di configurazione [spring-config.xml] per istanziare la nuova classe [ArticlesDaoFirebirdProvider]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoFirebirdProvider, webarticles-dao">
        <constructor-arg index="0">
            <value>localhost</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>D:\data\serge\databases\firebird\dbarticles2.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>sysdba</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>masterkey</value>
        </constructor-arg>
    </object>
</objects>

Commenti:

  • alla riga 7, l'oggetto [articlesdao] è ora associato a un'istanza della classe [ArticlesDaoFirebirdProvider]
  • questa classe ha un costruttore a quattro argomenti
  • il computer host di SGBD - riga 9
  • il percorso di accesso al database Firebird - riga 12
  • il nome utente di chi effettua l'accesso - riga 15
  • la sua password - riga 18

2.5.4.2. Test

La tabella [ARTICLES] della fonte dati è popolata con i seguenti articoli (utilizzare IBExpert):

Image

Siamo pronti per i test. Utilizzando l'applicazione [Nunit-Gui], carichiamo il file DLL e [test-webarticles-dao.dll] dalla cartella [tests] sopra indicata ed eseguiamo il test [testGetAllArticles]:

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] assegnato inizialmente alla classe di test, è proprio la classe [ArticlesDaoFirebirdProvider] ad essere qui testata. La schermata mostra che abbiamo recuperato correttamente gli articoli che avevamo inserito nella tabella [ARTICLES]. Ora eseguiamo tutti i test:

Image

Il lettore che visualizza questo documento sullo schermo potrà vedere che tutti i test hanno avuto esito positivo (colore verde). Ciò che non può vedere è che i test si sono svolti in modo nettamente più rapido rispetto a quando si accedeva al database degli articoli tramite un driver ODBC della nostra prima implementazione.

2.5.5. Integrazione del nuovo livello [dao] nell’applicazione [webarticles]

Seguiamo la procedura già spiegata due volte, in particolare al paragrafo 2.3.4. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • nella cartella [bin], il file DLL delvecchio livello [dao] viene sostituito dal DLL del nuovo livello [dao] implementato dalla classe [ArticlesDaoFirebirdProvider]. Inseriamo inoltre la DLL necessaria per Firebird [FirebirdSql.Data.Firebird.dll]:

Image

  • in [runtime], il file di configurazione [web.config] viene sostituito da un file che tiene conto della nuova classe di implementazione:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
    <spring>
        <context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoFirebirdProvider, webarticles-dao">
                <constructor-arg index="0">
                    <value>localhost</value>
                </constructor-arg>
                <constructor-arg index="1">
                    <value>D:\data\serge\databases\firebird\dbarticles2.gdb</value>
                </constructor-arg>
                <constructor-arg index="2">
                    <value>sysdba</value>
                </constructor-arg>
                <constructor-arg index="3">
                    <value>masterkey</value>
                </constructor-arg>
            </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

Commenti:

  • le righe 14-27 associano al singleton [articlesDao] un'istanza della nuova classe [ArticlesDaoFirebirdProvider]. Questa è l'unica modifica.

Siamo pronti per i test. Configuriamo il server web [Cassini] come nei test precedenti. Inizializziamo la tabella degli articoli con i seguenti valori:

Image

Con un browser richiediamo l’URL [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES]:

Image

Gli articoli [crayon bille] e [ramette 50 feuilles] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [stylo plume] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.5.6. La classe di implementazione [ArticlesDaoSqlMap]

2.5.6.1. Il prodotto Ibatis SqlMap

Abbiamo scritto quattro diverse implementazioni del livello [dao] della nostra applicazione [webarticles]. In ogni caso siamo riusciti a integrare il nuovo livello [dao] nell’applicazione [webarticles] senza ricompilare gli altri due livelli [web] e [domain]. Ciò è stato ottenuto, ricordiamo, grazie a due scelte architetturali:

  • l’accesso ai livelli tramite interfacce
  • l’integrazione dei livelli tramite Spring

Vorremmo spingerci un po’ oltre. Sebbene diverse, le nostre quattro implementazioni del livello [dao] presentano somiglianze sorprendenti. Una volta scritta la prima implementazione, le altre tre sono state ottenute praticamente tramite copia-incolla e sostituzione di alcune parole chiave con altre. La logica, invece, non è stata modificata. Ci si può chiedere se non sia possibile avere un’implementazione che ci liberi dalle diverse modalità di accesso ai dati. Ne abbiamo utilizzate quattro:

  • accesso tramite un driver ODBC a una fonte di dati ODBC
  • accesso diretto a un database SQL Server
  • accesso tramite un driver Ole Db a una fonte dati Ole Db
  • accesso diretto a un database Firebird

Lo strumento Ibatis SqlMap [[http://www.ibatis.com/] consente lo sviluppo di livelli di accesso ai dati indipendenti dalla natura effettiva della fonte di dati. L’accesso ai dati è garantito tramite:

  • file di configurazione in cui sono inserite le informazioni che definiscono la fonte di dati e le operazioni che si desidera eseguire su di essa
  • una libreria di classi che si basano su queste informazioni per accedere ai dati

Lo strumento Ibatis SqlMap è stato inizialmente sviluppato per la piattaforma Java. Il suo porting sulla piattaforma .NET è recente e, a quanto pare, presenta alcuni bug (opinione personale che richiederebbe una verifica approfondita). Tuttavia, dato che lo strumento ha dato prova di efficacia sulla piattaforma Java, sembra interessante presentarne la versione .NET.

2.5.6.2. Dove trovare IBATIS SqlMap?

Il sito principale di Firebird è [http://www.ibatis.com/]. La pagina dei download offre i seguenti link:

Image

Selezioniamo il link [Stable Binaries] che ci reindirizza a [SourceForge.net]. Seguiamo la procedura di download fino alla fine. Otterremo un file zip contenente i seguenti file:

Image

In un progetto Visual Studio che utilizza Ibatis SqlMap, è necessario eseguire due operazioni:

  • inserire i file sopra indicati nella cartella [bin] del progetto
  • aggiungere al progetto un riferimento a ciascuno di questi file

2.5.6.3. I file di configurazione di Ibatis SqlMap

Una fonte di dati [SqlMap] verrà definita tramite i seguenti file di configurazione:

  1. providers.config: definisce le librerie di classi da utilizzare per accedere ai dati
  2. sqlmap.config: definisce le caratteristiche della connessione da stabilire
  3. file di mappatura: definiscono le operazioni da eseguire sui dati

La logica di questi file è la seguente:

  • per accedere ai dati, avremo bisogno di una connessione. Per rappresentarla, abbiamo già incontrato diverse classi: OdbcConnection, SqlConnection, OleDbConnection, FbConnection. Ci servirà inoltre un oggetto [Command] per inviare le richieste SQL: OdbcCommand, SqlCommand, OleDbCommand, FbCommand. E così via. Nel file [providers.config] definiamo l'insieme delle classi di cui abbiamo bisogno.
  • Il file [sqlmap.config] definisce essenzialmente la stringa di connessione al database che contiene i dati. La connessione al database verrà aperta tramite l'istanziazione della classe [Connection] definita in [providers.config], al cui costruttore verrà passata la stringa di connessione definita in [sqlmap.config].
  • I file di mappatura definiscono:
    • le associazioni tra le righe delle tabelle di dati e la classe .NET, le cui istanze conterranno tali righe
    • le operazioni SQL da eseguire. Queste sono identificate da un nome. Il codice .NET esegue queste operazioni tramite il loro nome, il che comporta l’eliminazione di tutto il codice SQL dal codice .NET.

2.5.6.4. I file di configurazione del progetto [dao-sqlmap]

Esaminiamo, con un esempio, la natura esatta dei file di configurazione di SqlMap. Consideriamo il caso in cui la fonte dei dati sia la fonte Firebird ODBC del paragrafo 2.3.3.1.

2.5.6.4.1. providers.config

Il file [providers.config] relativo alla fonte ODBC è il seguente:

<?xml version="1.0" encoding="utf-8" ?> 

<providers>
    <clear/>
    <provider 
        name="Odbc1.1" 
        enabled="true" 
        assemblyName="System.Data, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
        connectionClass="System.Data.Odbc.OdbcConnection" 
        commandClass="System.Data.Odbc.OdbcCommand" 
        parameterClass="System.Data.Odbc.OdbcParameter" 
        parameterDbTypeClass="System.Data.Odbc.OdbcType" 
        parameterDbTypeProperty="OdbcType" 
        dataAdapterClass="System.Data.Odbc.OdbcDataAdapter" 
        commandBuilderClass="System.Data.Odbc.OdbcCommandBuilder" 
        usePositionalParameters = "true"
        useParameterPrefixInSql = "false"
        useParameterPrefixInParameter = "false"
        parameterPrefix = "@"
    />
</providers>

Note:

  • un file [providers.config] viene distribuito insieme al pacchetto [SqlMap]. Esso propone diversi provider standard. Il codice sopra riportato proviene direttamente da questo file.
  • un <provider> ha un nome - riga 6 - che può essere qualsiasi
  • un <provider> può essere attivato ([enabled=true]) o meno ([enabled=false]). Se è attivato, il DLL a cui si fa riferimento alla riga 8 deve essere accessibile. Un file [providers.config] può contenere più tag <provider>.
  • riga 8 - nome dell'assembly che contiene le classi definite righe 9-15
  • riga 9 - classe da utilizzare per creare una connessione
  • riga 10 - classe da utilizzare per creare un oggetto [Command] per l’invio di comandi SQL
  • riga 11 - classe da utilizzare per gestire i parametri di un comando SQL configurato
  • riga 12 - classe di enumerazione dei possibili tipi di dati per i campi di una tabella
  • riga 13 - nome della proprietà di un oggetto [Parameter] che contiene il tipo del valore di tale parametro
  • riga 14 - nome della classe [Adapter] che consente di creare oggetti [DataSet] a partire dalla fonte dati
  • riga 15 - nome della classe [CommandBuilder] che, associata a un oggetto [Adapter], consente di generare automaticamente le proprietà [InsertCommand, DeleteCommand, UpdateCommand] di quest'ultimo a partire dalla sua proprietà [SelectCommand]
  • righe 16 - 19 - si definisce come vengono gestiti i comandi SQL configurati. A seconda dei casi, occorre scrivere ad esempio:
insert into ARTICLES(id,nom,prix,stockactuel,stockminimum) values (?,?,?,?,?)

oppure

insert into ARTICLES(id,nom,prix,stockactuel,stockminimum) values (@id,@nom,@prix,@sa,@sm)

Nel primo caso si tratta di parametri posizionali formali. I valori effettivi di questi devono essere forniti nell'ordine dei parametri formali. Nel secondo caso si tratta di parametri denominati. Si assegna un valore a un parametro di questo tipo specificandone il nome. L'ordine non ha più importanza.

  • riga 16 - si indica che le sorgenti ODBC utilizzano parametri posizionali
  • righe 17-19 - riguardano i parametri denominati. In questo caso non ce ne sono.

Queste informazioni consentono a SqlMap di sapere, ad esempio, quale classe deve istanziare per creare una connessione. In questo caso sarà la classe [OdbcConnection] (riga 9).

2.5.6.4.2. sqlmap.config

Il file [providers.config] definisce le classi da utilizzare per accedere a una fonte ODBC. Non indica alcuna fonte ODBC. È il file [sqlmap.config] a farlo:

<?xml version="1.0" encoding="utf-8" ?>
<sqlMapConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="Schemas\SqlMapConfig.xsd">
    <properties resource="properties.xml"/>
    <settings>
        <setting useStatementNamespaces="false" />
        <setting cacheModelsEnabled="false" />
    </settings>
     <!-- ==== fonte dati ========= -->    
    <database>
        <provider name="${provider}"/>
        <dataSource name="sqlmaparticles"  connectionString="${connectionString}"/>
        <transactionManager type="ADO/SWC" />
    </database>
    <sqlMaps>
        <sqlMap resource="articles.xml" />
    </sqlMaps>
</sqlMapConfig>

Commenti:

  • riga 3 - si definisce un file di proprietà [properties.xml]. Questo definisce coppie (chiave, valore). Le chiavi possono essere qualsiasi cosa. Il valore associato a una chiave C si ottiene tramite la notazione ${C} nel file [sqlmap.config]. Ecco il file [properties.xml] che sarà associato al precedente file [sqlmap.config]:
1
2
3
4
5
<?xml version="1.0" encoding="utf-8" ?>
<settings>
    <add key="provider" value="Odbc1.1" />
    <add key="connectionString" value="DSN=odbc-firebird-articles;UID=SYSDBA;PASSWORD=masterkey" />
</settings>

riga 3 - viene definita la chiave [provider]. Il suo valore è il nome del tag <provider> da utilizzare in [providers.config]

riga 4 - è definita la chiave [connectionString]. Il suo valore è la stringa di connessione da utilizzare per aprire una connessione alla fonte dati ODBC Firebird.

  • righe 4-7 - parametri di configurazione:
    • riga 5 - le query SQL saranno identificate da un nome che a sua volta può far parte di uno spazio dei nomi. [useStatementNamespaces="false"] indica che non verranno utilizzati spazi dei nomi.
    • riga 6 - SqlMap dispone di diverse strategie di cache per ridurre al minimo gli accessi alla fonte di dati. [cacheModelsEnabled="false"] indica che non ne verrà utilizzata alcuna.
  • righe 9-13 - si definiscono le caratteristiche della fonte dati:
    • riga 10 - nome del <provider> di [providers.config] da utilizzare
    • riga 11 - stringa di connessione alla fonte dati
    • riga 12 - gestore delle transazioni. In questo caso non lo abbiamo utilizzato, ma abbiamo comunque lasciato la riga poiché era presente nel file di distribuzione standard.
  • righe 14-16 - elenco dei file che definiscono le operazioni SQL da eseguire sulla fonte dati.
  • riga 15 - definisce il file di mappatura [articles.xml]
2.5.6.4.3. articles.xml

Questo file svolge due funzioni:

  • definire una mappatura degli oggetti delle tabelle della fonte dati. Nei casi più semplici, ciò equivale ad associare una classe a una riga di una tabella.
  • definire operazioni SQL configurate e assegnare loro un nome.

Utilizzeremo il seguente file [articles.xml]:

<?xml version="1.0" encoding="iso-8859-1" ?>
<sqlMap namespace="Articles" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="SqlMap.xsd">
     <!-- i resultMap -->
    <resultMaps>
        <resultMap id="article" class="istia.st.articles.dao.Article">
            <result property="id" column="ID" />
            <result property="nom" column="NOM" />
            <result property="prix" column="PRIX" />
            <result property="stockactuel" column="STOCKACTUEL" />
            <result property="stockminimum" column="STOCKMINIMUM" />
        </resultMap>
    </resultMaps>
     <!-- le query SQL -->
    <statements>
         <!-- recupero di tutti gli articoli -->
        <select id="getAllArticles" resultMap="article">
                select ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM FROM ARTICLES
            </select> 
         <!-- eliminazione di tutti gli articoli-->
        <delete id="clearAllArticles">
                delete from ARTICLES
            </delete> 
         <!-- inserimento di un articolo -->
        <insert id="insertArticle" parameterClass="istia.st.articles.dao.Article">
                insert into ARTICLES (id, nom, prix,stockactuel, stockminimum) values
                ( #(id#, #nome#, #prezzo#, #stockattuale#, #stockminimo#)
            </insert>
         <!-- eliminazione di un articolo -->
        <delete id="deleteArticle" parameterClass="int">
                delete FROM ARTICLES where ID= #valore#
            </delete>
         <!-- modifica di un articolo -->
        <update id="modifyArticle" parameterClass="istia.st.articles.dao.Article">
                update ARTICLES set NOM= #nome# ,PRIX= #prezzo# ,STOCKACTUEL= #stockattuale# ,STOCKMINIMUM= #stockminimo# dove ID= #id#
            </update>
         <!-- ricerca di un articolo specifico -->
        <select id="getArticleById" resultMap="article" parameterClass="int">
                select ID, NOM, PRIX,    STOCKACTUEL, STOCKMINIMUM FROM ARTICLES where ID= #valore#
            </select>
         <!-- modifica delle scorte di un articolo -->
        <update id="changerStockArticle" parameterClass="Hashtable">
                update ARTICLES set STOCKACTUEL=(STOCKACTUEL + #movimento#) dove ID=#id# e ((STOCKACTUEL + #movimento#) >=0)
      </update>
    </statements>
</sqlMap>

Commenti:

  • righe 4-11: si definisce una mappatura tra una riga della tabella [ARTICLES] della fonte dati e la classe [istia.st.articles.dao.Article]. A ogni colonna (column) della tabella è associata una proprietà (property) della classe [Article]. Questa mappatura consente a [SqlMap] di costruire il risultato di un'operazione SQL SELECT. Ogni riga risultante da SELECT verrà inserita in un oggetto [Article] secondo le regole della mappatura.
  • riga 5 - il mapping è racchiuso in un tag <resultMap> ed è denominato tramite l'attributo [id="article"]. La classe associata è indicata dall'attributo [class="istia.st.articles.dao.Article"].
  • righe 14-44 - si definiscono le operazioni SQL necessarie
  • righe 16-18 - si definisce un'operazione SELECT denominata [getAllArticles]
    • riga 16 - l’operazione SELECT viene denominata [name= "getAllArticles "] e il mapping da utilizzare è definito dall’attributo [resultMap="article"]. Si fa quindi riferimento al mapping delle righe 5-11
    • riga 17 - testo del comando SQL da eseguire
  • righe 20-22 - si definisce il comando SQL-Delete [clearAllArticles] destinato a svuotare la tabella degli articoli.
  • righe 24-27 - si definisce il comando SQL-Insert [insertArticle] destinato ad aggiungere un nuovo articolo nella tabella degli articoli. Si tratta di una query parametrizzata dagli elementi (#id#, #nome#, #prezzo#, #stockattuale#, #stockminimo#). I valori di questi cinque elementi proverranno da un oggetto [Article] passato come parametro: [parameterClass="istia.st.articles.dao.Article"]. L’oggetto parametro deve avere le proprietà (id, nome, prezzo, stockattuale, stockminimo) a cui fa riferimento il comando SQL configurato.
  • righe 29-31 - si definisce il comando SQL Delete [deleteArticle] destinato a eliminare un articolo di cui si conosce il numero #value#. Questo numero verrà passato come parametro: [parameterClass="int"]. Si tratta di una regola generale. Quando il parametro è unico, viene indicato dalla parola chiave #value# nel testo del comando SQL.
  • righe 33-35 - si definisce il comando SQL-Update [modifyArticle] destinato a modificare un articolo di cui si conosce il numero. Come per il comando [insertArticle], le cinque informazioni necessarie proverranno dalle proprietà di un oggetto [istia.st.articles.dao.Article].
  • righe 37-39 - si definisce il comando SQL-Select [getArticleById] che consente di ottenere la riga di un articolo di cui si conosce il numero.
  • righe 41-43 - si definisce il comando SQL-Update [changerStockArticle] che modifica il campo [stockactuel] di un articolo di cui si conosce il numero. Le due informazioni necessarie, il n. #id# dell’articolo e l’incremento #mouvement# delle scorte, si trovano in un dizionario: [parameterClass="Hashtable"]. Questo dovrà avere due chiavi: id e mouvement. Saranno i valori associati a queste due chiavi ad essere utilizzati nel comando SQL.
2.5.6.4.4. Posizione dei file di configurazione

Esamineremo due situazioni diverse:

  • nel caso di un test Nunit, i file di configurazione di [SqlMap] saranno collocati nella stessa cartella dei binari sottoposti a test.
  • nel caso di un’applicazione web, saranno collocati nella directory principale dell’applicazione.

2.5.6.5. Il API di SqlMap

Le classi di SqlMap sono contenute in DLL, che in genere viene collocato nella cartella [bin] dell’applicazione:

Image

Le applicazioni che utilizzano le classi di SqlMap devono importare lo spazio dei nomi [IBatisNet.DataMapper]:

Imports IBatisNet.DataMapper

Tutte le operazioni SQL vengono eseguite tramite un singleton di tipo [Mapper], una classe dello spazio dei nomi [IBatisNet.DataMapper ]. Il singleton viene ottenuto nel modo seguente:

        Dim mappeur As SqlMapper = Mapper.Instance

Per eseguire il comando SqlMap [getAllArticles], si scriverà:

                    dim articles as IList=mappeur.QueryForList("getAllArticles", Nothing)
  • il metodo [QueryForList] consente di ottenere il risultato di un comando SELECT in un elenco
  • il primo parametro è il nome del comando SQL da eseguire (cfr. articles.xml)
  • il secondo parametro è il parametro da trasmettere alla richiesta SQL. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], si ha [parameterClass=Nothing]. Pertanto, qui si passa un puntatore nullo.
  • Il risultato è di tipo IList. Gli oggetti di questo elenco sono indicati dall'attributo [resultMap] del comando SQL-select: [resultMap="article"]. «articolo» è un nome di mappatura:
<resultMap id="article" class="istia.st.articles.dao.Article">

La classe associata a questo mapping è [istia.st.articles.dao.Article]. In definitiva, la variabile [articles] definita in precedenza è un elenco di oggetti [ istia.st.articles.dao.Article]. Abbiamo quindi ottenuto l'intera tabella [ARTICLES] con un'unica istruzione. Se la tabella [ARTICLES] è vuota, si ottiene un oggetto [IList] con 0 elementi.

Per eseguire il comando SqlMap [getArticleById], si scriverà:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • il metodo [QueryForObject] consente di ottenere il risultato di un comando SELECT che restituisce una sola riga
  • il primo parametro è il nome del comando SqlMap da eseguire
  • il secondo parametro è il parametro da trasmettere alla richiesta SQL. Deve corrispondere all’attributo [parameterClass] del comando SqlMap. In [articles.xml], si ha [parameterClass="int"]. Pertanto, qui si passa un numero intero che rappresenta il numero dell'articolo cercato.
  • Il risultato è di tipo Object. Se il comando SELECT non ha restituito alcuna riga, il risultato è un puntatore nullo (nothing).

Per eseguire il comando SqlMap [insertArticle], si scriverà:

                    mappeur.Insert("insertArticle", unArticle)
  • il metodo [Insert] consente di eseguire i comandi SQL e INSERT
  • il primo parametro è il nome del comando SqlMap da eseguire
  • il secondo parametro è il parametro da trasmettere a quest'ultimo. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], si ha [parameterClass="istia.st.articles.dao.Article"]. Pertanto, qui si passa un oggetto di tipo [istia.st.articles.dao.Article].

Per eseguire il comando SqlMap [deleteArticle], si scriverà:

                    dim nbArticles as Integer=mappeur.Delete("deleteArticle", idArticle)
  • il metodo [Delete] consente di eseguire i comandi SQL DELETE
  • il primo parametro è il nome del comando SQL da eseguire
  • il secondo parametro è il parametro da trasmettere a quest'ultimo. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], si ha [parameterClass="int"]. Pertanto, qui si passa il numero dell'articolo da eliminare.
  • Il risultato del metodo [Delete] è il numero di righe eliminate

Analogamente, per eseguire il comando SqlMap [clearAllArticles], si scriverà:

                    dim nbArticles as Integer=mappeur.Delete("clearAllArticles", nothing)

Per eseguire il comando SqlMap [modifyArticle], si digiterà:

                    dim nbArticles as Integer=mappeur.Update("modifyArticle", unArticle)
  • il metodo [Update] consente di eseguire i comandi SQL e UPDATE
  • il primo parametro è il nome del comando SqlMap da eseguire
  • il secondo parametro è il parametro da trasmettere a quest'ultimo. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], si ha [parameterClass="istia.st.articles.dao.Article"]. Pertanto, qui si passa un oggetto di tipo [istia.st.articles.dao.Article].
  • Il risultato del metodo [Update] è il numero di righe modificate.

Analogamente, per eseguire il comando SqlMap [changerStockArticle], si scriverà:

                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' aggiornamento
                    dim nbLignes as Integer= mappeur.Update("changerStockArticle", paramètres)
  • il secondo parametro corrisponde all'attributo [parameterClass] del comando SqlMap. In [articles.xml], si ha [parameterClass="Hashtable"]. Il comando SQL, configurato con [changerStockArticle], utilizza i parametri di [id, mouvement]. Pertanto, in questo caso si passa un dizionario contenente queste due chiavi.

2.5.6.6. Il codice della classe [ArticlesDaoSqlMap]

Alla luce delle spiegazioni precedenti, siamo ora in grado di scrivere la seguente nuova classe di implementazione [ArticlesDaoSqlMap]:

Option Explicit On 
Option Strict On

Imports System
Imports IBatisNet.DataMapper
Imports System.Collections

Namespace istia.st.articles.dao

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

         ' campi privati
        Dim mappeur As SqlMapper = Mapper.Instance

         ' elenco di tutti gli articoli
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
            SyncLock Me
                Try
                    Return mappeur.QueryForList("getAllArticles", Nothing)
                Catch ex As Exception
                    Throw New Exception("Echec de l'obtention de tous les articles : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' aggiunta di un articolo
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            SyncLock Me
                Try
                     ' unArticle: articolo da aggiungere
                     ' inserimento
                    mappeur.Insert("insertArticle", unArticle)
                    Return 1
                Catch ex As Exception
                    Throw New Exception("Echec de l'ajout de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' elimina un articolo
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id: ID dell'articolo da eliminare
                     ' eliminazione
                    Return mappeur.Delete("deleteArticle", idArticle)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la suppression de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' modifica un articolo
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' aggiornamento
                    Return mappeur.Update("modifyArticle", unArticle)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la mise à jour de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' ricerca di un articolo
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            SyncLock Me
                Try
                     ' id: ID dell'articolo cercato
                    Return CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la recherche de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' eliminazione di tutti gli articoli
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
            SyncLock Me
                Try
                    mappeur.Delete("clearAllArticles", Nothing)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de l'effacement de la table des articles : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Sub

         ' modifica delle giacenze di un articolo
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' ID: ID dell'articolo di cui si modifica la giacenza
                     ' movimento: movimento di magazzino
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' aggiornamento
                    Return mappeur.Update("changerStockArticle", paramètres)
                Catch ex As Exception
                    Throw New Exception(String.Format("Erreur lors du changement de stock [{0},{1}] : {2}", idArticle, mouvement, ex.ToString))
                End Try
            End SyncLock
        End Function
    End Class
End Namespace

Si invita il lettore a leggere questo codice alla luce delle spiegazioni fornite per il codice API e SqlMap. È interessante notare che l'utilizzo di [SqlMap] ha ridotto notevolmente la quantità di codice da scrivere.

2.5.6.7. Generazione dell'assembly del livello [dao]

Il nuovo progetto Visual Studio presenta la seguente struttura:

Image

Si noti la presenza degli “assembly” necessari a SqlMap nei riferimenti del progetto. Questi DLL sono stati collocati nella cartella [bin] del progetto. Il progetto è configurato per generare un DLL denominato [webarticles-dao.dll]:

2.5.6.8. Test Nunit del livello [dao]

2.5.6.8.1. La classe di test NUnit

La classe di test Nunit della classe di implementazione [ArticlesDaoSqlMap] è la stessa di quella della classe [ArticlesDaoPlainODBC] (cfr. paragrafo 2.3.3.2). Seguiamo una procedura analoga per preparare il test Nunit della classe [ArticlesDaoSqlMap]:

  • nella cartella Visual Studio del progetto [dao-sqlmap] creiamo la cartella [test1] (a destra) copiando la cartella [tests] dal progetto [dao-odbc] (a sinistra):
  • nella cartella [tests] sostituiamo DLL e [webarticles-dao.dll] con DLL e [webarticles-dao.dll] generati dal progetto [dao-sqlmap].
  • Aggiungiamo i file DLL necessari per SqlMap, nonché i file di configurazione analizzati [providers.config, sqlmap.config, properties.xml, articles.xml].
  • Modifichiamo il file di configurazione [spring-config.xml] per istanziare la nuova classe [ArticlesDaoSqlMap]:
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoSqlMap, webarticles-dao"/>
</objects>

Commenti:

  • alla riga 7, l'oggetto [articlesdao] è ora associato a un'istanza della classe [ArticlesDaoSqlMap]
  • questa classe non ha un costruttore. Verrà utilizzato il costruttore predefinito.
2.5.6.8.2. Tests

La tabella [ARTICLES] della fonte dati Firebird è popolata con i seguenti elementi:

Image

Siamo pronti per i test. Utilizzando l'applicazione [Nunit-Gui], carichiamo DLL e [test-webarticles-dao.dll] dalla cartella [test1] sopra indicata ed eseguiamo il test [testGetAllArticles]:

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] assegnato inizialmente alla classe di test, è proprio la classe [ArticlesDaoSqlMap] ad essere qui testata. La schermata mostra che abbiamo recuperato correttamente gli articoli che avevamo inserito nella tabella [ARTICLES]. Ora eseguiamo tutti i test:

Image

Il lettore che visualizza questo documento sullo schermo potrà notare che alcuni test hanno avuto esito positivo (colore verde), mentre altri sono falliti (colore rosso). I test che non sono andati a buon fine sono i test [testArticleAbsent] e [testChangerStockArticle]. Dopo lunghe ricerche, sembra che le cause di questi errori siano le seguenti:

  • nel test [testArticleAbsent], viene richiesto di modificare un articolo che non esiste. A tal fine si utilizza il metodo [modifieArticle], che restituisce il numero di righe modificate, ovvero 0 o 1. In questo caso, il valore dovrebbe essere 0. Invece, si verifica un'eccezione di tipo [IBatisNet.Common.Exceptions.ConcurrentException].
  • In [changerStockArticle] si verifica nuovamente un'operazione di tipo [update]. Si tratta di ridurre una giacenza di una quantità superiore alla giacenza stessa. A tal fine si utilizza il metodo [changerStockArticle], che restituisce il numero di righe modificate, ovvero 0 o 1. Il comando SQL è stato scritto per evitare un aggiornamento (cfr. comando SQL "changerStockArticle" in articles.xml) che renderebbe lo stock negativo. In questo caso ci si aspetta di ottenere 0 come risultato del metodo [changerStockArticle]. Ancora una volta, si verifica un'eccezione di tipo [IBatisNet.Common.Exceptions.ConcurrentException].

Le possibili cause di errore sono numerose:

  1. il codice della classe [ArticlesDaoSqlMap] è errato. È possibile. Tuttavia, deriva dal porting di una classe Java che aveva funzionato correttamente con la versione Java di SqlMap.
  2. la versione .NET di SqlMap presenta un bug
  3. il driver ODBC di Firebird presenta un bug
  4. ...

In mancanza di certezze, aggireremo l’ostacolo intercettando la famosa eccezione [IBatisNet.Common.Exceptions.ConcurrentException]. Il nuovo codice della classe [ArticlesDaoSqlMap] diventa il seguente:

....
Namespace istia.st.articles.dao

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

         ' campi privati
        Dim mappeur As SqlMapper = Mapper.Instance

         ' elenco di tutti gli articoli
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
...
        End Function

         ' aggiunta di un articolo
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

         ' elimina un articolo
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id: ID dell'articolo da eliminare
                     ' eliminazione
                    Return mappeur.Delete("deleteArticle", idArticle)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception("Erreur lors de la suppression de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' modifica di un articolo
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' aggiornamento
                    Return mappeur.Update("modifyArticle", unArticle)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception("Erreur lors de la mise à jour de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' ricerca di un articolo
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
...
        End Function

         ' eliminazione di tutti gli articoli
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
....
        End Sub

         ' modifica delle scorte di un articolo
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id: ID dell'articolo di cui si modifica la giacenza
                     ' movimento: movimento di magazzino
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' aggiornamento
                    Return mappeur.Update("changerStockArticle", paramètres)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception(String.Format("Erreur lors du changement de stock [{0},{1}] : {2}", idArticle, mouvement, ex.ToString))
                End Try
            End SyncLock
        End Function
    End Class
End Namespace

Le modifiche si trovano alle righe: 28, 41, 69. Per le operazioni SQL di tipo [UPDATE, DELETE], se si verifica un'eccezione di tipo [IBatisNet.Common.Exceptions.ConcurrentException], si restituisce 0 come risultato, indicando così che nessuna riga è stata modificata o eliminata. A questo punto, il DLL del progetto viene rigenerato, inserito nella cartella [test1] e i test NUnit vengono riavviati:

Image

Questa volta è quella buona. D'ora in poi lavoreremo con questo DLL.

2.5.6.9. Integrazione del nuovo livello [dao] nell'applicazione [webarticles]

2.5.6.9.1. fonte dati ODBC

Qui testiamo la fonte dati ODBC analizzata nel paragrafo 2.3.3.1. In questo caso viene utilizzata tramite SqlMap.

Seguiamo la procedura descritta nel paragrafo 2.3.4. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • nella cartella [bin], la DLL delvecchio livello [dao] viene sostituito dal DLL del nuovo livello [dao] implementato dalla classe [ArticlesDaoSqlMap]. Aggiungiamo i DLL necessari per Firebird e SqlMap:

Image

  • in [runtime], inseriamo i file di configurazione di SqlMap e [providers.config, sqlmap.config, properties.xml, articles.xml]:

Image

  • in [runtime], il file di configurazione [web.config] viene sostituito da un file che tiene conto della nuova classe di implementazione:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
    <spring>
        <context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoSqlMap, webarticles-dao"/>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

Commenti:

  • alla riga 14 si associa al singleton [articlesDao] un'istanza della nuova classe [ArticlesDaoSqlMap]. Questa è l'unica modifica.

Siamo pronti per i test. Configuriamo il server web [Cassini] come nei test precedenti. Inizializziamo la tabella degli articoli con i seguenti valori:

Image

Con un browser richiediamo l’URL [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES]:

Image

Gli articoli [couteau] e [cuiller] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [fourchette] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.5.6.9.2. fonte dati MSDE

Qui testiamo la fonte dati MSDE esaminata nel paragrafo 2.4.3.1. In questo caso viene utilizzata tramite SqlMap. Seguiamo la stessa procedura di prima. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • il contenuto della cartella [bin] rimane invariato
  • in [runtime], i file di configurazione di SqlMap e [providers.config, properties.xml] cambiano. I file di configurazione di [sqlmap.config, articles.xml] non cambiano.
  • Il file [providers.config] configura un nuovo <provider>:
<?xml version="1.0" encoding="utf-8" ?> 

<providers>
    <clear/>
    <provider
        name="sqlServer1.1"
        assemblyName="System.Data, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        connectionClass="System.Data.SqlClient.SqlConnection"
        commandClass="System.Data.SqlClient.SqlCommand"
        parameterClass="System.Data.SqlClient.SqlParameter"
        parameterDbTypeClass="System.Data.SqlDbType"
        parameterDbTypeProperty="SqlDbType"
        dataAdapterClass="System.Data.SqlClient.SqlDataAdapter"
        commandBuilderClass="System.Data.SqlClient.SqlCommandBuilder"    
        usePositionalParameters = "false"    
        useParameterPrefixInSql = "true"
        useParameterPrefixInParameter = "true"                
        parameterPrefix="@"
    />        
</providers>

Questo <provider> utilizza le classi .NET per l'accesso alle fonti di dati SQL Server. È integrato di serie nel file modello [providers.config] distribuito con SqlMap.

  • Il file [properties.xml] definisce il <provider> della fonte MSDE e la relativa stringa di connessione:
<?xml version="1.0" encoding="utf-8" ?> 
<settings>
    <add key="provider" value="sqlServer1.1" />
    <add 
        key="connectionString" 
        value="Data Source=portable1_tahe\msde140405;Initial Catalog=dbarticles;UID=admarticles;PASSWORD=mdparticles;"/>
</settings>
  • in [runtime], il file di configurazione [web.config] rimane invariato.

Siamo pronti per i test. Il server web [Cassini] mantiene la sua configurazione abituale. Inizializziamo la tabella degli articoli della fonte MSDE con [EMS MS SQL Manager]:

Image

Con un browser richiediamo URL [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES] con [EMS MS SQL Manager]:

Image

Gli articoli [ballon foot] e [raquette tennis] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [rollers] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.5.6.9.3. fonte dati OleDb

Qui testiamo la fonte dati ACCESS presentata al paragrafo 2.4.5.1. In questo caso viene utilizzata tramite SqlMap. Seguiamo la stessa procedura di prima. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • il contenuto della cartella [bin] rimane invariato
  • in [runtime], i file di configurazione di SqlMap e [providers.config, properties.xml] cambiano. I file di configurazione di [sqlmap.config, articles.xml] non cambiano.
  • Il file [providers.config] configura un nuovo <provider>:
<?xml version="1.0" encoding="utf-8" ?> 

<providers>
    <clear/>
    <provider 
        name="OleDb1.1" 
        enabled="true" 
        assemblyName="System.Data, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
        connectionClass="System.Data.OleDb.OleDbConnection" 
        commandClass="System.Data.OleDb.OleDbCommand" 
        parameterClass="System.Data.OleDb.OleDbParameter" 
        parameterDbTypeClass="System.Data.OleDb.OleDbType" 
        parameterDbTypeProperty="OleDbType" 
        dataAdapterClass="System.Data.OleDb.OleDbDataAdapter" 
        commandBuilderClass="System.Data.OleDb.OleDbCommandBuilder" 
        usePositionalParameters = "true"
        useParameterPrefixInSql = "false"
        useParameterPrefixInParameter = "false"
        parameterPrefix = ""
    />
</providers>

Questo <provider> utilizza le classi .NET per l'accesso alle fonti di dati OleDb. È integrato di serie nel file modello [providers.config] distribuito con SqlMap.

  • Il file [properties.xml] definisce il <provider> della fonte OleDb e la relativa stringa di connessione:
<?xml version="1.0" encoding="utf-8" ?> 
<settings>
    <add key="provider" value="OleDb1.1" />
    <add 
        key="connectionString" 
        value="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;"/>
</settings>
  • in [runtime], il file di configurazione [web.config] rimane invariato.

Siamo pronti per i test. Il server web [Cassini] mantiene la sua configurazione abituale. Inizializziamo la tabella degli articoli della fonte ACCESS nel modo seguente:

Image

Con un browser richiediamo l'URL [http://localhost/webarticles/main.aspx]:

Image

Ora verifichiamo il contenuto della tabella [ARTICLES] con:

Image

Gli articoli [pantalon] e [jupe] sono stati acquistati e le relative scorte sono state ridotte della quantità acquistata. L'articolo [manteau] non ha potuto essere acquistato poiché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a effettuare ulteriori test.

2.5.7. Conclusione

Concludiamo qui questo lungo articolo-tutorial. Cosa abbiamo fatto?

  • Abbiamo implementato il livello [dao] di un’applicazione web a tre livelli in quattro modi diversi:
    1. utilizzando le classi di accesso .NET alle sorgenti ODBC
    2. utilizzando le classi di accesso .NET alle sorgenti SQL Server
    3. utilizzando le classi di accesso .NET ai sorgenti OleDb
    4. utilizzando classi di accesso di terze parti per accedere a un database Firebird
  • ogni volta, abbiamo integrato il nuovo livello [dao] nell'applicazione a tre livelli [webarticles] [web, domain, dao] senza ricompilare nessuno dei livelli [web, domain]
  • abbiamo infine introdotto lo strumento [SqlMap] che ci ha permesso di creare un livello [dao] in grado di adattarsi a diverse fonti di dati in modo trasparente per il codice. In questo modo, grazie a questo nuovo livello, abbiamo potuto utilizzare in successione le fonti di dati delle precedenti implementazioni da 1 a 3. Ciò è stato realizzato in modo trasparente tramite file di configurazione.
  • Abbiamo dimostrato la grande flessibilità che gli strumenti Spring e SqlMap apportano alle applicazioni web a tre livelli.