Skip to content

2. Parte 2

2.1. Introduzione

Inizieremo rivedendo ciò che è stato fatto nella Parte 1, in particolare l'architettura a tre livelli [web, dominio, dao] utilizzata. Nella soluzione proposta, il livello [dao] era un livello di test: l'origine dati è stata implementata utilizzando un oggetto [ArrayList]. In questo articolo ci concentreremo sul livello [DAO], presentando varie possibili implementazioni dello stesso quando i dati si trovano in un DBMS.

Strumenti utilizzati:

  • il DBMS Firebird — vedere Appendice, Sezione 3.5.
  • il DBMS MSDE (Microsoft Data Engine) — vedi Appendice, Sezione 3.12.
  • IBExpert, Personal Edition, per l'amministrazione grafica del DBMS Firebird — vedere Appendice, Sezione 3.6.
  • EMS MS SQL Manager per l'amministrazione grafica del DBMS MSDE — vedi Appendice, Sezione 3.14.
  • Ibatis SqlMap per il livello di accesso ai dati del DBMS — vedi sezione 2.5.6.2.

Su una scala che va da principiante a intermedio ad avanzato, questo documento rientra nella categoria [intermedio-avanzato]. Per comprenderlo sono necessari vari prerequisiti. Alcuni di questi si possono trovare nei documenti che ho scritto. In tali casi, li cito. Va da sé che questo è solo un suggerimento e che il lettore è libero di utilizzare le risorse che preferisce.

  • Linguaggio VB.NET: [Introduzione a VB.NET attraverso esempi]
  • Programmazione web in VB.NET: [Sviluppo web con ASP.NET 1.1]
  • Utilizzo dell'IoC di Spring: [Spring IoC per .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]

2.2. L'applicazione webarticles - Recensione

Di seguito presentiamo i componenti dell'applicazione web di e-commerce semplificata descritta nella Parte 1. Questa applicazione consente agli utenti web di:

  • visualizzare un elenco di articoli da un database
  • aggiungerne alcuni a un carrello elettronico
  • confermare il carrello. Questa conferma aggiorna semplicemente i livelli di inventario degli articoli acquistati nel database.

2.2.1. Viste dell'applicazione

Le diverse viste presentate all'utente sono le seguenti:

  • la vista [ERRORI], 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 tramite l'uso di interfacce
  • L'integrazione dei diversi livelli è stata implementata utilizzando Spring
  • Ogni livello ha il proprio namespace: web (livello UI), domain (livello business) e dao (livello di accesso ai dati).

L'applicazione segue un'architettura MVC (Model-View-Controller). Se ci riferiamo al diagramma a livelli sopra riportato, l'architettura MVC si inserisce in esso come segue:

L'elaborazione di una richiesta del client segue questi passaggi:

  1. Il client invia una richiesta al controller. Questo controller è, in questo caso, una pagina .aspx che svolge un ruolo specifico. Gestisce tutte le richieste dei client. È il punto di ingresso dell'applicazione. È la C in MVC.
  2. Il controller elabora questa richiesta. Per farlo, potrebbe aver bisogno dell’assistenza del livello business, noto come la M nella struttura MVC.
  3. Il controller riceve una risposta dal livello di business. La richiesta del cliente è stata elaborata. Ciò può innescare diverse possibili risposte. Un classico esempio è
    • 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 molto spesso di una pagina contenente elementi dinamici. Il controller fornisce questi alla vista.
  5. La vista viene inviata al cliente. Questa è la V in MVC.

2.2.3. Il Modello

La M in MVC è costituita dai seguenti elementi:

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

2.2.3.1. Il database

Il database contiene una sola tabella denominata ARTICLES, generata utilizzando 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
);
/* constraints */
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);
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chiave primaria che identifica in modo univoco un articolo
nome
nome dell'elemento
prezzo
il suo prezzo
scorte attuali
scorte attuali
scorte minime
il livello di scorte al di sotto del quale è necessario effettuare un riordino

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 nel proprio file "assembly":

assembly
contenuto
ruolo
webarticles-dao
- [IArticlesDao]: l'interfaccia per accedere al livello [dao]. Questa è l'unica interfaccia visibile al livello [domain]. Non ne vede altre.
- [Article]: classe che definisce un articolo
- [ArticlesDaoArrayList]: classe di implementazione dell'interfaccia [IArticlesDao] che utilizza una classe [ArrayList]
livello di accesso ai dati - situato interamente all'interno del livello [dao] dell'architettura a 3 livelli dell'applicazione web
webarticles-domain
- [IArticlesDomain]: l'interfaccia per l'accesso al livello [domain]. È l'unica interfaccia visibile al livello web. Non ne vede altre.
- [ArticlePurchases]: una classe che implementa [IArticlesDomain]
- [Purchase]: una classe che rappresenta l'acquisto di un cliente
- [Cart]: una classe che rappresenta il totale degli acquisti di un cliente
rappresenta il modello di acquisto web - risiede interamente nel livello [domain] dell'architettura a 3 livelli dell'applicazione web

2.2.4. Distribuzione e test dell'applicazione [webarticles]

2.2.4.1. Distribuzione

Distribuisciamo l'applicazione sviluppata nella Parte 1 di questo articolo in una cartella denominata [runtime]:

 

Commenti:

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:
    • le DLL per i tre livelli [webarticles-dao.dll], [webarticles-domain.dll], [webarticles-web.dll]
    • i file richiesti da Spring [Spring-Core.*], [log4net.dll]
  • la cartella [views], che contiene il codice di presentazione per le varie viste.
  • La presenza di file di codice .vb non è necessaria poiché le loro versioni compilate si trovano nelle DLL.

2.2.4.2. Test

Configuriamo il server web [Cassini] come segue:

Image

con:

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

Percorso virtuale: /webarticles

Utilizzando un browser, richiediamo l'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. Dalla vista sopra riportata, utilizziamo i collegamenti del menu per eseguire le operazioni. Eccone alcune. La colonna di sinistra rappresenta la richiesta del client, mentre quella di destra rappresenta la risposta inviata al client.

2.2.5. Il livello [DAO] rivisitato

Nella nostra prima implementazione del livello [dao], l'interfaccia di accesso ai dati [IArticlesDao] era stata implementata da una classe che memorizzava gli elementi in un oggetto [ArrayList]. Questo ci ha permesso di evitare di complicare eccessivamente questo livello e di dimostrare che contava solo la sua interfaccia, non la sua implementazione. Siamo così riusciti a costruire un'applicazione web funzionante. Questa applicazione ha tre livelli: [web], [domain] e [dao]. Qui proporremo diverse implementazioni del livello [dao]. Ognuna di esse può sostituire l'attuale livello [dao] senza alcuna modifica ai livelli [domain] e [web]. Questa flessibilità si ottiene perché:

  • il livello [domain] non fa riferimento a una classe concreta ma a un'interfaccia [IArticlesDao]
  • grazie a Spring, siamo stati in grado di nascondere il nome della classe che implementa l'interfaccia [IArticlesDao] dal livello [domain].

2.2.5.1. Elementi del livello [dao]

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

  • - [IArticlesDao]: l'interfaccia per accedere al livello [dao]
  • - [Article]: la 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

        ' private fields
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

        ' id article
        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

        ' item name
        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

        ' item price
        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

        ' current stock item
        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

        ' minimum stock item
        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

        ' default builder
        Public Sub New()
        End Sub

        ' builder with properties
        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

        ' article identification method
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Questa classe fornisce:

  1. un costruttore per impostare le 5 informazioni relative a un articolo: [id, nome, prezzo, stockAttuale, stockMinimo]
  2. proprietà pubbliche per la lettura e la scrittura delle 5 informazioni.
  3. una convalida dei dati inseriti per l'articolo. Se i dati non sono validi, viene generata un'eccezione.
  4. un metodo toString che restituisce il valore di un articolo come stringa. Questo è 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
        ' list of all items
        Function getAllArticles() As IList
        ' add an article
        Function ajouteArticle(ByVal unArticle As Article) As Integer
        ' deletes an article
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
        ' modify an article
        Function modifieArticle(ByVal unArticle As Article) As Integer
        ' search for an article
        Function getArticleById(ByVal idArticle As Integer) As Article
        ' deletes all articles
        Sub clearAllArticles()
        ' changes the stock of an item
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

I ruoli dei vari metodi nell'interfaccia sono i seguenti:

getAllArticles
restituisce tutti gli articoli dall'origine dati
clearAllArticles
cancella l'origine dati
getArticleById
restituisce l'oggetto [Article] identificato dal suo numero
aggiungiArticolo
consente di aggiungere un articolo all'origine dati
modificaArticolo
consente di modificare un articolo nell'origine dati
deleteItem
consente di eliminare un elemento dall'origine dati
updateItemStock
consente di modificare la quantità di un articolo nell'origine dati

L'interfaccia fornisce ai programmi client una serie di metodi definiti esclusivamente dalle loro firme. Non si occupa di come questi metodi saranno effettivamente implementati. Ciò garantisce flessibilità all'applicazione. Il programma client effettua chiamate su un'interfaccia piuttosto che 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 sorgente ODBC. Sappiamo che su Windows, praticamente tutti i DBMS presenti sul mercato dispongono di un driver ODBC. Il vantaggio di questa soluzione è che è possibile cambiare DBMS in modo trasparente per l'applicazione. Lo svantaggio è che un driver ODBC che sfrutta solo le funzionalità comuni a tutti i DBMS è generalmente meno efficiente di un driver scritto specificamente per sfruttare appieno il potenziale di un particolare DBMS. Vedere la Sezione 3.7 per un esempio di creazione di una sorgente ODBC.

2.3.1. Il codice

2.3.1.1. Lo scheletro

La classe [ArticlesDaoPlainODBC] implementa l'interfaccia [IArticlesDao] come segue:

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

Namespace istia.st.articles.dao

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

        ' private fields
        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

        ' manufacturer
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
            ' DSN: name of source ODBC: name of source
            ' uid: user identity
            ' password: your 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
            ' query execution SELECT 
....
        End Function

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

End Namespace

Commenti:

  • Riga 3: importa lo spazio dei nomi contenente le classi .NET per l'accesso alle origini ODBC
  • Riga 11: memorizza la connessione alla sorgente ODBC
  • Riga 12: memorizza il nome DSN dell'origine dati
  • Righe 13–19: variabili private di tipo [OdbcCommand] che definiscono le query SQL utilizzate dai vari metodi della classe
  • Righe 22–27: il costruttore. Riceve gli elementi necessari per costruire l'oggetto [OdbcConnection] che collegherà il codice alla fonte dati ODBC
  • righe 29–31 – il metodo per aggiungere un articolo
  • righe 33–35 – il metodo per modificare la disponibilità di un articolo
  • righe 37–39 – il metodo che elimina tutti gli articoli dall'origine dati ODBC
  • righe 41-43: il metodo che recupera l'elenco di tutti gli articoli dall'origine ODBC
  • righe 45–47 – il metodo che recupera un articolo specifico
  • righe 49-51 – il metodo che consente di modificare determinati campi di un articolo di cui si conosce il numero
  • righe 53-55 – il metodo che consente di eliminare un articolo in base al suo numero
  • righe 57-60 - metodo di utilità per eseguire un [SELECT] sull'origine dati e restituire il risultato
  • righe 62-64 – metodo di utilità per eseguire un [INSERT, UPDATE, DELETE] sull'origine dati e restituire il risultato

2.3.1.2. Il

        ' manufacturer
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
            ' DSN: name of source ODBC: name of source
            ' uid: user identity
            ' password: your password

            'retrieve the name of the database passed as an argument
            Me.DSN = DSN
            Dim connectString As String = String.Format("DSN={0};UID={1};PASSWORD={2}", DSN, uid, password)
            'we instantiate the connection
            connexion = New OdbcConnection(connectString)
            ' prepare SQL requests
            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 sorgente ODBC: il nome DSN della sorgente, il nome utente con cui connettersi e la password associata.
  • Riga 8 - Il nome DSN della sorgente viene memorizzato in modo da poter essere incluso nei messaggi di errore.
  • Riga 9 - Viene istanziato l'oggetto [OdbcConnection]. Una connessione istanziata non è una connessione aperta. Il metodo [open] viene utilizzato per aprire la connessione.
  • Righe 12–19 – Prepariamo le query SQL negli oggetti [OdbcCommand]. Questo ci eviterà di doverle ricostruire ogni volta che ne avremo bisogno. I parametri formali ? nelle query saranno sostituiti con valori effettivi quando la query verrà eseguita.

2.3.1.3. Il metodo executeQuery

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
            ' query execution SELECT 
            ' declaration of the object providing access to all rows in the result table
            Dim myReader As OdbcDataReader = Nothing
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                myReader = query.ExecuteReader()
                'declare a list of items and return it later
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                    'prepare an article with the reader's values
                    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)
                    'add the item to the list
                    articles.Add(unArticle)
                End While
                'returns the result
                Return articles
            Finally
                ' freeing up resources
                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, name, price, currentStock, minimumStock from ARTICLES ...] sull'origine dati
    • restituisce il risultato come un elenco di oggetti [Article]
  • Riga 1 - L'unico parametro del metodo è l'oggetto [OdbcCommand] contenente la query [Select] da eseguire.
  • Riga 7 - Viene aperta la connessione. Verrà chiusa alla riga 29 indipendentemente dal fatto che si sia verificato o meno un errore.
  • Riga 9 - Viene istanziato l'oggetto [OdbcDataReader] necessario per elaborare il risultato della [Select]
  • Righe 13–23 – Ogni riga risultante dalla query [Select] viene inserita in un oggetto [Article], che viene poi aggiunto agli altri articoli in un [ArrayList]
  • L'elenco degli elementi viene restituito alla riga 25
  • Non vengono gestite eccezioni. Devono essere gestite dal codice che chiama questo metodo.

2.3.1.4. Il metodo executeUpdate

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
            ' execute an update request
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                Return sqlCommand.ExecuteNonQuery()
            Finally
                ' freeing up resources
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

Commenti:

  • Il metodo riceve un oggetto [OdbcCommand] contenente una query 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 query di aggiornamento viene eseguita alla riga 7. Il risultato, ovvero il numero di righe nella tabella ARTICLES modificate dalla query, viene restituito immediatamente.

2.3.1.5. Il metodo addArticle

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            ' exclusive section
            SyncLock Me
                ' prepare the insertion request
                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
                    'it is executed
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                    'query error
                    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'elemento da aggiungere all'origine dati ODBC. Restituisce il numero di righe interessate da questa operazione, ovvero 1 o 0
  • Righe 3 e 20 - Il metodo è sincronizzato. Questo vale per tutti i metodi di accesso ai dati. Ciò significa che solo un thread alla volta può lavorare sull'origine dati. Probabilmente si tratta di un approccio troppo conservativo. Esistono alternative migliori, in particolare includere queste operazioni all'interno di transazioni. In questo caso, il DBMS gestisce l'accesso concorrente. Non abbiamo voluto introdurre il concetto di transazioni in questa fase. Spring ci offre la possibilità di introdurle nel livello [domain]. Potremmo avere l'opportunità di riprendere questo argomento in un altro articolo.
  • Le righe 5-12 assegnano valori ai parametri formali della query per l'oggetto [insertCommand] inizializzato dal costruttore. Ecco di nuovo la query:
insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)

I 5 valori richiesti per la query sono forniti dalle righe 7–11.

  • Righe 13–19: la query viene eseguita. Se ha esito positivo, 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
            ' exclusive section
            SyncLock Me
                ' prepare the update request
                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
                ' it is executed
                Try
                    'execute the insertion request
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                    'query error
                    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'elemento da modificare dall'origine dati ODBC. Restituisce il numero di righe interessate da questa operazione, ovvero 1 o 0
  • Qui è possibile inserire i commenti del metodo [addItem]

2.3.1.7. Il metodo deleteArticle

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            ' exclusive section
            SyncLock Me
                ' prepare the delete request
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                'it is executed
                Try
                    'execute the delete request
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                    'query error
                    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 l'ID dell'elemento da eliminare dall'origine dati ODBC. Restituisce il numero di righe interessate da questa operazione, ovvero 1 o 0
  • I commenti relativi al metodo [addItem] possono essere inseriti qui

2.3.1.8. Il metodo getAllArticles

            Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
            ' exclusive section
            SyncLock Me
                Try
                    'execute the select query
                    Dim articles As IList = executeQuery(selectAllCommand)
                    'we return the list
                    Return articles
                Catch ex As Exception
                    'query error
                    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 accetta parametri. Restituisce un elenco di tutti gli elementi provenienti dall'origine dati ODBC
  • La query [Select] che recupera tutti i record viene passata al metodo [executeQuery] - riga 6
  • L'elenco risultante viene restituito alla riga 8
  • Le righe 9–12 gestiscono eventuali eccezioni

2.3.1.9. Il metodo getArticleById

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            ' exclusive section
            SyncLock Me
                ' prepare the select query
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                'it is executed
                Try
                    'execute the query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                    'we test if we have found the article
                    If articles.Count = 0 Then Return Nothing
                    'we return the item
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                    'query error
                    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 l'ID dell'elemento desiderato come parametro. Restituisce l'elemento se viene trovato nella sorgente ODBC; in caso contrario, restituisce il riferimento [nothing].
  • La query [Select] che richiede l'elemento viene inizializzata nelle righe 5–8
  • viene eseguita alla riga 12 - si ottiene un elenco di elementi
  • se questo elenco è vuoto, alla riga 14 viene restituito il riferimento [nothing]
  • Altrimenti, l'unico elemento dell'elenco viene visualizzato alla riga 16
  • Le righe 17–20 gestiscono eventuali eccezioni

2.3.1.10. Il metodo clearAllArticles

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
            ' exclusive section
            SyncLock Me
                Try
                    'execute the delete request
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                    'query error
                    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 accetta parametri e non restituisce alcun valore
  • Riga 6 - Viene eseguita la query per eliminare tutti gli elementi
  • Righe 7–10: Gestione delle eccezioni

2.3.1.11. Il metodo changerStockArticle

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            ' exclusive section
            SyncLock Me
                ' prepare the stock update request
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("mvt1", mouvement))
                    .Add(New OdbcParameter("id", idArticle))
                    .Add(New OdbcParameter("mvt2", mouvement))
                End With
                'it is executed
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                    'query error
                    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 per il quale è necessario modificare lo stock, nonché l'incremento dello stock (positivo o negativo). Restituisce il numero di righe modificate dall'operazione, ovvero 0 o 1.
  • Righe 5–10: Viene inizializzata la query [updateStockCommand]. Ecco 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, dopo la modifica, rimane >=0.

  • La query per aggiornare lo stock dell'articolo viene eseguita alla riga 13 e il risultato viene restituito
  • righe 14–18, dove gestiamo eventuali eccezioni

2.3.2. Generazione dell'assembly del livello [DAO]

Il progetto Visual Studio per questa nuova versione del livello [dao] ha la seguente struttura:

Image

Il progetto è configurato per generare una DLL denominata [webarticles-dao.dll]:

2.3.3. Test NUnit per il livello [dao]

2.3.3.1. Creazione di un'origine dati ODBC-Firebird in

Per testare il nostro nuovo livello [DAO], abbiamo bisogno di una sorgente dati ODBC e quindi di un database. Stiamo utilizzando il DBMS Firebird (Sezione 3.5). Utilizzando IBExpert (Sezione 3.6), creiamo il seguente database degli articoli:

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

Image

Ora creiamo la seguente sorgente ODBC Firebird (vedere la sezione 3.7):

La sorgente ODBC creata presenta le seguenti caratteristiche:

  • Nome DSN: odbc-firebird-articles
  • ID connessione: SYSDBA
  • password associata: masterkey

2.3.3.2. La classe di test NUnit per il livello [dao]

Abbiamo già scritto una classe di test per il livello [dao] inizialmente realizzato. Se il lettore 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
        ' the test object
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' retrieve an instance of the Spring object manufacturer
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' request instantiation of articlesdao object
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

....

Possiamo notare che nel metodo <Setup()> chiediamo a Spring un riferimento al singleton denominato [articlesdao] di tipo [IArticlesDao], ovvero di tipo interfaccia. Il singleton [articlesdao] è stato 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 o ricompilazioni.

  • Nella cartella Visual Studio del nostro nuovo livello [dao], creare la cartella [tests] (mostrata in basso a destra) copiando la cartella [bin] dal progetto di test del livello [dao] iniziale (mostrato 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], sostituire il file DLL [webarticles-dao.dll] del vecchio livello [dao] con il file DLL [webarticles-dao.dll] del nuovo livello [dao]
  • Modifica 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:

  • alla riga 6, l'oggetto [articlesdao] è ora associato a un'istanza della classe [ArticlesDaoPlainODBC]
  • Questa classe ha un costruttore con tre argomenti:
  • il nome della sorgente DSN - riga 8
  • l'identità utilizzata per accedere al database - riga 11
  • la password associata a questa identità - riga 14

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

2.3.3.3. Test

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

Image

Guardando lo screenshot qui sopra, potremmo pentirci del nome [NUnitTestArticlesDaoArrayList] inizialmente assegnato alla classe di test. È fuorviante. In realtà è la classe [ArticlesDaoPlainODBC] che viene testata qui. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che abbiamo inserito nella tabella [ARTICLES]. Ora, eseguiamo tutti i test:

Image

Nella finestra a sinistra, vediamo l'elenco dei metodi testati. Il colore del punto che precede il nome di ciascun metodo indica se il metodo è stato superato (verde) o ha fallito (rosso). I lettori che visualizzano questo documento sullo schermo vedranno che tutti i test hanno avuto esito positivo.

2.3.3.4. Conclusione

Abbiamo appena dimostrato che:

  • poiché la classe di test NUnit non faceva riferimento a una classe ma a un'interfaccia;
  • perché il nome esatto della classe che istanzia l'interfaccia è stato fornito in un file di configurazione e non nel codice;
  • perché Spring si è occupato di istanziare la classe e di fornire un riferimento ad essa nel codice di test;

quindi, il codice di test scritto per il livello [dao] iniziale è rimasto valido per una nuova implementazione di quello stesso livello. Non abbiamo avuto bisogno di accedere al codice della classe di test. Abbiamo usato solo la sua versione compilata, quella generata durante il test del livello [dao] iniziale. Trarremo conclusioni simili quando arriverà il momento di integrare il nuovo livello [dao] nell'applicazione [webarticles].

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

2.3.4.1. Test di integrazione

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

 

Si invitano i lettori a consultare la Sezione 2.2.4, che descrive in dettaglio le procedure di distribuzione dell'applicazione [webarticles]. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • Nella cartella [bin], la DLL del vecchio livello [dao] viene sostituita dalla 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 il singleton [articlesDao] a un'istanza della nuova classe [ArticlesDaoPlainODBC]. Questa è l'unica modifica. Abbiamo già riscontrato questo problema durante il test del nuovo livello [dao].

Siamo pronti per il test. Configuriamo il server web [Cassini] seguendo le istruzioni riportate nella sezione 2.2.4. Inizializziamo la tabella del prodotto [Firebird] con i seguenti valori:

Image

Assicurarsi che il server web Cassini e il DBMS [Firebird] siano in esecuzione. Utilizzando un browser, richiediamo l'URL [http://localhost/webarticles/main.aspx]:

Image

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

Image

Gli articoli [ombrello] e [stivali] sono stati acquistati e le loro giacenze sono state ridotte della quantità acquistata. L'articolo [cappello] non ha potuto essere acquistato perché la quantità richiesta superava la quantità disponibile in magazzino. Invitiamo il lettore a eseguire ulteriori test.

2.3.4.2. Conclusione

Cosa abbiamo fatto?

  • Siamo tornati alla versione di distribuzione della versione precedente;
  • abbiamo sostituito la DLL del livello [dao] con una nuova versione. Le DLL dei livelli [web] e [domain] sono rimaste invariate;
  • abbiamo modificato il file di configurazione [web.config] in modo che tenga conto della nuova classe di implementazione del livello [dao]

Tutto questo è pulito e rende molto facile far evolvere l'applicazione web. Queste importanti caratteristiche sono garantite da due scelte architetturali:

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

Stiamo ora proponendo 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 fornisce un DBMS chiamato MSDE, che è una versione limitata di SQL Server. Vedere l'appendice per le istruzioni su come ottenerlo e installarlo, sezione 3.12.

2.4.1. Il codice

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

  • le classi richieste si trovano nello spazio dei nomi [System.Data.SqlClient] anziché nello spazio dei nomi [System.Data.Odbc]
  • la connessione [OdbcConnection] è ora di tipo [SqlConnection]
  • gli oggetti [OdbcCommand] sono ora di tipo [SqlCommand]
  • la sintassi delle query SQL parametrizzate è cambiata. La query di inserimento ora ha questo aspetto:
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 [addItem] diventa quindi il seguente:
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            ' exclusive section
            SyncLock Me
                ' prepare the insertion request
                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
                    'it is executed
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                    'query error
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function
  • Anche il costruttore viene modificato:
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
            ' server: instance name SQL server to reach
            ' databaseName: name of the database to be reached
            ' uid: user identity
            ' password: your password

            'retrieve the name of the database passed as an argument
            Me.databaseName = databaseName
            'we instantiate the connection
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
            ' prepare SQL requests
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
...
        End Sub

Il costruttore ora accetta quattro parametri:

            ' server: instance name SQL server to reach
            ' databaseName: name of the database to be reached
            ' uid: user identity
            ' password: your password

Il codice completo per la 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

        ' private fields
        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

        ' manufacturer
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
            ' server: instance name SQL server to reach
            ' databaseName: name of the database to be reached
            ' uid: user identity
            ' password: your password

            'retrieve the name of the database passed as an argument
            Me.databaseName = databaseName
            'we instantiate the connection
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
            ' prepare SQL requests
            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
            ' exclusive section
            SyncLock Me
                ' prepare the insertion request
                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
                    'it is executed
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the stock update request
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@mvt", mouvement))
                    .Add(New SqlParameter("@id", idArticle))
                End With
                'it is executed
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                Try
                    'execute the insertion request
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                Try
                    'execute the select query
                    Dim articles As IList = executeQuery(selectAllCommand)
                    'we return the list
                    Return articles
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the select query
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                'it is executed
                Try
                    'execute the query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                    'we test if we've found the article
                    If articles.Count = 0 Then Return Nothing
                    'we return the item
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the update request
                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
                ' it is executed
                Try
                    'execute the insertion request
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the delete request
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                'it is executed
                Try
                    'execute the delete request
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                    'query error
                    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
            ' query execution SELECT 
            ' declaration of the object providing access to all rows in the result table
            Dim myReader As SqlDataReader = Nothing
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                myReader = query.ExecuteReader()
                'declare a list of items and return it later
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                    'we prepare an article with the reader's values
                    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)
                    'add the item to the list
                    articles.Add(unArticle)
                End While
                'returns the result
                Return articles
            Finally
                ' freeing up resources
                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
            ' execute an update request
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                Return updateCommand.ExecuteNonQuery()
            Finally
                ' freeing up resources
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Si invita il lettore a rivedere questo codice alla luce dei commenti sulla 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 una DLL denominata [webarticles-dao.dll]:

2.4.3. Test NUnit per il livello [dao]

2.4.3.1. Creazione di un'origine dati SQL Server

Per testare il nostro nuovo livello [dao], abbiamo bisogno di una fonte dati SQL Server e quindi del DBMS SQL Server. Utilizzeremo effettivamente il DBMS MSDE (Microsoft Data Engine) (Sezione 3.12), che è una versione di SQL Server limitata solo dal numero di utenti simultanei che supporta. Utilizzando [EMS MS SQL Manager] (sezione 3.14), creiamo il seguente database di prodotto in un'istanza MSDE denominata [portable1_tahe\msde140405]:

Image

Il database è di proprietà dell'utente [mdparticles] con password [admarticles]. Il comando Transact-SQL per creare la 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 elementi:

Image

2.4.3.2. La classe di test NUnit

La classe di test NUnit per la classe di implementazione [ArticlesDaoSqlServer] è la stessa di quella per la classe [ArticlesDaoPlainODBC] (vedere la sezione 2.3.3.2). Seguiamo un approccio simile per preparare il test NUnit per la classe:

  • creiamo la cartella [tests] (a destra) nella cartella Visual Studio del progetto [dao-sqlserver] copiando la cartella [tests] dal progetto [dao-odbc] (a sinistra):
  • nella cartella [tests] del progetto [dao-sqlserver], sostituiamo la DLL [webarticles-dao.dll] con la DLL [webarticles-dao.dll] generata 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 [ArticlesDaoSqlServer]
  • Questa classe ha un costruttore con quattro argomenti:
  • il nome dell'istanza MSDE utilizzata - riga 9
  • il nome del database - riga 12
  • l'identità utilizzata per accedere al database - riga 15
  • la password associata a questa identità - riga 18

Qui stiamo utilizzando le informazioni provenienti dalla sorgente MSDE che abbiamo creato in precedenza.

2.4.3.3. Test

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

Image

Sebbene la classe di test fosse inizialmente denominata [NUnitTestArticlesDaoArrayList] — un nome mantenuto poiché stiamo utilizzando la DLL [tests-webarticles-dao.dll] derivata da questa classe — è in realtà la classe [ArticlesDaoSqlserver] che viene testata qui. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che abbiamo inserito nella tabella [ARTICLES]. Ora, eseguiamo tutti i test:

Image

Nella finestra a sinistra, vediamo 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). I lettori che visualizzano questo documento sullo schermo vedranno che tutti i test hanno avuto esito positivo.

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

Seguiamo la procedura spiegata nella sezione 2.3.4. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • Nella cartella [bin], la DLL del vecchio livello [dao] viene sostituita dalla 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 il singleton [articlesDao] a un'istanza della nuova classe [ArticlesDaoSqlServer]. Questa è l'unica modifica. Abbiamo già riscontrato questo problema durante il test del nuovo livello [dao]

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

Image

Assicurarsi che il server web Cassini e il DBMS MSDE (in questo caso, l'istanza portable1_tahe\msde140405) siano in esecuzione. Utilizzando un browser, richiediamo l'URL [http://localhost/webarticles/main.aspx]:

Image

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

Image

Gli articoli [pallone da calcio] e [racchetta da tennis] sono stati acquistati e le loro scorte sono state ridotte della quantità acquistata. L'articolo [pattini a rotelle] non ha potuto essere acquistato perché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a eseguire ulteriori test.

2.4.5. La classe di implementazione [ArticlesDaoOleDb]

2.4.5.1. e 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 alla base delle origini OleDb è analogo a quello delle origini ODBC. Un programma che utilizza un'origine OleDb lo fa tramite un'interfaccia standard comune a tutte le origini OleDb. La modifica dell'origine OleDb comporta semplicemente la modifica del driver OleDb. Il codice stesso rimane invariato.

È possibile scoprire quali driver OleDb sono disponibili sul proprio computer utilizzando Visual Studio:

  • visualizzare Server Explorer selezionando [Visualizza/Server Explorer]:

Image

  • Per aggiungere una nuova connessione, fare clic con il tasto destro del mouse su [Connessione dati] e selezionare l'opzione [Aggiungi connessione]. Si aprirà una procedura guidata in cui è possibile definire le impostazioni di connessione:

Image

  • Il riquadro [Provider] elenca i driver OLEDB disponibili. Per il nuovo livello [DAO], useremo il driver [Microsoft Jet 4.0 OLE DB Provider], che fornisce l'accesso ai database di Access.
  • Usciamo temporaneamente da Visual Studio per creare il database ACCESS [articles.mdb] con la seguente tabella singola:

Image

  • La struttura della tabella è la seguente:
id
numerico - intero - chiave primaria
nome
testo - 20 caratteri -
prezzo
numerico - doppio
stock attuale
numerico - intero
scorta minima
numerico - intero
  • Torniamo a Visual Studio e creiamo una nuova connessione come spiegato in precedenza:

Image

  • Selezioniamo il driver [Microsoft Jet 4.0] e andiamo al pannello [Connessione]:

Image

  • Utilizzando il pulsante [1], selezioniamo il database ACCESS appena creato, quindi completiamo la configurazione della connessione facendo clic sul pulsante [Fine]. La connessione appena creata appare ora nell'elenco delle connessioni disponibili:

Image

  • Facendo doppio clic sulla tabella [ARTICLES] si accede al suo contenuto:

Image

  • È quindi possibile aggiungere, modificare o eliminare righe nella tabella.
  • In Esplora server, selezionare la nuova connessione per accedere alla scheda Proprietà:

Image

  • È utile conoscere la stringa di connessione. La useremo per connetterci 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
  • Da questa stringa, manterremo 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 per la classe [ArticlesDaoOleDb]

La classe [ArticlesDaoOleDb] è molto simile alla classe [ArticlesDaoPlainODBC] discussa in precedenza. Pertanto, elencheremo solo le modifiche apportate alla versione precedente:

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

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

        ' manufacturer
        Public Sub New(ByVal connectString As String)
            ' connectString: source connection string OleDb: source connection string connectString: source connection string OleDb: source connection string
            'we instantiate the connection
            connexion = New OleDbConnection(connectString)
            ' prepare SQL requests
...
        End Sub

Il codice completo per la 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

        ' private fields
        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

        ' manufacturer
        Public Sub New(ByVal connectString As String)
            ' connectString: source connection string OleDb: source connection string connectString: source connection string OleDb: source connection string
            'we instantiate the connection
            connexion = New OleDbConnection(connectString)
            ' prepare SQL requests
            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
            ' exclusive section
            SyncLock Me
                ' prepare the insertion request
                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
                    'it is executed
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the stock update request
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("mvt1", mouvement))
                    .Add(New OleDbParameter("id", idArticle))
                    .Add(New OleDbParameter("mvt2", mouvement))
                End With
                'it is executed
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                Try
                    'execute the insertion request
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                Try
                    'execute the select query
                    Dim articles As IList = executeQuery(selectAllCommand)
                    'we return the list
                    Return articles
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the select query
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                'it is executed
                Try
                    'execute the query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                    'we test if we've found the article
                    If articles.Count = 0 Then Return Nothing
                    'we return the item
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the update request
                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
                ' it is executed
                Try
                    'execute the insertion request
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the delete request
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                'it is executed
                Try
                    'execute the delete request
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                    'query error
                    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
            ' query execution SELECT 
            ' declaration of the object providing access to all rows in the result table
            Dim myReader As OleDbDataReader = Nothing
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                myReader = query.ExecuteReader()
                'declare a list of items and return it later
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                    'we prepare an article with the reader's values
                    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)
                    'add the item to the list
                    articles.Add(unArticle)
                End While
                'returns the result
                Return articles
            Finally
                ' freeing up resources
                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
            ' execute an update request
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                Return sqlCommand.ExecuteNonQuery()
            Finally
                ' freeing up resources
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Si invita il lettore a rivedere questo codice alla luce dei commenti sulla 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 una DLL denominata [webarticles-dao.dll]:

2.4.5.4. Test NUnit per il livello [dao]

2.4.5.4.1. La classe di test NUnit

La classe di test NUnit per la classe di implementazione [ArticlesDaoOleDb] è la stessa di quella per la classe [ArticlesDaoPlainODBC] (vedere la sezione 2.3.3.2). Seguiamo un approccio simile per preparare il test NUnit per la classe:

  • creiamo la cartella [tests] (a destra) nella cartella Visual Studio del progetto [dao-oledb] copiando la cartella [tests] dal progetto [dao-odbc] (a sinistra):
  • nella cartella [tests] del progetto [dao-oledb], sostituiamo la DLL [webarticles-dao.dll] con la DLL [webarticles-dao.dll] generata dal 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:

  • alla 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 ACCESS OleDb - riga 9
2.4.5.4.2. Test

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

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] inizialmente assegnato alla classe di test, è in realtà la classe [ArticlesDaoOleDb] che viene testata qui. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che abbiamo inserito nella tabella [ARTICLES]. Ora, eseguiamo tutti i test:

Image

I lettori che visualizzano questo documento sullo schermo vedranno che tutti i test sono stati superati (colore verde).

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

Seguiamo la procedura spiegata nella sezione 2.3.4. Apportiamo le seguenti modifiche al contenuto della cartella [runtime]:

  • Nella cartella [bin], la DLL del vecchio livello [dao] viene sostituita dalla 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 il singleton [articlesDao] a un'istanza della nuova classe [ArticlesDaoOleDb]. Questa è l'unica modifica.

Manteniamo la stessa configurazione del server web [Cassini] di prima. Inizializziamo la tabella dei prodotti 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. Utilizzando un browser, richiediamo l'URL [http://localhost/webarticles/main.aspx]:

Image

Ora controlliamo il contenuto della tabella [ARTICLES] utilizzando Access:

Image

Gli articoli [pants] e [skirt] sono stati acquistati e le loro scorte sono state ridotte della quantità acquistata. L'articolo [coat] non ha potuto essere acquistato perché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a eseguire ulteriori test.

2.5. La classe di implementazione [ArticlesDaoFirebirdProvider]

2.5.1. Il provider Firebird-net

In precedenza abbiamo utilizzato un'origine dati [Firebird] tramite un driver ODBC. Sebbene i driver ODBC offrano un'elevata riutilizzabilità per il codice che li utilizza, sono meno efficienti dei driver scritti specificamente per il DBMS di destinazione. Il DBMS [Firebird] può essere utilizzato tramite una libreria di classi specifiche scaricabile dal sito web 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 per l'accesso al DBMS Firebird. L'installazione del pacchetto crea una cartella simile alla seguente:

Image

Due elementi sono di nostro interesse:

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

Successivamente, per consentire a un progetto Visual Studio di utilizzare queste classi, faremo due cose:

  • Inseriremo l'assembly [FirebirdSql.Data.Firebird.dll] nella cartella [bin] del progetto
  • aggiungeremo questo stesso assembly ai riferimenti del progetto

2.5.2. Il codice per la classe [ArticlesDaoFirebirdProvider]

La classe [ArticlesDaoFirebirdProvider] è molto simile alla classe [ArticlesDaoSqlServer] discussa in precedenza. Pertanto, evidenzieremo solo le modifiche apportate rispetto a quella versione:

  • Le classi richieste si trovano nello spazio dei nomi [FirebirdSql.Data.Firebird] anziché nello spazio dei nomi [System.Data.SqlClient]
  • la connessione [SqlConnection] è ora di tipo [FbConnection]
  • gli oggetti [SqlCommand] sono ora di tipo [FbCommand]
  • Gli oggetti [SqlParameter] sono ora di tipo [FbParameter]

Il costruttore della classe accetta quattro parametri, che utilizza per costruire la stringa di connessione al database:

        ' manufacturer
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
            ' server: name of the SGBD host machine
            ' databaseName: path to database
            ' uid: identity of the user logging in
            ' password: your password
...
        End Sub

Il codice completo per la 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

        ' private fields
        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

        ' manufacturer
        Public Sub New(ByVal serveur As String, ByVal databasePath As String, ByVal uid As String, ByVal password As String)
            ' server: name of the SGBD Firebird host machine
            ' databaseName: path to the database to be used
            ' uid: identity of the user connecting to the database
            ' password: your password

            'retrieve the name of the database passed as an argument
            Me.databasePath = databasePath
            'we instantiate the connection
            Dim connectString As String = String.Format("DataSource={0};Database={1};User={2};Password={3}", serveur, databasePath, uid, password)
            connexion = New FbConnection(connectString)
            ' prepare SQL requests
            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
            ' exclusive section
            SyncLock Me
                ' prepare the insertion request
                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
                    'it is executed
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the stock update request
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@mvt", mouvement))
                    .Add(New FbParameter("@id", idArticle))
                End With
                'it is executed
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                Try
                    'execute the insertion request
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                Try
                    'execute the select query
                    Dim articles As IList = executeQuery(selectAllCommand)
                    'we return the list
                    Return articles
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the select query
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                'it is executed
                Try
                    'execute the query
                    Dim articles As IList = executeQuery(selectSomeCommand)
                    'we test if we've found the article
                    If articles.Count = 0 Then Return Nothing
                    'we return the item
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the update request
                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
                ' it is executed
                Try
                    'execute the insertion request
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                    'query error
                    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
            ' exclusive section
            SyncLock Me
                ' prepare the delete request
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                'it is executed
                Try
                    'execute the delete request
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                    'query error
                    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
            ' query execution SELECT 
            ' declaration of the object providing access to all rows in the result table
            Dim myReader As FbDataReader = Nothing
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                myReader = query.ExecuteReader()
                'declare a list of items and return it later
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                    'we prepare an article with the reader's values
                    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)
                    'add the item to the list
                    articles.Add(unArticle)
                End While
                'returns the result
                Return articles
            Finally
                ' freeing up resources
                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
            ' execute an update request
            Try
                'create a connection to BDD
                connexion.Open()
                'execute the query
                Return updateCommand.ExecuteNonQuery()
            Finally
                ' freeing up resources
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Si invita il lettore a rivedere questo codice alla luce dei commenti sulla 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. Questa DLL è stata collocata nella cartella [bin] del progetto. Il progetto è configurato per generare una DLL denominata [webarticles-dao.dll]:

2.5.4. Test NUnit per il livello [dao]

2.5.4.1. La classe di test NUnit

La classe di test NUnit per la classe di implementazione [ArticlesDaoFirebirdProvider] è la stessa di quella per la classe [ArticlesDaoPlainODBC] (vedere la sezione 2.3.3.2). Seguiamo un approccio simile per preparare il test NUnit per la classe [ArticlesDaoFirebirdProvider]:

  • creiamo la cartella [tests] (a destra) nella cartella Visual Studio del progetto [dao-firebird-provider] copiando la cartella [bin] dal progetto di test del livello [dao-odbc] (a sinistra):
  • Nella cartella [tests], sostituiamo la DLL [webarticles-dao.dll] con la DLL [webarticles-dao.dll] generata 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 con quattro argomenti
  • il computer host del DBMS - riga 9
  • il percorso del database Firebird - riga 12
  • il login dell'utente che si connette - riga 15
  • la sua password - riga 18

2.5.4.2. Test

La tabella [ARTICLES] nell'origine dati è popolata con i seguenti elementi (utilizzare IBExpert):

Image

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

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] inizialmente assegnato alla classe di test, è in realtà la classe [ArticlesDaoFirebirdProvider] che viene testata qui. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che abbiamo inserito nella tabella [ARTICLES]. Ora, eseguiamo tutti i test:

Image

I lettori che visualizzano questo documento sullo schermo vedranno che tutti i test sono stati superati (verde). Ciò che non possono vedere è che i test sono stati eseguiti in modo significativamente più veloce rispetto al database degli articoli a cui si accedeva tramite un driver ODBC nella nostra prima implementazione.

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

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

  • Nella cartella [bin], la DLL per il vecchio livello [dao] viene sostituita dalla DLL per il nuovo livello [dao] implementato dalla classe [ArticlesDaoFirebirdProvider]. Inseriamo lì anche la DLL richiesta da Firebird [FirebirdSql.Data.Firebird.dll]:

Image

  • In [runtime], il file di configurazione [web.config] viene sostituito con 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 il singleton [articlesDao] a un'istanza della nuova classe [ArticlesDaoFirebirdProvider]. Questa è l'unica modifica.

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

Image

Utilizzando un browser, inseriamo l'URL [http://localhost/webarticles/main.aspx]:

Image

Ora controlliamo il contenuto della tabella [ARTICLES]:

Image

Gli articoli [matita] e [blocco da 50 fogli] sono stati acquistati e le loro scorte sono state ridotte della quantità acquistata. L'articolo [penna stilografica] non ha potuto essere acquistato perché la quantità richiesta superava la quantità disponibile in magazzino. Invitiamo il lettore a eseguire 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] per la nostra applicazione [webarticles]. In ogni caso, siamo stati in grado di integrare il nuovo livello [dao] nell'applicazione [webarticles] senza ricompilare gli altri due livelli, [web] e [domain]. Questo risultato è stato ottenuto, come promemoria, attraverso due scelte architetturali:

  • accesso ai livelli tramite interfacce
  • integrazione dei livelli utilizzando Spring

Vogliamo fare un ulteriore passo avanti. Sebbene diverse, le nostre quattro implementazioni del livello [dao] presentano sorprendenti somiglianze. Una volta scritta la prima implementazione, le altre tre sono state create quasi interamente tramite copia-incolla e sostituzione di alcune parole chiave con altre. La logica, tuttavia, è rimasta invariata. Ci si potrebbe chiedere se fosse possibile avere un'unica implementazione che ci liberasse dai vari metodi di accesso ai dati. Ne abbiamo utilizzati quattro:

  • accesso tramite un driver ODBC a un'origine dati ODBC
  • accesso diretto a un database SQL Server
  • accesso tramite un driver Ole DB a un'origine 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 dei dati. L'accesso ai dati avviene tramite:

  • file di configurazione contenenti informazioni che definiscono la fonte dati e le operazioni da eseguire su di essa
  • una libreria di classi che utilizza 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 sembra presentare alcuni bug (opinione personale che richiederebbe una verifica approfondita). Tuttavia, dato che lo strumento ha dato prova di sé sulla piattaforma Java, sembra valga la pena presentare la versione .NET.

2.5.6.2. Dove posso trovare l' e di IBATIS SqlMap?

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

Image

Selezionare il link [Stable Binaries], che rimanda a [SourceForge.net]. Seguire la procedura di download fino al completamento. Si riceverà 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 un riferimento a ciascuno di questi file al progetto

2.5.6.3. File di configurazione di iBatis SqlMap

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

  1. providers.config: definisce le librerie di classi da utilizzare per l'accesso ai dati
  2. sqlmap.config: definisce le impostazioni di connessione
  3. File di mappatura: definiscono le operazioni da eseguire sui dati

La logica alla base 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. Avremo anche bisogno di un oggetto [Command] per eseguire le query SQL: OdbcCommand, SqlCommand, OleDbCommand, FbCommand, ecc. Nel file [providers.config], definiamo tutte le classi di cui abbiamo bisogno.
  • Il file [sqlmap.config] definisce essenzialmente la stringa di connessione al database contenente i dati. La connessione al database verrà aperta istanziando la 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 nelle tabelle di dati e le classi .NET, le cui istanze conterranno queste righe
    • le operazioni SQL da eseguire. Queste sono identificate da un nome. Il codice .NET esegue queste operazioni tramite i loro nomi, il che elimina tutto il codice SQL dal codice .NET.

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

Esaminiamo la natura esatta dei file di configurazione di SqlMap utilizzando un esempio. Prenderemo in considerazione il caso in cui l'origine dati sia l'origine ODBC Firebird della sezione 2.3.3.1.

2.5.6.4.1. providers.config

Il file [providers.config] per una sorgente 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>

Commenti:

  • Il pacchetto [SqlMap] include un file [providers.config] che offre diversi provider standard. Il codice sopra riportato è tratto direttamente da questo file.
  • Un <provider> ha un nome — riga 6 — che può essere qualsiasi cosa
  • Un <provider> può essere abilitato [enabled=true] o disabilitato [enabled=false]. Se abilitato, la 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 contenente le classi definite alle righe 9-15
  • riga 9 - classe da utilizzare per creare una connessione
  • riga 10 - classe da utilizzare per creare un oggetto [Command] per l'esecuzione di comandi SQL
  • riga 11 - classe da utilizzare per la gestione dei parametri di un comando SQL parametrizzato
  • riga 12 - classe di enumerazione dei possibili tipi di dati per i campi della tabella
  • riga 13 - nome della proprietà di un oggetto [Parameter] che contiene il tipo del valore di questo parametro
  • riga 14 - nome della classe [Adapter] utilizzata per creare oggetti [DataSet] dall'origine dati
  • riga 15 - nome della classe [CommandBuilder] che, quando associata a un oggetto [Adapter], genera automaticamente le sue proprietà [InsertCommand, DeleteCommand, UpdateCommand] dalla sua proprietà [SelectCommand]
  • righe 16–19 – definiscono come vengono gestiti i comandi SQL parametrizzati. A seconda della situazione, potresti 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 loro valori effettivi devono essere forniti nell'ordine dei parametri formali. Nel secondo caso, si tratta di parametri denominati. A un parametro di questo tipo viene fornito un valore specificandone il nome. L'ordine non ha più importanza.

  • Riga 16 - indica che le sorgenti ODBC utilizzano parametri posizionali
  • Righe 17–19 – riguardano i parametri con nome. Qui 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 sorgente ODBC. Non specifica alcuna sorgente ODBC. Il file [sqlmap.config] lo fa:

<?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>
     <!-- ==== data source ========= -->    
    <database>
        <provider name="${provider}"/>
        <dataSource name="sqlmaparticles"  connectionString="${connectionString}"/>
        <transactionManager type="ADO/SWC" />
    </database>
    <sqlMaps>
        <sqlMap resource="articles.xml" />
    </sqlMaps>
</sqlMapConfig>

Commenti:

  • Riga 3 - Definiamo un file di proprietà [properties.xml]. Questo file definisce coppie chiave-valore. Le chiavi possono essere qualsiasi cosa. Il valore associato a una chiave C viene ottenuto utilizzando la notazione ${C} in [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 - viene definita la chiave [connectionString]. Il suo valore è la stringa di connessione da utilizzare per aprire una connessione all'origine dati ODBC di Firebird.

  • righe 4-7 - parametri di configurazione:
    • riga 5 - le query SQL saranno identificate da un nome che può a sua volta far parte di uno spazio dei nomi. [useStatementNamespaces="false"] indica che gli spazi dei nomi non saranno utilizzati.
    • riga 6 - SqlMap dispone di varie strategie di caching per ridurre al minimo l'accesso all'origine dati. [cacheModelsEnabled="false"] indica che non ne verrà utilizzata nessuna.
  • Righe 9–13 – Vengono definite le proprietà dell'origine dati:
    • riga 10 - nome del <provider> da [providers.config] da utilizzare
    • Riga 11 - Stringa di connessione alla fonte dati
    • riga 12 - gestore delle transazioni. Non l'abbiamo utilizzato in questo caso, ma abbiamo lasciato la riga perché era presente nel file di distribuzione standard.
  • righe 14-16 – elenco dei file che definiscono le operazioni SQL da eseguire sull'origine dati.
  • riga 15 - definisce il file di mappatura [articles.xml]
2.5.6.4.3. articles.xml

Questo file ha due scopi:

  • Definire una mappatura degli oggetti per le tabelle dell'origine dati. Nei casi più semplici, ciò equivale ad associare una classe a una riga in una tabella.
  • definire operazioni SQL parametrizzate e assegnare loro un nome.

Useremo 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">
     <!-- the 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>
     <!-- SQL queries -->
    <statements>
         <!-- get all articles -->
        <select id="getAllArticles" resultMap="article">
                select ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM FROM ARTICLES
            </select> 
         <!-- delete all items-->
        <delete id="clearAllArticles">
                delete from ARTICLES
            </delete> 
         <!-- insert article -->
        <insert id="insertArticle" parameterClass="istia.st.articles.dao.Article">
                insert into ARTICLES (id, nom, prix,stockactuel, stockminimum) values
                ( #id# , #nom# , #prix# , #stockactuel# , #stockminimum# )
            </insert>
         <!-- article deletion -->
        <delete id="deleteArticle" parameterClass="int">
                delete FROM ARTICLES where ID= #value#
            </delete>
         <!-- article modification -->
        <update id="modifyArticle" parameterClass="istia.st.articles.dao.Article">
                update ARTICLES set NOM= #nom# ,PRIX= #prix# ,STOCKACTUEL= #stockactuel# ,STOCKMINIMUM= #stockminimum# where ID= #id#
            </update>
         <!-- search for a specific item -->
        <select id="getArticleById" resultMap="article" parameterClass="int">
                select ID, NOM, PRIX,    STOCKACTUEL, STOCKMINIMUM FROM ARTICLES where ID= #value#
            </select>
         <!-- item stock change -->
        <update id="changerStockArticle" parameterClass="Hashtable">
                update ARTICLES set STOCKACTUEL=(STOCKACTUEL + #mouvement#) where ID=#id# and ((STOCKACTUEL + #mouvement#) >=0)
      </update>
    </statements>
</sqlMap>

Commenti:

  • Righe 4-11 - Definiamo una mappatura tra una riga della tabella [ARTICLES] dell'origine dati e la classe [istia.st.articles.dao.Article]. Ogni colonna della tabella è associata a una proprietà della classe [Article]. Questa mappatura consente a [SqlMap] di costruire il risultato di un'operazione SQL SELECT. Ogni riga risultante dal SELECT verrà inserita in un oggetto [Article] secondo le regole di mappatura.
  • Riga 5 - La mappatura è racchiusa in un tag <resultMap> e viene denominata utilizzando l'attributo [id="article"]. La classe associata è specificata dall'attributo [class="istia.st.articles.dao.Article"].
  • Righe 14–44 – Vengono definite le operazioni SQL richieste
  • Righe 16–18 – Viene definita un'operazione SELECT denominata [getAllArticles]
    • riga 16 - l'operazione SELECT è denominata [name="getAllArticles"] e la mappatura da utilizzare è definita dall'attributo [resultMap="article"]. Ciò fa riferimento alla mappatura nelle righe 5–11
    • riga 17 – testo del comando SQL da eseguire
  • Righe 20–22 – Definiamo il comando SQL-Delete [clearAllArticles] per svuotare la tabella degli articoli.
  • Righe 24–27 – Definiamo il comando SQL-Insert [insertArticle] per aggiungere un nuovo articolo alla tabella degli articoli. Si tratta di una query parametrizzata che utilizza gli elementi (#id#, #name#, #price#, #currentStock#, #minStock#). I valori per 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, name, price, currentStock, minimumStock) a cui fa riferimento il comando SQL parametrizzato.
  • righe 29-31 - definiamo il comando SQL Delete [deleteArticle] destinato a cancellare un articolo il cui numero #value# è noto. Questo numero verrà passato come parametro: [parameterClass="int"]. Questa è una regola generale. Quando il parametro è univoco, viene fatto riferimento ad esso tramite la parola chiave #value# nel testo del comando SQL.
  • Righe 33–35 – Definiamo il comando SQL-Update [modifyArticle] per modificare un articolo il cui numero è noto. Come per il comando [insertArticle], le cinque informazioni richieste proverranno dalle proprietà di un oggetto [istia.st.articles.dao.Article].
  • Righe 37-39 – Definiamo il comando SQL-Select [getArticleById], che recupera il record di un articolo il cui numero è noto.
  • Righe 41–43 – Definiamo il comando SQL-Update [changerStockArticle], che modifica il campo [stockactuel] di un articolo il cui numero è noto. Le due informazioni richieste – l’#id# dell’articolo e l’incremento di stock #mouvement# – si trovano in un dizionario: [parameterClass="Hashtable"]. Questo dizionario deve avere due chiavi: id e mouvement. I valori associati a queste due chiavi saranno utilizzati nel comando SQL.
2.5.6.4.4. Posizione dei file di configurazione

Prenderemo in considerazione due diversi scenari:

  • Nel caso di un test Nunit, i file di configurazione [SqlMap] saranno collocati nella stessa cartella dei binari testati.
  • Nel caso di un'applicazione web, saranno collocati nella radice dell'applicazione.

2.5.6.5. L'API SqlMap

Le classi SqlMap sono contenute in DLL che in genere si trovano nella cartella [bin] dell'applicazione:

Image

Le applicazioni che utilizzano le classi 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 nel namespace [IBatisNet.DataMapper]. Il singleton viene ottenuto come segue:

        Dim mappeur As SqlMapper = Mapper.Instance

Per eseguire il comando SqlMap [getAllArticles], scriviamo:

                    dim articles as IList=mappeur.QueryForList("getAllArticles", Nothing)
  • Il metodo [QueryForList] restituisce il risultato di un comando SELECT sotto forma di elenco
  • Il primo parametro è il nome del comando SQL da eseguire (vedi articles.xml)
  • Il secondo parametro è il parametro da passare alla query SQL. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], abbiamo [parameterClass=Nothing]. Pertanto, qui passiamo un puntatore nullo.
  • Il risultato è di tipo IList. Gli oggetti in questo elenco sono specificati dall'attributo [resultMap] del comando SQL-select: [resultMap="article"]. "article" è un nome di mappatura:
<resultMap id="article" class="istia.st.articles.dao.Article">

La classe associata a questa mappatura è [istia.st.articles.dao.Article]. In definitiva, la variabile [articles] definita sopra è un elenco di oggetti [istia.st.articles.dao.Article]. Abbiamo quindi recuperato l'intera tabella [ARTICLES] in un'unica istruzione. Se la tabella [ARTICLES] è vuota, otteniamo un oggetto [IList] con 0 elementi.

Per eseguire il comando SqlMap [getArticleById], scriviamo:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • Il metodo [QueryForObject] recupera 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 passare alla query SQL. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], abbiamo [parameterClass="int"]. Pertanto, qui passiamo un numero intero che rappresenta l'ID dell'articolo che si sta cercando.
  • Il risultato è di tipo Object. Se il comando SELECT non ha restituito alcuna riga, il risultato è un puntatore nullo (nulla).

Per eseguire il comando SqlMap [insertArticle], scriviamo:

                    mappeur.Insert("insertArticle", unArticle)
  • Il metodo [Insert] consente di eseguire comandi SQL INSERT
  • Il primo parametro è il nome del comando SqlMap da eseguire
  • Il secondo parametro è il parametro da passare al comando. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], abbiamo [parameterClass="istia.st.articles.dao.Article"]. Pertanto, qui passiamo un oggetto di tipo [istia.st.articles.dao.Article].

Per eseguire il comando SqlMap [deleteArticle], scriviamo:

                    dim nbArticles as Integer=mappeur.Delete("deleteArticle", idArticle)
  • Il metodo [Delete] consente di eseguire comandi SQL DELETE
  • Il primo parametro è il nome del comando SQL da eseguire
  • Il secondo parametro è il parametro da passare al comando. Deve corrispondere all'attributo [parameterClass] del comando SqlMap. In [articles.xml], abbiamo [parameterClass="int"]. Pertanto, qui passiamo l'ID dell'articolo da eliminare.
  • Il risultato del metodo [Delete] è il numero di righe eliminate

Allo stesso modo, per eseguire il comando SqlMap [clearAllArticles], scriviamo:

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

Per eseguire il comando SqlMap [modifyArticle], scriviamo:

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

Allo stesso modo, per eseguire il comando SqlMap [changerStockArticle], scriveremmo:

                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                    ' update
                    dim nbLignes as Integer= mappeur.Update("changerStockArticle", paramètres)
  • Il secondo parametro corrisponde all'attributo [parameterClass] del comando SqlMap. In [articles.xml], abbiamo [parameterClass="Hashtable"]. Il comando SQL parametrizzato [changeItemStock] utilizza i parametri [id, movement]. Pertanto, qui passiamo un dizionario con queste due chiavi.

2.5.6.6. Il codice per la classe [ArticlesDaoSqlMap]

Seguendo le 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

        ' private fields
        Dim mappeur As SqlMapper = Mapper.Instance

        ' list of all items
        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

        ' add an item
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            SyncLock Me
                Try
                    ' unArticle : item to add
                    ' insertion
                    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

        ' deletes an article
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                    ' id : id of item to be deleted
                    ' delete
                    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

        ' modify an article
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                    ' update
                    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

        ' article search
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            SyncLock Me
                Try
                    ' id: id of the item searched for
                    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

        ' delete all items
        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

        ' change the stock of an item
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                    ' id: id of the item whose stock is being changed
                    ' movement: stock movement
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                    ' update
                    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 invitano i lettori a rivedere questo codice alla luce delle spiegazioni fornite per l'API SqlMap. Vale la pena notare che l'uso di [SqlMap] ha ridotto significativamente la quantità di codice richiesta.

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" richiesti da SqlMap nei riferimenti del progetto. Queste DLL sono state collocate nella cartella [bin] del progetto. Il progetto è configurato per generare una DLL denominata [webarticles-dao.dll]:

2.5.6.8. Test NUnit per il livello [dao]

2.5.6.8.1. La classe di test NUnit

La classe di test NUnit per la classe di implementazione [ArticlesDaoSqlMap] è la stessa di quella per la classe [ArticlesDaoPlainODBC] (vedere la sezione 2.3.3.2). Seguiamo un approccio simile per preparare il test NUnit per la classe [ArticlesDaoSqlMap]:

  • creiamo la cartella [test1] (a destra) nella cartella Visual Studio del progetto [dao-sqlmap] copiando la cartella [tests] dal progetto [dao-odbc] (a sinistra):
  • Nella cartella [tests], sostituiamo la DLL [webarticles-dao.dll] con la DLL [webarticles-dao.dll] generata dal progetto [dao-sqlmap].
  • Aggiungiamo le DLL richieste da SqlMap e i file di configurazione menzionati [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:

  • 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. Test

La tabella [ARTICLES] nell'origine dati Firebird è popolata con i seguenti articoli:

Image

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

Image

Nonostante il nome [NUnitTestArticlesDaoArrayList] inizialmente assegnato alla classe di test, è in realtà la classe [ArticlesDaoSqlMap] che viene testata qui. Lo screenshot mostra che abbiamo recuperato correttamente gli articoli che abbiamo inserito nella tabella [ARTICLES]. Ora, eseguiamo tutti i test:

Image

I lettori che visualizzano questo documento sullo schermo vedranno che alcuni test sono stati superati (verde) mentre altri hanno dato esito negativo (rosso). I test che hanno dato esito negativo sono [testArticleAbsent] e [testChangerStockArticle]. Dopo un'approfondita analisi, sembra che le cause di questi fallimenti siano le seguenti:

  • In [testArticleAbsent], tentiamo di modificare un elemento che non esiste. A tal fine utilizziamo il metodo [modifieArticle], che restituisce il numero di righe modificate come 0 o 1. In questo caso, dovremmo ottenere 0. Invece, otteniamo un'eccezione di tipo [IBatisNet.Common.Exceptions.ConcurrentException].
  • In [changerStockArticle], c'è un'altra operazione di tipo [update]. Questa comporta la riduzione delle scorte di un importo superiore alle scorte attuali. Per farlo, si utilizza il metodo [changerStockArticle], che restituisce il numero di righe modificate come 0 o 1. Il comando SQL è stato scritto per impedire un aggiornamento (vedere il comando SQL "changerStockArticle" in articles.xml) che comporterebbe un livello di stock negativo. In questo caso, ci aspettiamo di ottenere 0 come risultato del metodo [changerStockArticle]. Anche in questo caso, otteniamo un'eccezione di tipo [IBatisNet.Common.Exceptions.ConcurrentException].

Esistono molte possibili cause di errore:

  1. il codice nella classe [ArticlesDaoSqlMap] è errato. Questo è possibile. Tuttavia, proviene da un porting di una classe Java che aveva funzionato correttamente con la versione Java di SqlMap.
  2. la versione .NET di SqlMap è difettosa
  3. il driver ODBC di Firebird è difettoso
  4. ...

In assenza di certezze, aggireremo il problema intercettando l'eccezione [IBatisNet.Common.Exceptions.ConcurrentException]. Il nuovo codice per la classe [ArticlesDaoSqlMap] diventa il seguente:

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

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

        ' private fields
        Dim mappeur As SqlMapper = Mapper.Instance

        ' list of all items
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
...
        End Function

        ' add an item
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

        ' deletes an article
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                    ' id : id of item to be deleted
                    ' delete
                    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

        ' modify an article
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                    ' update
                    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

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

        ' delete all items
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
....
        End Sub

        ' change the stock of an item
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                    ' id: id of the item whose stock is being changed
                    ' movement: stock movement
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                    ' update
                    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], viene restituito 0 come risultato, indicando così che nessuna riga è stata modificata o eliminata. Una volta fatto ciò, la DLL del progetto viene rigenerata, collocata nella cartella [test1] e i test NUnit vengono rieseguiti:

Image

Questa volta funziona. Ora lavoreremo con questa DLL.

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

2.5.6.9.1. Origine dati ODBC

Qui testiamo l'origine dati ODBC discussa nella sezione 2.3.3.1. Qui viene utilizzata tramite SqlMap.

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

  • Nella cartella [bin], la DLL per il vecchio livello [dao] viene sostituita dalla DLL per il nuovo livello [dao] implementato dalla classe [ArticlesDaoSqlMap]. Aggiungiamo le DLL necessarie per Firebird e SqlMap:

Image

  • in [runtime], inseriamo i file di configurazione di SqlMap [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:

  • Le righe 14 associano il singleton [articlesDao] a un'istanza della nuova classe [ArticlesDaoSqlMap]. Questa è l'unica modifica.

Siamo pronti per eseguire i test. Configureremo il server web [Cassini] come abbiamo fatto nei test precedenti. Popoleremo la tabella articles con i seguenti valori:

Image

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

Image

Ora controlliamo il contenuto della tabella [ARTICLES]:

Image

Gli articoli [coltello] e [cucchiaio] sono stati acquistati e le loro giacenze sono state ridotte della quantità acquistata. L'articolo [forchetta] non ha potuto essere acquistato perché la quantità richiesta superava la quantità disponibile in magazzino. Invitiamo il lettore a eseguire ulteriori test.

2.5.6.9.2. Origine dati MSDE

Qui stiamo testando l'origine dati MSDE discussa nella sezione 2.4.3.1. Viene utilizzata qui 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 [providers.config, properties.xml] cambiano. I file di configurazione [sqlmap.config, articles.xml] rimangono invariati.
  • 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 accedere alle origini dati di SQL Server. È incluso di default nel file modello [providers.config] distribuito con SqlMap.

  • Il file [properties.xml] definisce il <provider> per l'origine 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 il test. Il server web [Cassini] mantiene la sua configurazione abituale. Inizializziamo la tabella degli articoli nel sorgente MSDE utilizzando [EMS MS SQL Manager]:

Image

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

Image

Ora controlliamo il contenuto della tabella [ARTICLES] utilizzando [EMS MS SQL Manager]:

Image

Gli articoli [pallone da calcio] e [racchetta da tennis] sono stati acquistati e le loro giacenze sono state ridotte della quantità acquistata. L'articolo [pattini a rotelle] non ha potuto essere acquistato perché la quantità richiesta superava la quantità disponibile in magazzino. Invitiamo il lettore a eseguire ulteriori test.

2.5.6.9.3. Origine dati OleDb

Qui stiamo testando l'origine dati ACCESS presentata nella sezione 2.4.5.1. Viene utilizzata qui 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 [providers.config, properties.xml] cambiano. I file di configurazione [sqlmap.config, articles.xml] rimangono invariati.
  • 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 accedere alle origini dati OleDb. È incluso per impostazione predefinita nel file modello [providers.config] distribuito con SqlMap.

  • Il file [properties.xml] definisce il <provider> della sorgente OleDb e la sua 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 il test. Il server web [Cassini] mantiene la sua configurazione abituale. Inizializziamo la tabella degli articoli dalla fonte ACCESS come segue:

Image

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

Image

Ora controlliamo il contenuto della tabella [ARTICLES] con:

Image

Gli articoli [pants] e [skirt] sono stati acquistati e le loro scorte sono state ridotte della quantità acquistata. L'articolo [coat] non ha potuto essere acquistato perché la quantità richiesta superava quella disponibile in magazzino. Invitiamo il lettore a eseguire 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 per le sorgenti SQL Server
    3. utilizzando classi di accesso .NET per 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, dominio, DAO] senza ricompilare nessuno dei livelli [web, dominio]
  • 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 al codice. Pertanto, con questo nuovo livello, siamo stati in grado di utilizzare successivamente le fonti di dati delle precedenti implementazioni da 1 a 3. Ciò è stato fatto in modo trasparente utilizzando file di configurazione.
  • Abbiamo dimostrato la grande flessibilità che gli strumenti Spring e SqlMap apportano alle applicazioni web a tre livelli.