Skip to content

2. Parte 2

2.1. Introdução

Começaremos por rever o que foi feito na Parte 1, em particular a arquitetura de três camadas [web, domínio, DAO] utilizada. Na solução proposta, a camada [DAO] era uma camada de teste: a fonte de dados foi implementada utilizando um objeto [ArrayList]. Neste artigo, iremos centrar-nos na camada [DAO], apresentando várias implementações possíveis da mesma quando os dados se encontram num SGBD.

Ferramentas utilizadas:

  • o SGBD Firebird — ver Apêndice, Secção 3.5.
  • o SGBD MSDE (Microsoft Data Engine) — ver Apêndice, Secção 3.12.
  • IBExpert, Edição Pessoal, para administração gráfica do SGBD Firebird — ver Apêndice, Secção 3.6.
  • EMS MS SQL Manager para administração gráfica do SGBD MSDE — ver Apêndice, Secção 3.14.
  • Ibatis SqlMap para a camada de acesso aos dados do SGBD — consulte a secção 2.5.6.2.

Numa escala iniciante-intermédio-avançado, este documento enquadra-se na categoria [intermédio-avançado]. A sua compreensão requer vários pré-requisitos. Alguns destes podem ser encontrados em documentos que escrevi. Nesses casos, faço referência aos mesmos. Escusado será dizer que isto é meramente uma sugestão e que o leitor é livre de utilizar os recursos da sua preferência.

2.2. Aplicação webarticles - Revisão

Apresentamos aqui os componentes da aplicação web de comércio eletrónico simplificada abordada na Parte 1. Esta aplicação permite aos utilizadores da web:

  • visualizar uma lista de artigos de uma base de dados
  • adicionar alguns deles a um carrinho de compras eletrónico
  • confirmar o carrinho. Esta confirmação atualiza simplesmente os níveis de inventário dos itens adquiridos na base de dados.

2.2.1. Visualizações da aplicação

As diferentes vistas apresentadas ao utilizador são as seguintes:

  • a vista [ERRORS], que apresenta quaisquer erros da aplicação

Image

2.2.2. Arquitetura geral da aplicação

A aplicação criada na Parte 1 tem uma arquitetura de três camadas:

  • As três camadas foram tornadas independentes através da utilização de interfaces
  • A integração das diferentes camadas foi implementada utilizando o Spring
  • Cada camada tem o seu próprio namespace: web (camada de interface do utilizador), domain (camada de negócios) e dao (camada de acesso a dados).

A aplicação segue uma arquitetura MVC (Model-View-Controller). Se nos referirmos ao diagrama em camadas acima, a arquitetura MVC encaixa-se nele da seguinte forma:

O processamento de um pedido do cliente segue estes passos:

  1. O cliente envia uma solicitação ao controlador. Este controlador é, neste caso, uma página .aspx que desempenha uma função específica. Ele lida com todas as solicitações do cliente. É o ponto de entrada da aplicação. É o C em MVC.
  2. O controlador processa esta solicitação. Para tal, pode necessitar da assistência da camada de negócios, conhecida como o M na estrutura MVC.
  3. O controlador recebe uma resposta da camada de negócios. A solicitação do cliente foi processada. Isso pode desencadear várias respostas possíveis. Um exemplo clássico é
    • uma página de erro, caso a solicitação não tenha sido processada corretamente
    • uma página de confirmação, caso contrário
  4. O controlador escolhe a resposta (= vista) a enviar ao cliente. Trata-se, na maioria das vezes, de uma página que contém elementos dinâmicos. O controlador fornece estes à vista.
  5. A vista é enviada ao cliente. Esta é o V em MVC.

2.2.3. O Modelo

O M em MVC consiste nos seguintes elementos:

  1. classes de negócio
  2. classes de acesso a dados
  3. a base de dados

2.2.3.1. A base de dados

A base de dados contém apenas uma tabela chamada ARTICLES, gerada utilizando os seguintes comandos 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
chave primária que identifica um item de forma única
nome
nome do item
preço
o seu preço
stock atual
stock atual
stock mínimo
o nível de stock abaixo do qual deve ser efetuada uma nova encomenda

2.2.3.2. Os namespaces do modelo

O Modelo M é fornecido na forma de dois espaços de nomes:

  • istia.st.articles.dao: contém as classes de acesso a dados da camada [dao]
  • istia.st.articles.domain: contém as classes de negócio da camada [domain]

Cada um destes namespaces está contido no seu próprio ficheiro «assembly»:

assembly
conteúdo
função
webarticles-dao
- [IArticlesDao]: a interface para aceder à camada [dao]. Esta é a única interface visível para a camada [domain]. Não vê outras.
- [Article]: classe que define um artigo
- [ArticlesDaoArrayList]: classe de implementação da interface [IArticlesDao] utilizando uma classe [ArrayList]
camada de acesso a dados - localizada inteiramente dentro da camada [dao] da arquitetura de três camadas da aplicação web
webarticles-domain
- [IArticlesDomain]: a interface para aceder à camada [domain]. É a única interface visível para a camada web. Não vê outras.
- [ArticlePurchases]: uma classe que implementa [IArticlesDomain]
- [Purchase]: uma classe que representa a compra de um cliente
- [Cart]: uma classe que representa o total de compras de um cliente
representa o modelo de compra web - reside inteiramente na camada [domain] da arquitetura de 3 camadas da aplicação web

2.2.4. Implantação e teste da aplicação [webarticles]

2.2.4.1. Implantação

Implementamos a aplicação desenvolvida na Parte 1 deste artigo numa pasta denominada [runtime]:

 

Comentários:

A pasta [runtime] contém três ficheiros e duas subpastas:

  • os controladores [global.asax] e [main.aspx]
  • o ficheiro de configuração [web.config]
  • a pasta [bin], que contém:
    • as DLLs para as três camadas [webarticles-dao.dll], [webarticles-domain.dll], [webarticles-web.dll]
    • os ficheiros necessários ao Spring [Spring-Core.*], [log4net.dll]
  • a pasta [views], que contém o código de apresentação para as várias vistas.
  • A presença de ficheiros de código .vb é desnecessária, uma vez que as suas versões compiladas se encontram nas DLLs.

2.2.4.2. Testes

Configuramos o servidor web [Cassini] da seguinte forma:

Image

com:

Caminho físico: D:\data\serge\work\2004-2005\aspnet\webarticles-010405\runtime\

Caminho virtual: /webarticles

Utilizando um navegador, solicitamos o URL [http://localhost/webarticles/main.aspx]

Image

Recorde-se que a camada [dao] é implementada por uma classe que armazena os artigos num objeto [ArrayList]. Esta classe cria uma lista inicial de quatro artigos. A partir da vista acima, utilizamos os links do menu para realizar operações. Aqui estão algumas delas. A coluna da esquerda representa o pedido do cliente e a coluna da direita representa a resposta enviada ao cliente.

2.2.5. A camada [DAO] revisitada

Na nossa primeira implementação da camada [dao], a interface de acesso a dados [IArticlesDao] foi implementada por uma classe que armazenava os itens num objeto [ArrayList]. Isto permitiu-nos evitar complicar excessivamente esta camada e demonstrar que apenas a sua interface era importante, e não a sua implementação. Conseguimos, assim, construir uma aplicação web funcional. Esta aplicação tem três camadas: [web], [domain] e [dao]. Aqui, iremos propor diferentes implementações da camada [dao]. Cada uma delas pode substituir a atual camada [dao] sem qualquer modificação nas camadas [domain] e [web]. Esta flexibilidade é alcançada porque:

  • a camada [domain] não se refere a uma classe concreta, mas a uma interface [IArticlesDao]
  • graças ao Spring, conseguimos ocultar o nome da classe que implementa a interface [IArticlesDao] da camada [domain].

2.2.5.1. Elementos da camada [dao]

Vamos rever alguns dos elementos da camada [dao] que serão mantidos nas novas implementações:

  • - [IArticlesDao]: a interface para aceder à camada [dao]
  • - [Article]: a classe que define um artigo

2.2.5.2. A classe [Article]

A classe que define um artigo é a seguinte:

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

Esta classe fornece:

  1. um construtor para definir as 5 informações de um item: [id, nome, preço, stockAtual, stockMínimo]
  2. propriedades públicas para ler e gravar as 5 informações.
  3. uma validação dos dados introduzidos para o item. Se os dados forem inválidos, é lançada uma exceção.
  4. um método toString que devolve o valor de um item como uma cadeia de caracteres. Isto é frequentemente útil para depurar uma aplicação.

2.2.5.3. A interface [IArticlesDao]

A interface [IArticlesDao] é definida da seguinte forma:

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

As funções dos vários métodos na interface são as seguintes:

getAllArticles
retorna todos os artigos da fonte de dados
clearAllArticles
limpa a fonte de dados
getArticleById
retorna o objeto [Article] identificado pelo seu número
addArticle
permite adicionar um artigo à fonte de dados
modifyArticle
permite modificar um artigo na fonte de dados
deleteItem
permite eliminar um item da fonte de dados
updateItemStock
permite modificar o stock de um item na fonte de dados

A interface fornece aos programas clientes vários métodos definidos exclusivamente pelas suas assinaturas. Não se preocupa com a forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua chamadas a uma interface, em vez de a uma implementação específica da mesma.

A escolha de uma implementação específica é feita através de um ficheiro de configuração do Spring.

2.3. A classe de implementação [ArticlesDaoPlainODBC]

Propomos uma nova implementação da camada [dao] que pressupõe que os dados se encontram numa fonte ODBC. Sabemos que, no Windows, praticamente todos os SGBDs disponíveis no mercado possuem um controlador ODBC. A vantagem desta solução é que é possível alternar entre SGBDs de forma transparente para a aplicação. A desvantagem é que um controlador ODBC que apenas explora funcionalidades comuns a todos os SGBDs é, geralmente, menos eficiente do que um controlador escrito especificamente para explorar todo o potencial de um SGBD específico. Consulte a Secção 3.7 para ver um exemplo de criação de uma fonte ODBC.

2.3.1. O código

2.3.1.1. O esqueleto

A classe [ArticlesDaoPlainODBC] implementa a interface [IArticlesDao] da seguinte forma:

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

Comentários:

  • Linha 3: importa o namespace que contém as classes .NET para aceder a fontes ODBC
  • Linha 11 - irá armazenar a ligação à fonte ODBC
  • Linha 12 - armazena o nome DSN da fonte de dados
  • Linhas 13–19: variáveis privadas do tipo [OdbcCommand] que definem as consultas SQL utilizadas pelos vários métodos da classe
  • Linhas 22–27: o construtor. Recebe os elementos necessários para construir o objeto [OdbcConnection] que ligará o código à fonte de dados ODBC
  • linhas 29–31 – o método para adicionar um item
  • linhas 33–35 – o método para alterar o stock de um item
  • linhas 37–39 – o método que elimina todos os itens da fonte de dados ODBC
  • linhas 41–43 – o método que recupera a lista de todos os itens da fonte ODBC
  • linhas 45–47 – o método que recupera um item específico
  • linhas 49-51 – o método que permite modificar determinados campos de um item cujo número se conhece
  • linhas 53-55 – o método que permite eliminar um item pelo seu número
  • linhas 57-60 – método utilitário para executar um [SELECT] na fonte de dados e devolver o resultado
  • linhas 62–64 – método utilitário para executar um [INSERT, UPDATE, DELETE] na fonte de dados e devolver o resultado

2.3.1.2. O

        ' 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

Comentários:

  • Linha 2 - O construtor recebe as três informações necessárias para se ligar a uma fonte ODBC: o nome DSN da fonte, o nome de utilizador para se ligar e a palavra-passe associada.
  • Linha 8 - O nome DSN da fonte é armazenado para que possa ser incluído nas mensagens de erro.
  • Linha 9 - O objeto [OdbcConnection] é instanciado. Uma conexão instanciada não é uma conexão aberta. O método [open] é usado para abrir a conexão.
  • Linhas 12–19 – Preparamos as consultas SQL em objetos [OdbcCommand]. Isto evita que tenhamos de as reconstruir sempre que precisarmos delas. Os parâmetros formais ? nas consultas serão substituídos por valores reais quando a consulta for executada.

2.3.1.3. O método 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

Comentários:

  • O método [executeQuery] é um método utilitário que:
    • executa uma consulta [SELECT id, name, price, currentStock, minimumStock from ARTICLES ...] na fonte de dados
    • retorna o resultado como uma lista de objetos [Article]
  • Linha 1 - O único parâmetro do método é o objeto [OdbcCommand] que contém a consulta [Select] a ser executada.
  • Linha 7 - A ligação é aberta. Será fechada na linha 29, independentemente de ter ocorrido um erro ou não.
  • Linha 9 - O objeto [OdbcDataReader] necessário para processar o resultado da consulta [Select] é instanciado
  • Linhas 13–23 – Cada linha resultante da consulta [Select] é colocada num objeto [Article], que é então adicionado aos outros artigos numa [ArrayList]
  • A lista de itens é devolvida na linha 25
  • Não são tratadas exceções. Estas devem ser tratadas pelo código que chama este método.

2.3.1.4. O método 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

Comentários:

  • O método recebe um objeto [OdbcCommand] que contém uma consulta SQL do tipo [Insert, Update, Delete].
  • A ligação é aberta na linha 5. Será fechada na linha 10, independentemente de ter ocorrido ou não uma exceção.
  • A consulta de atualização é executada na linha 7. O resultado — o número de linhas na tabela ARTICLES modificadas pela consulta — é imediatamente devolvido.

2.3.1.5. O método 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

Comentários:

  • Linha 1 - O método recebe o item a ser adicionado à fonte de dados ODBC. Ele retorna o número de linhas afetadas por esta operação, ou seja, 1 ou 0
  • Linhas 3 e 20 - O método é sincronizado. Este será o caso para todos os métodos de acesso a dados. Isto significa que apenas um thread de cada vez pode trabalhar na fonte de dados. Isto é provavelmente demasiado conservador. Existem alternativas melhores, nomeadamente incluir estas operações em transações. Neste caso, o SGBD gere o acesso simultâneo. Não quisemos introduzir o conceito de transações nesta fase. O Spring oferece-nos a opção de as introduzir na camada [domain]. Poderemos ter a oportunidade de revisitar este tema noutro artigo.
  • As linhas 5–12 atribuem valores aos parâmetros formais da consulta para o objeto [insertCommand] inicializado pelo construtor. Aqui está a consulta novamente:
insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)

Os 5 valores necessários para a consulta são fornecidos pelas linhas 7–11.

  • Linhas 13–19: A consulta é executada. Se for bem-sucedida, o resultado é devolvido. Caso contrário, é lançada uma exceção genérica com uma mensagem de erro explícita

2.3.1.6. O método 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

Comentários:

  • Linha 1 - O método recebe o item a ser modificado da fonte de dados ODBC. Ele retorna o número de linhas afetadas por esta operação, ou seja, 1 ou 0
  • Os comentários do método [addItem] podem ser incluídos aqui

2.3.1.7. O método 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

Comentários:

  • Linha 1 - O método recebe o ID do item a ser eliminado da fonte de dados ODBC. Devolve o número de linhas afetadas por esta operação, ou seja, 1 ou 0
  • Os comentários para o método [addItem] podem ser incluídos aqui

2.3.1.8. O método 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

Comentários:

  • Linha 1 - O método não recebe parâmetros. Ele retorna uma lista de todos os itens da fonte de dados ODBC
  • A consulta [Select] que recupera todos os registos é passada para o método [executeQuery] - linha 6
  • A lista resultante é devolvida na linha 8
  • As linhas 9–12 tratam de quaisquer exceções

2.3.1.9. O método 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

Comentários:

  • Linha 1 - O método recebe o ID do item pretendido como parâmetro. Devolve o item se este for encontrado na fonte ODBC; caso contrário, devolve a referência [nothing].
  • A consulta [Select] que solicita o item é inicializada nas linhas 5–8
  • e é executada na linha 12 - é obtida uma lista de itens
  • se esta lista estiver vazia, a referência [nothing] é devolvida na linha 14
  • Caso contrário, o único item da lista é apresentado na linha 16
  • As linhas 17–20 tratam de quaisquer exceções

2.3.1.10. O método 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

Comentários:

  • Linha 1 - O método não recebe parâmetros e não retorna nada
  • Linha 6 - A consulta para eliminar todos os itens é executada
  • Linhas 7–10: Tratar quaisquer exceções

2.3.1.11. O método 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

Comentários:

  • Linha 1 - O método recebe como parâmetros o número do item cujo stock precisa de ser modificado, bem como o incremento do stock (positivo ou negativo). Devolve o número de linhas modificadas pela operação, ou seja, 0 ou 1.
  • Linhas 5–10: A consulta [updateStockCommand] é inicializada. Aqui está o texto da consulta SQL:
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)

Note que o stock só é modificado se, após a alteração, permanecer >=0.

  • A consulta para atualizar o stock do artigo é executada na linha 13, e o resultado é devolvido
  • nas linhas 14–18, onde tratamos quaisquer exceções

2.3.2. Gerando a montagem da camada [DAO]

O projeto do Visual Studio para esta nova versão da camada [dao] tem a seguinte estrutura:

Image

O projeto está configurado para gerar uma DLL chamada [webarticles-dao.dll]:

2.3.3. Testes NUnit para a camada [dao]

2.3.3.1. Criação de uma fonte de dados ODBC-Firebird no

Para testar a nossa nova camada [DAO], precisamos de uma fonte de dados ODBC e, portanto, de uma base de dados. Estamos a utilizar o SGBD Firebird (Secção 3.5). Utilizando o IBExpert (Secção 3.6), criamos a seguinte base de dados de artigos:

O administrador desta base de dados será o utilizador [SYSDBA] com a palavra-passe [masterkey]. Criamos alguns registos:

Image

Criamos agora a seguinte fonte ODBC do Firebird (ver secção 3.7):

A fonte ODBC criada tem as seguintes características:

  • Nome do DSN: odbc-firebird-articles
  • ID de ligação: SYSDBA
  • senha associada: masterkey

2.3.3.2. A classe de teste NUnit para a camada [dao]

Já escrevemos uma classe de teste para a camada [dao] inicialmente construída. Se o leitor se lembra, esta classe testava não uma classe específica, mas a interface [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

....

Podemos ver que, no método <Setup()>, solicitamos ao Spring uma referência ao singleton denominado [articlesdao] do tipo [IArticlesDao], ou seja, do tipo de interface. O singleton [articlesdao] foi definido pelo seguinte ficheiro de configuração [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>

Vamos demonstrar que a classe de teste inicial nos permite testar a nossa nova camada [dao] sem modificações ou recompilação.

  • Na pasta do Visual Studio para a nossa nova camada [dao], crie a pasta [tests] (mostrada à direita abaixo) copiando a pasta [bin] do projeto de teste da camada [dao] inicial (mostrada à esquerda abaixo). Se necessário, convidamos o leitor a rever o projeto de teste da primeira versão da camada [dao] na primeira parte do artigo.
  • Na pasta [tests], substitua a DLL [webarticles-dao.dll] da antiga camada [dao] pela DLL [webarticles-dao.dll] da nova camada [dao]
  • Modifique o ficheiro de configuração [spring-config.xml] para instanciar a nova 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>

Comentários:

  • na linha 6, o objeto [articlesdao] está agora associado a uma instância da classe [ArticlesDaoPlainODBC]
  • Esta classe tem um construtor com três argumentos:
  • o nome da fonte DSN - linha 8
  • a identidade utilizada para aceder à base de dados - linha 11
  • a palavra-passe associada a esta identidade - linha 14

Aqui, estamos a utilizar as informações da fonte ODBC-Firebird que criámos anteriormente.

2.3.3.3. Testes

Estamos agora prontos para executar os testes. Utilizando a aplicação [Nunit-Gui], carregamos a DLL [test-webarticles-dao.dll] da pasta [tests] acima e executamos o teste [testGetAllArticles]:

Image

Olhando para a captura de ecrã acima, podemos arrepender-nos do nome [NUnitTestArticlesDaoArrayList] inicialmente atribuído à classe de teste. É confuso. Na verdade, é a classe [ArticlesDaoPlainODBC] que está a ser testada aqui. A captura de ecrã mostra que recuperámos corretamente os artigos que colocámos na tabela [ARTICLES]. Agora, vamos executar todos os testes:

Image

Na janela da esquerda, vemos a lista de métodos testados. A cor do ponto que precede o nome de cada método indica se o método passou (verde) ou falhou (vermelho). Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram bem-sucedidos.

2.3.3.4. Conclusão

Acabámos de demonstrar que:

  • porque a classe de teste NUnit não referenciava uma classe, mas sim uma interface;
  • porque o nome exato da classe que instanciava a interface foi fornecido num ficheiro de configuração e não no código;
  • porque o Spring tratou da instanciação da classe e forneceu uma referência à mesma no código de teste;

por isso, o código de teste escrito para a camada [dao] inicial permaneceu válido para uma nova implementação dessa mesma camada. Não precisámos de aceder ao código da classe de teste. Utilizámos apenas a sua versão compilada, aquela gerada durante o teste da camada [dao] inicial. Chegaremos a conclusões semelhantes quando chegar a altura de integrar a nova camada [dao] na aplicação [webarticles].

2.3.4. Integração da nova camada [dao] na aplicação [webarticles]

2.3.4.1. Testes de integração

Lembre-se de que a versão inicial da aplicação [webarticles] foi implementada na seguinte pasta [runtime]:

 

Recomenda-se aos leitores que consultem a Secção 2.2.4, que detalha os procedimentos de implementação da aplicação [webarticles]. Efetuaremos as seguintes alterações ao conteúdo da pasta [runtime]:

  • Na pasta [bin], a DLL da antiga camada [dao] é substituída pela DLL da nova camada [dao]
  • em [runtime], o ficheiro de configuração [web.config] é substituído por um ficheiro que tem em conta a nova classe de implementação da camada [dao]:

O novo ficheiro de configuração [web.config] é o seguinte:

<?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>

Comentários:

  • As linhas 14–24 associam o singleton [articlesDao] a uma instância da nova classe [ArticlesDaoPlainODBC]. Esta é a única alteração. Já nos deparámos com isto durante os testes da nova camada [dao].

Estamos prontos para os testes. Configuramos o servidor web [Cassini] da mesma forma que na secção 2.2.4. Inicializamos a tabela de produtos [Firebird] com os seguintes valores:

Image

Certifique-se de que o servidor web Cassini e o SGBD [Firebird] estão em execução. Utilizando um navegador, acedemos ao URL [http://localhost/webarticles/main.aspx]:

Image

Agora vamos verificar o conteúdo da tabela [ARTICLES] na base de dados [Firebird]:

Image

Os artigos [guarda-chuva] e [botas] foram comprados, e os seus níveis de stock foram reduzidos pela quantidade comprada. O artigo [chapéu] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.3.4.2. Conclusão

O que fizemos?

  • Revertemos para a versão de implementação da versão anterior;
  • substituímos a DLL da camada [dao] por uma nova versão. As DLLs das camadas [web] e [domain] permaneceram inalteradas;
  • modificámos o ficheiro de configuração [web.config] para que este tenha em conta a nova classe de implementação da camada [dao]

Tudo isto é simples e facilita muito a evolução da aplicação web. Estas características importantes são proporcionadas por duas escolhas arquitetónicas:

  • acesso às camadas através de interfaces
  • integração e configuração das camadas pelo Spring.

Estamos agora a propor uma nova implementação da camada [dao].

2.4. A classe de implementação [ArticlesDaoSqlServer]

A segunda implementação da camada [dao] pressupõe que os dados se encontram numa base de dados SQL Server. A Microsoft fornece um SGBD denominado MSDE, que é uma versão limitada do SQL Server. Consulte o apêndice para obter instruções sobre como o obter e instalar, secção 3.12.

2.4.1. O código

A classe [ArticlesDaoSqlServer] é muito semelhante à classe [ArticlesDaoPlainODBC] discutida anteriormente. Por isso, iremos apenas destacar as alterações feitas à versão anterior:

  • as classes necessárias estão no namespace [System.Data.SqlClient] em vez do namespace [System.Data.Odbc]
  • a ligação [OdbcConnection] é agora do tipo [SqlConnection]
  • os objetos [OdbcCommand] são agora do tipo [SqlCommand]
  • a sintaxe das consultas SQL parametrizadas mudou. A consulta de inserção tem agora o seguinte aspeto:
insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)

enquanto anteriormente era:

insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
  • O método [addItem] passa então a ser o seguinte:
        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
  • O construtor também é modificado:
        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

O construtor aceita agora quatro parâmetros:

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

O código completo para a classe [ArticlesDaoSqlServer] é o seguinte:

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

Recomenda-se ao leitor que analise este código à luz dos comentários sobre a classe [ArticlesDaoPlainODBC] apresentados anteriormente.

2.4.2. Gerar a compilação da camada [dao]

O novo projeto do Visual Studio tem a seguinte estrutura:

Image

O projeto está configurado para gerar uma DLL denominada [webarticles-dao.dll]:

2.4.3. Testes NUnit para a camada [dao]

2.4.3.1. Criação de uma fonte de dados do SQL Server

Para testar a nossa nova camada [dao], precisamos de uma fonte de dados do SQL Server e, por conseguinte, do SGBD SQL Server. Na prática, utilizaremos o SGBD MSDE (Microsoft Data Engine) (Secção 3.12), que é uma versão do SQL Server limitada apenas pelo número de utilizadores simultâneos que suporta. Utilizando o [EMS MS SQL Manager] (secção 3.14), criamos a seguinte base de dados de produtos numa instância do MSDE denominada [portable1_tahe\msde140405]:

Image

A base de dados pertence ao utilizador [mdparticles] com a palavra-passe [admarticles]. O comando Transact-SQL para criar a tabela [ARTICLES] é o seguinte:

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

Criamos alguns itens:

Image

2.4.3.2. A classe de teste NUnit

A classe de teste NUnit para a classe de implementação [ArticlesDaoSqlServer] é idêntica à da classe [ArticlesDaoPlainODBC] (ver secção 2.3.3.2). Seguimos uma abordagem semelhante para preparar o teste NUnit para a classe:

  • criamos a pasta [tests] (à direita) na pasta do Visual Studio do projeto [dao-sqlserver], copiando a pasta [tests] do projeto [dao-odbc] (à esquerda):
  • Na pasta [tests] do projeto [dao-sqlserver], substituímos a DLL [webarticles-dao.dll] pela DLL [webarticles-dao.dll] gerada pelo projeto [dao-sqlserver]
  • Modificamos o ficheiro de configuração [spring-config.xml] para instanciar a nova 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>

Comentários:

  • na linha 7, o objeto [articlesdao] está agora associado a uma instância da classe [ArticlesDaoSqlServer]
  • Esta classe possui um construtor com quatro argumentos:
  • o nome da instância MSDE utilizada - linha 9
  • o nome da base de dados - linha 12
  • a identidade utilizada para aceder à base de dados - linha 15
  • a palavra-passe associada a esta identidade - linha 18

Aqui estamos a utilizar as informações da fonte MSDE que criámos anteriormente.

2.4.3.3. Testes

Estamos prontos para executar os testes. Utilizando a aplicação [Nunit-Gui], carregamos a DLL [test-webarticles-dao.dll] da pasta [tests] acima e executamos o teste [testGetAllArticles]:

Image

Embora a classe de teste tenha sido inicialmente denominada [NUnitTestArticlesDaoArrayList] — um nome mantido porque estamos a utilizar a DLL [tests-webarticles-dao.dll] derivada desta classe —, é de facto a classe [ArticlesDaoSqlserver] que está a ser testada aqui. A captura de ecrã mostra que recuperámos corretamente os artigos que colocámos na tabela [ARTICLES]. Agora, vamos executar todos os testes:

Image

Na janela à esquerda, vemos a lista de métodos testados. A cor do ponto que precede o nome de cada método indica se o método passou (verde) ou falhou (vermelho). Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram bem-sucedidos.

2.4.4. Integrar a nova camada [dao] na aplicação [webarticles]

Seguimos o procedimento explicado na secção 2.3.4. Efetuamos as seguintes alterações ao conteúdo da pasta [runtime]:

  • Na pasta [bin], a DLL da antiga camada [dao] é substituída pela DLL da nova camada [dao] implementada pela classe [ArticlesDaoSqlServer]
  • Em [runtime], o ficheiro de configuração [web.config] é substituído por um ficheiro que tem em conta a nova classe de implementação:
<?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>

Comentários:

  • As linhas 15–33 associam o singleton [articlesDao] a uma instância da nova classe [ArticlesDaoSqlServer]. Esta é a única alteração. Já nos deparámos com isto durante os testes da nova camada [dao]

Estamos prontos para o teste. Mantemos a mesma configuração do servidor web [Cassini] de antes. Inicializamos a tabela de produtos [MSDE] com os seguintes valores:

Image

Certifique-se de que o servidor web Cassini e o SGBD MSDE (aqui, a instância portable1_tahe\msde140405) estão em execução. Utilizando um navegador, solicitamos a URL [http://localhost/webarticles/main.aspx]:

Image

Agora, vamos verificar o conteúdo da tabela [ARTICLES] na base de dados [MSDE]:

Image

Os artigos [bola de futebol] e [raquete de ténis] foram comprados, e os seus níveis de stock foram reduzidos pela quantidade comprada. O artigo [patins em linha] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.4.5. A classe de implementação [ArticlesDaoOleDb]

2.4.5.1. de fonte de dados OleDb

A terceira implementação da camada [dao] pressupõe que os dados se encontram numa base de dados acessível através de um controlador OleDb. O princípio subjacente às fontes OleDb é análogo ao das fontes ODBC. Um programa que utilize uma fonte OleDb fá-lo através de uma interface padrão comum a todas as fontes OleDb. Alterar a fonte OleDb implica simplesmente alterar o controlador OleDb. O código em si permanece inalterado.

Pode descobrir quais os controladores OleDb disponíveis no seu computador utilizando o Visual Studio:

  • exiba o Server Explorer selecionando [View/Server Explorer]:

Image

  • Para adicionar uma nova ligação, clique com o botão direito do rato em [Ligação de Dados] e selecione a opção [Adicionar Ligação]. Isto abre um assistente onde pode definir as definições de ligação:

Image

  • O painel [Provedor] lista os controladores OLEDB disponíveis. Para a nova camada [DAO], utilizaremos o controlador [Microsoft Jet 4.0 OLE DB Provider], que fornece acesso a bases de dados Access.
  • Vamos sair temporariamente do Visual Studio para criar a base de dados ACCESS [articles.mdb] com a seguinte tabela única:

Image

  • A estrutura da tabela é a seguinte:
id
numérico - inteiro - chave primária
nome
texto - 20 caracteres -
preço
numérico - duplo
stock atual
numérico - inteiro
stock mínimo
numérico - inteiro
  • Voltemos ao Visual Studio e criemos uma nova ligação, tal como explicado anteriormente:

Image

  • Selecionamos o controlador [Microsoft Jet 4.0] e vamos para o painel [Conexão]:

Image

  • Usando o botão [1], selecione a base de dados ACCESS que acabou de ser criada e, em seguida, conclua a configuração da ligação clicando no botão [Concluir]. A ligação que criou aparece agora na lista de ligações disponíveis:

Image

  • Ao clicar duas vezes na tabela [ARTICLES], temos acesso ao seu conteúdo:

Image

  • Pode então adicionar, modificar ou eliminar linhas na tabela.
  • No Explorador de Servidores, selecione a nova ligação para aceder à sua ficha Propriedades:

Image

  • É útil saber a cadeia de ligação. Vamos utilizá-la para nos ligarmos à base de dados:
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
  • Desta cadeia de caracteres, iremos reter apenas os seguintes elementos:
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;

2.4.5.2. O código da classe [ArticlesDaoOleDb]

A classe [ArticlesDaoOleDb] é muito semelhante à classe [ArticlesDaoPlainODBC] discutida anteriormente. Por isso, iremos apenas listar as alterações feitas em relação à versão anterior:

  • as classes necessárias estão no namespace [System.Data.OleDb] em vez do namespace [System.Data.Odbc]
  • a ligação [OdbcConnection] é agora do tipo [OleDbConnection]
  • os objetos [OdbcCommand] são agora do tipo [OleDbCommand]

O construtor da classe aceita um único parâmetro: a cadeia de ligação à base de dados:

        ' 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

O código completo para a classe [ArticlesDaoOleDb] é o seguinte:

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

Recomenda-se ao leitor que analise este código à luz dos comentários sobre a classe [ArticlesDaoPlainODBC] apresentados anteriormente.

2.4.5.3. Gerar a compilação da camada [dao]

O novo projeto do Visual Studio tem a seguinte estrutura:

Image

O projeto está configurado para gerar uma DLL denominada [webarticles-dao.dll]:

2.4.5.4. Testes NUnit para a camada [dao]

2.4.5.4.1. A classe de teste NUnit

A classe de teste NUnit para a classe de implementação [ArticlesDaoOleDb] é a mesma que a da classe [ArticlesDaoPlainODBC] (ver secção 2.3.3.2). Seguimos uma abordagem semelhante para preparar o teste NUnit para a classe:

  • criamos a pasta [tests] (à direita) na pasta do Visual Studio do projeto [dao-oledb], copiando a pasta [tests] do projeto [dao-odbc] (à esquerda):
  • Na pasta [tests] do projeto [dao-oledb], substituímos a DLL [webarticles-dao.dll] pela DLL [webarticles-dao.dll] gerada pelo projeto [dao-oledb]
  • Modificamos o ficheiro de configuração [spring-config.xml] para instanciar a nova 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>

Comentários:

  • na linha 7, o objeto [articlesdao] está agora associado a uma instância da classe [ArticlesDaoOleDb]
  • Esta classe tem um construtor com um argumento: a cadeia de ligação à base de dados OleDb ACCESS - linha 9
2.4.5.4.2. Testes

Estamos prontos para testar. Utilizando a aplicação [Nunit-Gui], carregamos a DLL [test-webarticles-dao.dll] da pasta [tests] acima e executamos o teste [testGetAllArticles]:

Image

Apesar do nome [NUnitTestArticlesDaoArrayList] inicialmente atribuído à classe de teste, é de facto a classe [ArticlesDaoOleDb] que está a ser testada aqui. A captura de ecrã mostra que recuperámos corretamente os artigos que colocámos na tabela [ARTICLES]. Agora, vamos executar todos os testes:

Image

Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram aprovados (cor verde).

2.4.5.5. Integrar a nova camada [dao] na aplicação [webarticles]

Seguimos o procedimento explicado na secção 2.3.4. Efetuamos as seguintes alterações ao conteúdo da pasta [runtime]:

  • Na pasta [bin], a DLL da antiga camada [dao] é substituída pela DLL da nova camada [dao] implementada pela classe [ArticlesDaoOleDb]
  • Em [runtime], o ficheiro de configuração [web.config] é substituído por um ficheiro que tem em conta a nova classe de implementação:
<?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>

Comentários:

  • As linhas 14–18 associam o singleton [articlesDao] a uma instância da nova classe [ArticlesDaoOleDb]. Esta é a única alteração.

Mantemos a mesma configuração do servidor web [Cassini] de antes. Inicializamos a tabela de produtos com os seguintes valores:

Image

Certifique-se de que a base de dados de artigos não está a ser utilizada por um programa como o Visual Studio ou o Access. Utilizando um navegador, solicitamos o URL [http://localhost/webarticles/main.aspx]:

Image

Agora, vamos verificar o conteúdo da tabela [ARTICLES] utilizando o Access:

Image

Os artigos [calças] e [saia] foram comprados e os seus níveis de stock foram reduzidos pela quantidade comprada. O artigo [casaco] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.5. A classe de implementação [ArticlesDaoFirebirdProvider]

2.5.1. O Firebird-net-provider

Anteriormente, utilizámos uma fonte de dados [Firebird] através de um controlador ODBC. Embora os controladores ODBC ofereçam elevada reutilização para o código que os utiliza, são menos eficientes do que os controladores escritos especificamente para o SGBD de destino. O SGBD [Firebird] pode ser utilizado através de uma biblioteca de classes específicas que pode ser descarregada a partir do site do Firebird [http://firebird.sourceforge.net/]. A página de downloads disponibiliza as seguintes ligações (abril de 2005):

Image

O link [firebird-net-provider] é o que deve ser utilizado para descarregar as classes .NET para aceder ao SGBD Firebird. A instalação do pacote cria uma pasta semelhante à seguinte:

Image

Dois itens são do nosso interesse:

  • [FirebirdSql.Data.Firebird.dll]: o assembly que contém as classes .NET para aceder ao SGBD Firebird
  • [FirebirdNETProviderSDK.chm]: a documentação para estas classes

Em seguida, para permitir que um projeto do Visual Studio utilize estas classes, faremos duas coisas:

  • Colocaremos o assembly [FirebirdSql.Data.Firebird.dll] na pasta [bin] do projeto
  • adicionar este mesmo assembly às referências do projeto

2.5.2. O código da classe [ArticlesDaoFirebirdProvider]

A classe [ArticlesDaoFirebirdProvider] é muito semelhante à classe [ArticlesDaoSqlServer] discutida anteriormente. Por isso, iremos apenas destacar as alterações feitas em comparação com essa versão:

  • As classes necessárias estão no namespace [FirebirdSql.Data.Firebird] em vez do namespace [System.Data.SqlClient]
  • A ligação [SqlConnection] é agora do tipo [FbConnection]
  • Os objetos [SqlCommand] são agora do tipo [FbCommand]
  • Os objetos [SqlParameter] são agora do tipo [FbParameter]

O construtor da classe aceita quatro parâmetros, que utiliza para construir a cadeia de ligação à base de dados:

        ' 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

O código completo para a classe [ArticlesDaoFirebirdProvider] é o seguinte:

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

Recomenda-se ao leitor que analise este código à luz dos comentários sobre a classe [ArticlesDaoSqlServer] apresentados anteriormente.

2.5.3. Gerar a compilação da camada [dao]

O novo projeto do Visual Studio tem a seguinte estrutura:

Image

Repare na presença do assembly [FirebirdSql.Data.Firebird.dll] nas referências do projeto. Esta DLL foi colocada na pasta [bin] do projeto. O projeto está configurado para gerar uma DLL denominada [webarticles-dao.dll]:

2.5.4. Testes NUnit para a camada [dao]

2.5.4.1. A classe de teste NUnit

A classe de teste NUnit para a classe de implementação [ArticlesDaoFirebirdProvider] é idêntica à da classe [ArticlesDaoPlainODBC] (ver secção 2.3.3.2). Seguimos uma abordagem semelhante para preparar o teste NUnit para a classe [ArticlesDaoFirebirdProvider]:

  • criamos a pasta [tests] (à direita) na pasta do Visual Studio do projeto [dao-firebird-provider] copiando a pasta [bin] do projeto de teste da camada [dao-odbc] (à esquerda):
  • Na pasta [tests], substituímos a DLL [webarticles-dao.dll] pela DLL [webarticles-dao.dll] gerada a partir do projeto [dao-firebird-provider]
  • Modificamos o ficheiro de configuração [spring-config.xml] para instanciar a nova 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>

Comentários:

  • na linha 7, o objeto [articlesdao] está agora associado a uma instância da classe [ArticlesDaoFirebirdProvider]
  • esta classe tem um construtor com quatro argumentos
  • a máquina anfitriã do SGBD - linha 9
  • o caminho para a base de dados Firebird - linha 12
  • o nome de utilizador do utilizador que se está a ligar - linha 15
  • a sua palavra-passe - linha 18

2.5.4.2. Testes

A tabela [ARTICLES] na fonte de dados é preenchida com os seguintes itens (utilize o IBExpert):

Image

Estamos prontos para executar os testes. Utilizando a aplicação [Nunit-Gui], carregamos a DLL [test-webarticles-dao.dll] da pasta [tests] acima e executamos o teste [testGetAllArticles]:

Image

Apesar do nome [NUnitTestArticlesDaoArrayList] inicialmente atribuído à classe de teste, é de facto a classe [ArticlesDaoFirebirdProvider] que está a ser testada aqui. A captura de ecrã mostra que recuperámos corretamente os artigos que colocámos na tabela [ARTICLES]. Agora, vamos executar todos os testes:

Image

Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram aprovados (verde). O que não conseguem ver é que os testes foram executados significativamente mais rápido do que com a base de dados de artigos acedida através de um controlador ODBC na nossa primeira implementação.

2.5.5. Integrar a nova camada [dao] na aplicação [webarticles]

Seguimos o procedimento já explicado duas vezes, nomeadamente na secção 2.3.4. Efetuamos as seguintes alterações ao conteúdo da pasta [runtime]:

  • Na pasta [bin], a DLL da antiga camada [dao] é substituída pela DLL da nova camada [dao] implementada pela classe [ArticlesDaoFirebirdProvider]. Colocamos também aí a DLL necessária para o Firebird [FirebirdSql.Data.Firebird.dll]:

Image

  • Em [runtime], o ficheiro de configuração [web.config] é substituído por um ficheiro que tem em conta a nova classe de implementação:
<?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>

Comentários:

  • As linhas 14–27 associam o singleton [articlesDao] a uma instância da nova classe [ArticlesDaoFirebirdProvider]. Esta é a única alteração.

Estamos prontos para o teste. Configuramos o servidor web [Cassini] tal como nos testes anteriores. Inicializamos a tabela articles com os seguintes valores:

Image

Usando um navegador, acedemos ao URL [http://localhost/webarticles/main.aspx]:

Image

Agora vamos verificar o conteúdo da tabela [ARTICLES]:

Image

Os artigos [lápis] e [bloco de 50 folhas] foram comprados e os seus níveis de stock foram reduzidos pela quantidade comprada. O artigo [caneta de tinta permanente] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.5.6. A classe de implementação [ArticlesDaoSqlMap]

2.5.6.1. O produto Ibatis SqlMap

Escrevemos quatro implementações diferentes da camada [dao] para a nossa aplicação [webarticles]. Em cada caso, conseguimos integrar a nova camada [dao] na aplicação [webarticles] sem recompilar as outras duas camadas, [web] e [domain]. Isto foi conseguido, como lembrete, através de duas escolhas arquitetónicas:

  • acesso às camadas através de interfaces
  • integração das camadas utilizando o Spring

Queremos levar isto um passo mais além. Embora diferentes, as nossas quatro implementações da camada [dao] partilham semelhanças marcantes. Depois de escrita a primeira implementação, as outras três foram criadas quase inteiramente através de copiar e colar e substituir certas palavras-chave por outras. A lógica, no entanto, permaneceu inalterada. Poder-se-ia questionar se seria possível ter uma única implementação que nos libertasse dos vários métodos de acesso aos dados. Utilizámos quatro:

  • acesso através de um controlador ODBC a uma fonte de dados ODBC
  • acesso direto a uma base de dados SQL Server
  • acesso através de um controlador Ole DB a uma fonte de dados Ole DB
  • acesso direto a uma base de dados Firebird

A ferramenta Ibatis SqlMap [[http://www.ibatis.com/]] permite o desenvolvimento de camadas de acesso a dados independentes da natureza concreta da fonte de dados. O acesso aos dados é assegurado através de:

  • ficheiros de configuração que contêm informações que definem a fonte de dados e as operações a realizar na mesma
  • uma biblioteca de classes que utiliza estas informações para aceder aos dados

A ferramenta Ibatis SqlMap foi inicialmente desenvolvida para a plataforma Java. A sua portabilidade para a plataforma .NET é recente e parece apresentar alguns erros (opinião pessoal que exigiria uma verificação aprofundada). No entanto, uma vez que a ferramenta já deu provas da sua eficácia na plataforma Java, parece valer a pena apresentar a versão .NET.

2.5.6.2. Onde posso encontrar o IBATIS SqlMap ?

O site principal do Firebird é [http://www.ibatis.com/]. A página de downloads oferece os seguintes links:

Image

Selecione o link [Stable Binaries], que o levará para [SourceForge.net]. Siga o processo de download até ao fim. Receberá um ficheiro ZIP contendo os seguintes ficheiros:

Image

Num projeto do Visual Studio que utilize o iBatis SqlMap, é necessário fazer duas coisas:

  • colocar os ficheiros acima referidos na pasta [bin] do projeto
  • adicionar uma referência a cada um destes ficheiros ao projeto

2.5.6.3. Ficheiros de configuração do iBatis SqlMap

Uma fonte de dados [SqlMap] será definida utilizando os seguintes ficheiros de configuração:

  1. providers.config: define as bibliotecas de classes a utilizar para aceder aos dados
  2. sqlmap.config: define as configurações de ligação
  3. Ficheiros de mapeamento: definem as operações a realizar nos dados

A lógica por trás destes ficheiros é a seguinte:

  • Para aceder aos dados, precisaremos de uma ligação. Para representar isto, já nos deparámos com várias classes: OdbcConnection, SqlConnection, OleDbConnection, FbConnection. Também precisaremos de um objeto [Command] para emitir consultas SQL: OdbcCommand, SqlCommand, OleDbCommand, FbCommand, etc. No ficheiro [providers.config], definimos todas as classes de que precisamos.
  • O ficheiro [sqlmap.config] define essencialmente a cadeia de ligação à base de dados que contém os dados. A ligação à base de dados será aberta através da instanciação da classe [Connection] definida em [providers.config], cujo construtor receberá a cadeia de ligação definida em [sqlmap.config].
  • Os ficheiros de mapeamento definem:
    • associações entre linhas nas tabelas de dados e classes .NET, cujas instâncias conterão essas linhas
    • as operações SQL a serem executadas. Estas são identificadas por um nome. O código .NET executa estas operações através dos seus nomes, o que elimina todo o código SQL do código .NET.

2.5.6.4. Os ficheiros de configuração para o projeto [dao-sqlmap]

Vamos examinar a natureza exata dos ficheiros de configuração do SqlMap usando um exemplo. Consideraremos o caso em que a fonte de dados é a fonte ODBC do Firebird da secção 2.3.3.1.

2.5.6.4.1. providers.config

O ficheiro [providers.config] para uma fonte ODBC é o seguinte:

<?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>

Comentários:

  • Um ficheiro [providers.config] está incluído no pacote [SqlMap]. Este oferece vários fornecedores padrão. O código acima foi retirado diretamente deste ficheiro.
  • Um <provider> tem um nome — linha 6 — que pode ser qualquer coisa
  • Um <provider> pode ser ativado [enabled=true] ou desativado [enabled=false]. Se estiver ativado, a DLL referenciada na linha 8 deve estar acessível. Um ficheiro [providers.config] pode conter várias tags <provider>.
  • linha 8 - nome do assembly que contém as classes definidas nas linhas 9-15
  • linha 9 - classe a utilizar para criar uma ligação
  • linha 10 - classe a utilizar para criar um objeto [Command] para a execução de comandos SQL
  • linha 11 - classe a utilizar para gerir os parâmetros de um comando SQL parametrizado
  • linha 12 - classe de enumeração dos tipos de dados possíveis para os campos da tabela
  • linha 13 - nome da propriedade de um objeto [Parameter] que contém o tipo do valor deste parâmetro
  • linha 14 - nome da classe [Adapter] utilizada para criar objetos [DataSet] a partir da fonte de dados
  • linha 15 - nome da classe [CommandBuilder] que, quando associada a um objeto [Adapter], gera automaticamente as suas propriedades [InsertCommand, DeleteCommand, UpdateCommand] a partir da sua propriedade [SelectCommand]
  • linhas 16–19 – definem como os comandos SQL parametrizados são tratados. Dependendo da situação, pode escrever, por exemplo:
insert into ARTICLES(id,nom,prix,stockactuel,stockminimum) values (?,?,?,?,?)

ou

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

No primeiro caso, trata-se de parâmetros posicionais formais. Os seus valores reais devem ser fornecidos na ordem dos parâmetros formais. No segundo caso, trata-se de parâmetros nomeados. Um valor é fornecido para um parâmetro deste tipo especificando o seu nome. A ordem já não importa.

  • Linha 16 – indica que as fontes ODBC utilizam parâmetros posicionais
  • Linhas 17–19 – dizem respeito a parâmetros nomeados. Não há nenhum aqui.

Esta informação permite ao SqlMap saber, por exemplo, qual a classe que deve instanciar para criar uma ligação. Aqui, será a classe [OdbcConnection] (linha 9).

2.5.6.4.2. sqlmap.config

O ficheiro [providers.config] define as classes a utilizar para aceder a uma fonte ODBC. Não especifica quaisquer fontes ODBC. O ficheiro [sqlmap.config] faz isso:

<?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>

Comentários:

  • Linha 3 - Definimos um ficheiro de propriedades [properties.xml]. Este ficheiro define pares chave-valor. As chaves podem ser qualquer coisa. O valor associado a uma chave C é obtido utilizando a notação ${C} em [sqlmap.config]. Aqui está o ficheiro [properties.xml] que será associado ao ficheiro [sqlmap.config] anterior:
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>

Linha 3 - a chave [provider] é definida. O seu valor é o nome da tag <provider> a ser utilizada em [providers.config]

linha 4 - a chave [connectionString] é definida. O seu valor é a cadeia de ligação a utilizar para estabelecer uma ligação à fonte de dados ODBC do Firebird.

  • linhas 4-7 - parâmetros de configuração:
    • linha 5 - as consultas SQL serão identificadas por um nome que pode, por sua vez, fazer parte de um namespace. [useStatementNamespaces="false"] indica que os namespaces não serão utilizados.
    • linha 6 - O SqlMap possui várias estratégias de cache para minimizar o acesso à fonte de dados. [cacheModelsEnabled="false"] indica que nenhuma será utilizada.
  • Linhas 9–13 – As propriedades da fonte de dados são definidas:
    • linha 10 - nome do <provider> de [providers.config] a utilizar
    • Linha 11 - Cadeia de ligação à fonte de dados
    • linha 12 - gestor de transações. Não o utilizámos aqui, mas mantivemos a linha porque estava no ficheiro de distribuição padrão.
  • linhas 14-16 – lista de ficheiros que definem as operações SQL a serem executadas na fonte de dados.
  • linha 15 - define o ficheiro de mapeamento [articles.xml]
2.5.6.4.3. articles.xml

Este ficheiro tem duas finalidades:

  • Definir um mapeamento de objetos para as tabelas da fonte de dados. Nos casos mais simples, isto equivale a associar uma classe a uma linha numa tabela.
  • Definir operações SQL parametrizadas e nomeá-las.

Iremos utilizar o seguinte ficheiro [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>

Comentários:

  • Linhas 4-11 - Definimos um mapeamento entre uma linha na tabela [ARTICLES] da fonte de dados e a classe [istia.st.articles.dao.Article]. Cada coluna da tabela está associada a uma propriedade da classe [Article]. Este mapeamento permite que o [SqlMap] construa o resultado de uma operação SQL SELECT. Cada linha de resultado da SELECT será colocada num objeto [Article] de acordo com as regras de mapeamento.
  • Linha 5 - O mapeamento está entre as tags <resultMap> e é nomeado através do atributo [id="article"]. A classe associada é especificada pelo atributo [class="istia.st.articles.dao.Article"].
  • Linhas 14–44 – As operações SQL necessárias são definidas
  • Linhas 16–18 – É definida uma operação SELECT denominada [getAllArticles]
    • linha 16 - a operação SELECT é nomeada [name="getAllArticles"] e o mapeamento a utilizar é definido pelo atributo [resultMap="article"]. Isto refere-se ao mapeamento nas linhas 5–11
    • linha 17 – texto do comando SQL a ser executado
  • Linhas 20–22 – Definimos o comando SQL-Delete [clearAllArticles] para limpar a tabela de artigos.
  • Linhas 24–27 – Definimos o comando SQL-Insert [insertArticle] para adicionar um novo item à tabela de itens. Esta é uma consulta parametrizada que utiliza os elementos (#id#, #name#, #price#, #currentStock#, #minStock#). Os valores para estes cinco elementos provêm de um objeto [Article] passado como parâmetro: [parameterClass="istia.st.articles.dao.Article"]. O objeto parâmetro deve possuir as propriedades (id, name, price, currentStock, minimumStock) referenciadas pelo comando SQL parametrizado.
  • linhas 29-31 - definimos o comando SQL Delete [deleteArticle] destinado a eliminar um artigo cujo número #value# é conhecido. Este número será passado como parâmetro: [parameterClass="int"]. Esta é uma regra geral. Quando o parâmetro é único, é referenciado pela palavra-chave #value# no texto do comando SQL.
  • Linhas 33–35 – Definimos o comando SQL-Update [modifyArticle] para modificar um artigo cujo número é conhecido. Tal como no comando [insertArticle], as cinco informações necessárias provêm das propriedades de um objeto [istia.st.articles.dao.Article].
  • Linhas 37–39 – Definimos o comando SQL-Select [getArticleById], que recupera o registo de um artigo cujo número é conhecido.
  • Linhas 41–43 – Definimos o comando SQL-Update [changerStockArticle], que modifica o campo [stockactuel] de um item cujo número é conhecido. As duas informações necessárias — o #id# do item e o incremento de stock #mouvement# — serão encontradas num dicionário: [parameterClass="Hashtable"]. Este dicionário deve ter duas chaves: id e mouvement. Os valores associados a estas duas chaves serão utilizados no comando SQL.
2.5.6.4.4. Localização dos ficheiros de configuração

Vamos considerar dois cenários diferentes:

  • No caso de um teste Nunit, os ficheiros de configuração [SqlMap] serão colocados na mesma pasta que os binários testados.
  • No caso de uma aplicação web, serão colocados na raiz da aplicação.

2.5.6.5. A API do SqlMap

As classes SqlMap estão contidas em DLLs que são normalmente colocadas na pasta [bin] da aplicação:

Image

As aplicações que utilizam classes SqlMap devem importar o namespace [IBatisNet.DataMapper]:

Imports IBatisNet.DataMapper

Todas as operações SQL são realizadas através de um singleton do tipo [Mapper], uma classe no namespace [IBatisNet.DataMapper]. O singleton é obtido da seguinte forma:

        Dim mappeur As SqlMapper = Mapper.Instance

Para executar o comando SqlMap [getAllArticles], escrevemos:

                    dim articles as IList=mappeur.QueryForList("getAllArticles", Nothing)
  • O método [QueryForList] devolve o resultado de um comando SELECT como uma lista
  • O primeiro parâmetro é o nome do comando SQL a ser executado (ver articles.xml)
  • O segundo parâmetro é o parâmetro a passar para a consulta SQL. Deve corresponder ao atributo [parameterClass] do comando SqlMap. Em [articles.xml], temos [parameterClass=Nothing]. Por isso, passamos aqui um ponteiro nulo.
  • O resultado é do tipo IList. Os objetos nesta lista são especificados pelo atributo [resultMap] do comando SQL-select: [resultMap="article"]. "article" é um nome de mapeamento:
<resultMap id="article" class="istia.st.articles.dao.Article">

A classe associada a este mapeamento é [istia.st.articles.dao.Article]. Em última análise, a variável [articles] definida acima é uma lista de objetos [istia.st.articles.dao.Article]. Assim, recuperámos toda a tabela [ARTICLES] numa única instrução. Se a tabela [ARTICLES] estiver vazia, obtemos um objeto [IList] com 0 elementos.

Para executar o comando SqlMap [getArticleById], escrevemos:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • O método [QueryForObject] recupera o resultado de um comando SELECT que devolve apenas uma linha
  • O primeiro parâmetro é o nome do comando SqlMap a ser executado
  • O segundo parâmetro é o parâmetro a passar para a consulta SQL. Deve corresponder ao atributo [parameterClass] do comando SqlMap. Em [articles.xml], temos [parameterClass="int"]. Por isso, passamos aqui um inteiro que representa o ID do artigo que está a ser pesquisado.
  • O resultado é do tipo Object. Se o SELECT não devolveu nenhuma linha, o resultado é um ponteiro nulo (nada).

Para executar o comando SqlMap [insertArticle], escrevemos:

                    mappeur.Insert("insertArticle", unArticle)
  • O método [Insert] permite executar comandos SQL INSERT
  • O primeiro parâmetro é o nome do comando SqlMap a ser executado
  • O segundo parâmetro é o parâmetro a ser passado para ele. Deve corresponder ao atributo [parameterClass] do comando SqlMap. Em [articles.xml], temos [parameterClass="istia.st.articles.dao.Article"]. Portanto, passamos aqui um objeto do tipo [istia.st.articles.dao.Article].

Para executar o comando SqlMap [deleteArticle], escrevemos:

                    dim nbArticles as Integer=mappeur.Delete("deleteArticle", idArticle)
  • O método [Delete] permite executar comandos SQL DELETE
  • O primeiro parâmetro é o nome do comando SQL a executar
  • O segundo parâmetro é o parâmetro a ser passado para ele. Deve corresponder ao atributo [parameterClass] do comando SqlMap. Em [articles.xml], temos [parameterClass="int"]. Portanto, passamos aqui o ID do artigo a ser eliminado.
  • O resultado do método [Delete] é o número de linhas eliminadas

Da mesma forma, para executar o comando SqlMap [clearAllArticles], escrevemos:

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

Para executar o comando SqlMap [modifyArticle], escrevemos:

                    dim nbArticles as Integer=mappeur.Update("modifyArticle", unArticle)
  • O método [Update] permite executar comandos SQL UPDATE
  • O primeiro parâmetro é o nome do comando SqlMap a ser executado
  • O segundo parâmetro é o parâmetro a ser passado para ele. Deve corresponder ao atributo [parameterClass] do comando SqlMap. Em [articles.xml], temos [parameterClass="istia.st.articles.dao.Article"]. Portanto, passamos aqui um objeto do tipo [istia.st.articles.dao.Article].
  • O resultado do método [Update] é o número de linhas modificadas.

Da mesma forma, para executar o comando SqlMap [changerStockArticle], escreveríamos:

                    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)
  • O segundo parâmetro corresponde ao atributo [parameterClass] do comando SqlMap. Em [articles.xml], temos [parameterClass="Hashtable"]. O comando SQL parametrizado [changeItemStock] utiliza os parâmetros [id, movement]. Por isso, passamos aqui um dicionário com estas duas chaves.

2.5.6.6. O código para a classe [ArticlesDaoSqlMap]

Seguindo as explicações anteriores, podemos agora escrever a seguinte nova classe de implementação [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

Recomenda-se aos leitores que analisem este código à luz das explicações fornecidas para a API SqlMap. Vale a pena notar que a utilização do [SqlMap] reduziu significativamente a quantidade de código necessária.

2.5.6.7. Gerar a compilação da camada [dao]

O novo projeto do Visual Studio tem a seguinte estrutura:

Image

Repare na presença dos «assemblys» exigidos pelo SqlMap nas referências do projeto. Estas DLLs foram colocadas na pasta [bin] do projeto. O projeto está configurado para gerar uma DLL denominada [webarticles-dao.dll]:

2.5.6.8. Testes NUnit para a camada [dao]

2.5.6.8.1. A classe de teste NUnit

A classe de teste NUnit para a classe de implementação [ArticlesDaoSqlMap] é idêntica à da classe [ArticlesDaoPlainODBC] (ver secção 2.3.3.2). Seguimos uma abordagem semelhante para preparar o teste NUnit para a classe [ArticlesDaoSqlMap]:

  • criamos a pasta [test1] (à direita) na pasta do Visual Studio do projeto [dao-sqlmap], copiando a pasta [tests] do projeto [dao-odbc] (à esquerda):
  • Na pasta [tests], substituímos a DLL [webarticles-dao.dll] pela DLL [webarticles-dao.dll] gerada a partir do projeto [dao-sqlmap].
  • Adicionamos as DLLs necessárias ao SqlMap, bem como os ficheiros de configuração mencionados [providers.config, sqlmap.config, properties.xml, articles.xml].
  • Modificamos o ficheiro de configuração [spring-config.xml] para instanciar a nova 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>

Comentários:

  • Linha 7: O objeto [articlesdao] está agora associado a uma instância da classe [ArticlesDaoSqlMap]
  • Esta classe não tem construtor. Será utilizado o construtor padrão.
2.5.6.8.2. Testes

A tabela [ARTICLES] na fonte de dados Firebird é preenchida com os seguintes artigos:

Image

Estamos prontos para testar. Utilizando a aplicação [Nunit-Gui], carregamos a DLL [test-webarticles-dao.dll] da pasta [test1] acima e executamos o teste [testGetAllArticles]:

Image

Apesar do nome [NUnitTestArticlesDaoArrayList] inicialmente atribuído à classe de teste, é de facto a classe [ArticlesDaoSqlMap] que está a ser testada aqui. A captura de ecrã mostra que recuperámos corretamente os artigos que colocámos na tabela [ARTICLES]. Agora, vamos executar todos os testes:

Image

Os leitores que estiverem a visualizar este documento no ecrã verão que alguns testes foram aprovados (verde), mas outros falharam (vermelho). Os testes que falharam são [testArticleAbsent] e [testChangerStockArticle]. Após uma investigação exaustiva, parece que as causas destas falhas são as seguintes:

  • Em [testArticleAbsent], tentamos modificar um item que não existe. Utilizamos o método [modifieArticle] para isso, que retorna o número de linhas modificadas como 0 ou 1. Aqui, deveríamos obter 0. Em vez disso, obtemos uma exceção do tipo [IBatisNet.Common.Exceptions.ConcurrentException].
  • Em [changerStockArticle], existe outra operação do tipo [update]. Esta envolve a redução do stock numa quantidade superior ao stock atual. Para o fazer, utiliza-se o método [changerStockArticle], que devolve o número de linhas modificadas como 0 ou 1. O comando SQL foi escrito para impedir uma atualização (ver o comando SQL «changerStockArticle» em articles.xml) que resultaria num nível de stock negativo. Aqui, esperamos obter 0 como resultado do método [changerStockArticle]. Mais uma vez, obtemos uma exceção do tipo [IBatisNet.Common.Exceptions.ConcurrentException].

Existem muitas fontes possíveis de erro:

  1. o código na classe [ArticlesDaoSqlMap] está incorreto. Isso é possível. No entanto, provém de uma portabilidade de uma classe Java que funcionava corretamente com a versão Java do SqlMap.
  2. a versão .NET do SqlMap tem erros
  3. o controlador ODBC do Firebird tem erros
  4. ...

Na ausência de certezas, vamos contornar o problema capturando a [IBatisNet.Common.Exceptions.ConcurrentException]. O novo código para a classe [ArticlesDaoSqlMap] passa a ser o seguinte:

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

As alterações estão nas linhas: 28, 41, 69. Para operações SQL do tipo [UPDATE, DELETE], se ocorrer uma exceção do tipo [IBatisNet.Common.Exceptions.ConcurrentException], é devolvido 0 como resultado, indicando assim que nenhuma linha foi modificada ou eliminada. Depois de concluído, a DLL do projeto é regenerada, colocada na pasta [test1] e os testes NUnit são executados novamente:

Image

Desta vez funciona. Vamos agora trabalhar com esta DLL.

2.5.6.9. Integrar a nova camada [dao] na aplicação [webarticles]

2.5.6.9.1. Fonte de dados ODBC

Aqui testamos a fonte de dados ODBC discutida na secção 2.3.3.1. É utilizada aqui através do SqlMap.

Seguimos o procedimento descrito na secção 2.3.4. Efetuamos as seguintes alterações ao conteúdo da pasta [runtime]:

  • Na pasta [bin], a DLL da antiga camada [dao] é substituída pela DLL da nova camada [dao] implementada pela classe [ArticlesDaoSqlMap]. Adicionamos as DLLs necessárias para o Firebird e o SqlMap:

Image

  • em [runtime], colocamos os ficheiros de configuração do SqlMap [providers.config, sqlmap.config, properties.xml, articles.xml]:

Image

  • Em [runtime], o ficheiro de configuração [web.config] é substituído por um ficheiro que tem em conta a nova classe de implementação:
<?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>

Comentários:

  • As linhas 14 associam o singleton [articlesDao] a uma instância da nova classe [ArticlesDaoSqlMap]. Esta é a única alteração.

Estamos prontos para executar os testes. Vamos configurar o servidor web [Cassini] tal como fizemos nos testes anteriores. Vamos preencher a tabela articles com os seguintes valores:

Image

Usando um navegador, solicitamos a URL [http://localhost/webarticles/main.aspx]:

Image

Agora vamos verificar o conteúdo da tabela [ARTICLES]:

Image

Os itens [faca] e [colher] foram comprados e os seus níveis de stock foram reduzidos pela quantidade comprada. O item [garfo] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.5.6.9.2. Fonte de dados MSDE

Aqui estamos a testar a fonte de dados MSDE discutida na secção 2.4.3.1. É utilizada aqui através do SqlMap. Seguimos o mesmo procedimento de antes. Fazemos as seguintes alterações ao conteúdo da pasta [runtime]:

  • o conteúdo da pasta [bin] permanece inalterado
  • Em [runtime], os ficheiros de configuração do SqlMap [providers.config, properties.xml] são alterados. Os ficheiros de configuração [sqlmap.config, articles.xml] permanecem inalterados.
  • O ficheiro [providers.config] configura um novo <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>

Este <provider> utiliza as classes .NET para aceder a fontes de dados do SQL Server. Está incluído por predefinição no ficheiro de modelo [providers.config] distribuído com o SqlMap.

  • O ficheiro [properties.xml] define o <provider> para a fonte MSDE, bem como a sua cadeia de ligação:
<?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>
  • No [runtime], o ficheiro de configuração [web.config] permanece inalterado.

Estamos prontos para o teste. O servidor web [Cassini] mantém a sua configuração habitual. Inicializamos a tabela de artigos na fonte MSDE utilizando o [EMS MS SQL Manager]:

Image

Utilizando um navegador, solicitamos o URL [http://localhost/webarticles/main.aspx]:

Image

Agora, vamos verificar o conteúdo da tabela [ARTICLES] utilizando o [EMS MS SQL Manager]:

Image

Os artigos [bola de futebol] e [raquete de ténis] foram comprados e os seus níveis de stock foram reduzidos pela quantidade comprada. O artigo [patins em linha] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.5.6.9.3. Fonte de dados OleDb

Aqui estamos a testar a fonte de dados ACCESS apresentada na secção 2.4.5.1. É utilizada aqui através do SqlMap. Seguimos o mesmo procedimento de antes. Fazemos as seguintes alterações ao conteúdo da pasta [runtime]:

  • o conteúdo da pasta [bin] permanece inalterado
  • Em [runtime], os ficheiros de configuração do SqlMap [providers.config, properties.xml] são alterados. Os ficheiros de configuração [sqlmap.config, articles.xml] permanecem inalterados.
  • O ficheiro [providers.config] configura um novo <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>

Este <provider> utiliza as classes .NET para aceder a fontes de dados OleDb. Está incluído por predefinição no ficheiro de modelo [providers.config] distribuído com o SqlMap.

  • O ficheiro [properties.xml] define o <provider> da fonte OleDb e a sua cadeia de ligação:
<?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>
  • Em [runtime], o ficheiro de configuração [web.config] permanece inalterado.

Estamos prontos para o teste. O servidor web [Cassini] mantém a sua configuração habitual. Inicializamos a tabela de artigos a partir da fonte ACCESS da seguinte forma:

Image

Utilizando um navegador, solicitamos o URL [http://localhost/webarticles/main.aspx]:

Image

Agora, vamos verificar o conteúdo da tabela [ARTICLES] com:

Image

Os artigos [calças] e [saia] foram comprados e os seus níveis de stock foram reduzidos pela quantidade comprada. O artigo [casaco] não pôde ser comprado porque a quantidade solicitada excedeu a quantidade em stock. Convidamos o leitor a realizar testes adicionais.

2.5.7. Conclusão

Concluímos aqui este longo artigo tutorial. O que fizemos?

  • Implementámos a camada [DAO] de uma aplicação web de três camadas de quatro formas diferentes:
    1. utilizando classes de acesso .NET a fontes ODBC
    2. utilizando classes de acesso .NET para fontes SQL Server
    3. utilizando classes de acesso .NET para fontes OleDb
    4. utilizando classes de acesso de terceiros para aceder a uma base de dados Firebird
  • Em cada caso, integramos a nova camada [DAO] na aplicação [webarticles] de três camadas [web, domínio, DAO] sem recompilar nenhuma das camadas [web, domínio]
  • Introduzimos finalmente a ferramenta [SqlMap], que nos permitiu criar uma camada [DAO] capaz de se adaptar a diferentes fontes de dados de forma transparente para o código. Assim, com esta nova camada, conseguimos utilizar sucessivamente as fontes de dados das implementações anteriores 1 a 3. Isto foi feito de forma transparente utilizando ficheiros de configuração.
  • Demonstrámos a grande flexibilidade que as ferramentas Spring e SqlMap proporcionam às aplicações web de três camadas.