Skip to content

2. Parte 2

2.1. Introduction

Comenzaremos recordando lo que se hizo en la parte 1 y, en particular, la arquitectura de tres capas [web, domain, dao] utilizada. En la solución propuesta, la capa [dao] era una capa de prueba: la fuente de datos estaba implementada por un objeto [ArrayList]. En este artículo nos enfocaremos en la capa [dao], presentando diversas implementaciones posibles de la misma cuando los datos se encuentran en un SGBD.

Herramientas utilizadas:

  • el SGBD de Firebird —véase el anexo, párrafo 3.5—.
  • el SGBD MSDE (Microsoft Data Engine) —véase el anexo, párrafo 3.12.
  • IBExpert, edición personal para administrar gráficamente el SGBD Firebird —véase el anexo, párrafo 3.6.
  • EMS MS SQL Manager para administrar gráficamente el SGBD MSDE - véase el anexo, párrafo 3.14.
  • Ibatis SqlMap para la capa de acceso a datos del SGBD — véase el párrafo 2.5.6.2.

En una escala de principiante-intermedio-avanzado, este documento se encuentra en la sección [intermédiaire-avancé]. Para comprenderlo se requieren varios requisitos previos. Algunos de ellos pueden adquirirse en documentos que he escrito. En ese caso, los menciono. Es evidente que esto es solo una sugerencia y que el lector puede utilizar sus documentos favoritos.

  • lenguaje VB.net: [Introduction au langage VB.NET par l'exemple ]
  • programación web en VB.net: [Développement WEB avec ASP.NET 1.1 ]
  • uso del componente IoC de Spring: [Spring IoC pour .NET ]
  • Documentación de iBatis SqlMap: [http://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Documentación de Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf]
  • documentación Spring.net: [http://www.springframework.net/documentation.html]

2.2. La aplicación webarticles - Recordatorios

Aquí presentamos los elementos de la aplicación web simplificada de comercio electrónico estudiada en la parte 1. Esta permite a los usuarios de la web:

  • consultar una lista de artículos provenientes de una base de datos
  • agregar algunos de ellos a un carrito de compras
  • validar el carrito. Esta validación tiene como único efecto actualizar, en la base de datos, los inventarios de los artículos comprados.

2.2.1. Las vistas de la aplicación

Las diferentes vistas que se le presentan al usuario son las siguientes:

  • la vista [ERREURS], que notifica cualquier error de la aplicación

Image

2.2.2. Arquitectura general de la aplicación

La aplicación desarrollada en la parte 1 tiene una arquitectura de tres capas:

  • Las tres capas se han hecho independientes mediante el uso de interfaces
  • La integración de las diferentes capas se ha llevado a cabo con Spring
  • cada capa cuenta con espacios de nombres separados: web (capa UI), dominio (capa de negocio) y DAO (capa de acceso a datos).

La aplicación sigue una arquitectura MVC (Modelo - Vista - Controlador). Si retomamos el esquema de capas anterior, la arquitectura MVC se integra en él de la siguiente manera:

El procesamiento de una solicitud de un cliente se lleva a cabo siguiendo estos pasos:

  1. el cliente envía una solicitud al controlador. Este controlador es, en este caso, una página .aspx a la que se le asigna un papel específico. A través de ella pasan todas las solicitudes de los clientes. Es la puerta de entrada de la aplicación. Es la C de MVC.
  2. El controlador procesa esta solicitud. Para ello, puede necesitar la ayuda de la capa de negocio, lo que se conoce como el modelo M en la estructura MVC.
  3. El controlador recibe una respuesta de la capa de negocio. La solicitud del cliente ha sido procesada. Esto puede dar lugar a varias respuestas posibles. Un ejemplo clásico es
    • una página de errores si la solicitud no se pudo procesar correctamente
    • una página de confirmación, en caso contrario
  4. el controlador elige la respuesta (= vista) que se enviará al cliente. Esta suele ser una página que contiene elementos dinámicos. El controlador proporciona estos elementos a la vista.
  5. La vista se envía al cliente. Es la V de MVC.

2.2.3. La plantilla

El modelo M de MVC está compuesto aquí por los siguientes elementos:

  1. las clases de negocio
  2. las clases de acceso a datos
  3. la base de datos

2.2.3.1. La base de datos

La base de datos contiene únicamente una tabla llamada ARTICLES, generada con los siguientes 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
);
/* restricciones */
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);
/* clave primaria */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
clave primaria que identifica un artículo de manera única
nom
nombre del artículo
prix
su precio
stockactuel
su stock actual
stockminimum
el nivel de existencias por debajo del cual se debe realizar un pedido de reabastecimiento

2.2.3.2. Los espacios de nombres del modelo

El modelo M se proporciona en forma de dos espacios de nombres:

  • istia.st.articles.dao: contiene las clases de acceso a los datos de la capa [dao]
  • istia.st.articles.domain: contiene las clases de negocio de la capa [domain]

Cada uno de estos espacios de nombres se encuentra dentro de un archivo de «ensamblaje» propio:

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: la interfaz de acceso a la capa [dao]. Es la única interfaz que ve la capa [domain]. No ve ninguna otra.
- [Article]: clase que define un artículo
- [ArticlesDaoArrayList]: clase de implementación de la interfaz [IArticlesDao] con una clase [ArrayList]
capa de acceso a datos — se encuentra íntegramente en la capa [dao] de la arquitectura de tres capas de la aplicación web
webarticles-domain
- [IArticlesDomain]: la interfaz de acceso a la capa [domain]. Es la única interfaz que ve la capa web. No ve ninguna otra.
- [AchatsArticles]: una clase que implementa [IArticlesDomain]
- [Achat]: clase que representa la compra de un cliente
- [Panier]: clase que representa el conjunto de compras de un cliente
representa el modelo de compras en la web - se encuentra íntegramente en la capa [domain] de la arquitectura de tres capas de la aplicación web

2.2.4. Implementación y pruebas de la aplicación [webarticles]

2.2.4.1. Implementación

Implementamos la aplicación desarrollada en la parte 1 del artículo en una carpeta llamada [runtime]:

 

Comentarios:

La carpeta [runtime] contiene tres archivos y dos subcarpetas:

  • los controladores [global.asax] y [main.aspx]
  • el archivo de configuración [web.config]
  • la carpeta [bin], que contiene:
    • los DLL de las tres capas [webarticles-dao.dll], [webarticles-domain.dll] y [webarticles-web.dll]
    • los archivos necesarios para Spring: [Spring-Core.*] y [log4net.dll]
  • la carpeta [vues], que contiene el código de presentación de las diferentes vistas.
  • No es necesario incluir los archivos de código .vb, ya que su versión compilada se encuentra en los archivos DLL.

2.2.4.2. Pruebas

Configuramos el servidor web [Cassini] de la siguiente manera:

Image

con:

Ruta física: D:\data\serge\travail\2004-2005\aspnet\webarticles-010405\runtime\

Ruta virtual: /webarticles

Desde un navegador, solicitamos el URL [http://localhost/webarticles/main.aspx]

Image

Recordemos que la capa [dao] está implementada por una clase que almacena los artículos en un objeto [ArrayList]. Esta clase crea una lista inicial de cuatro artículos. A partir de la vista anterior, utilizamos los enlaces del menú para realizar operaciones. A continuación se muestran algunas de ellas. La columna de la izquierda representa la solicitud del cliente y la columna de la derecha, la respuesta que se le da.

2.2.5. La capa [dao] revisada

En nuestra primera implementación de la capa [dao], la interfaz [IArticlesDao] de acceso a los datos se había implementado mediante una clase que almacenaba los artículos en un objeto [ArrayList]. Esto nos permitió no detenernos en esta capa y demostrar que lo único importante era su interfaz y no su implementación. Así pudimos construir una aplicación web operativa. Esta cuenta con tres capas: [web], [domain] y [dao]. Aquí vamos a proponer diferentes implementaciones de la capa [dao]. Cada una de ellas podrá reemplazar la capa actual [dao] sin necesidad de modificar las capas [domain] y [web]. Esta flexibilidad se logra porque:

  • la capa [domain] no se dirige a una clase concreta, sino a una interfaz [IArticlesDao]
  • gracias a Spring, hemos podido ocultar a la capa [domain] el nombre de la clase de implementación de la interfaz [IArticlesDao].

2.2.5.1. Elementos de la capa [dao]

Recordemos algunos de los elementos de la capa [dao] que se conservarán en las nuevas implementaciones:

  • - [IArticlesDao]: la interfaz de acceso a la capa [dao]
  • - [Article]: clase que define un artículo

2.2.5.2. La clase [Article]

La clase que define un artículo es la siguiente:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

         ' campos privados
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

         ' ID del artículo
        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

         ' nombre del artículo
        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

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

         ' stock actual del artículo
        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

         ' stock mínimo del artículo
        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

         ' fabricante predeterminado
        Public Sub New()
        End Sub

         ' fabricante con propiedades
        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

         ' método de identificación del artículo
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Esta clase ofrece:

  1. un constructor que permite establecer los 5 datos de un artículo: [id, nom, prix, stockactuel, stockminimum]
  2. propiedades públicas que permiten leer y escribir los 5 datos.
  3. una verificación de los datos ingresados en el artículo. En caso de datos erróneos, se genera una excepción.
  4. un método toString que permite obtener el valor de un artículo en forma de cadena de caracteres. Esto suele ser útil para depurar una aplicación.

2.2.5.3. La interfaz [IArticlesDao]

La interfaz [IArticlesDao] se define de la siguiente manera:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' lista de todos los artículos
        Function getAllArticles() As IList
         ' agrega un artículo
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' elimina un artículo
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' modifica un artículo
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' busca un artículo
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' elimina todos los artículos
        Sub clearAllArticles()
         ' cambia el inventario de un artículo
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

La función de los distintos métodos de la interfaz es la siguiente:

getAllArticles
devuelve todos los artículos de la fuente de datos
clearAllArticles
vacía la fuente de datos
getArticleById
devuelve el objeto [Article] identificado por su número
ajouteArticle
permite agregar un artículo a la fuente de datos
modifieArticle
permite modificar un artículo de la fuente de datos
supprimerArticle
permite eliminar un artículo de la fuente de datos
changerStockArticle
permite modificar el inventario de un artículo de la fuente de datos

La interfaz pone a disposición de los programas clientes una serie de métodos definidos únicamente por sus firmas. No se ocupa de la forma en que estos métodos se implementarán realmente. Esto aporta flexibilidad a una aplicación. El programa cliente realiza sus llamadas a una interfaz y no a una implementación específica de la misma.

La elección de una implementación específica se realiza mediante un archivo de configuración de Spring.

2.3. La clase de implementación [ArticlesDaoPlainODBC]

Ofrecemos una nueva implementación de la capa [dao] que supone que los datos se encuentran en una fuente ODBC. Se sabe que, en Windows, prácticamente todos los SGBD del mercado cuentan con un controlador ODBC. La ventaja de esta solución es que se puede cambiar de SGBD de manera transparente para la aplicación. La desventaja es que un controlador ODBC que solo aprovecha las características comunes a todos los SGBD suele tener un rendimiento inferior al de un controlador escrito específicamente para aprovechar todo el potencial de un SGBD en particular. Se puede consultar el párrafo 3.7 para ver un ejemplo de cómo crear un código fuente para el ODBC.

2.3.1. El código

2.3.1.1. El esqueleto

La clase [ArticlesDaoPlainODBC] implementa la interfaz [IArticlesDao] de la siguiente manera:

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

Namespace istia.st.articles.dao

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

         ' campos privados
        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

         ' fabricante
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN: nombre de la fuente ODBC
             ' uid: identidad del usuario
             ' contraseña: su contraseña
...
        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
             ' ejecución de una consulta SELECT 
....
        End Function

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

End Namespace

Comentarios:

  • en la línea 3, se importa el espacio de nombres que contiene las clases .NET para acceder a las fuentes ODBC
  • línea 11: almacenará la conexión a la fuente ODBC
  • línea 12: almacenará el nombre DSN de la fuente de datos
  • líneas 13-19: variables privadas de tipo [OdbcCommand] que definen las consultas SQL utilizadas por los distintos métodos de la clase
  • líneas 22-27: el constructor. Recibe los elementos que le permiten construir el objeto [OdbcConnection], el cual vinculará el código con la fuente de datos ODBC
  • líneas 29-31: el método para agregar un artículo
  • líneas 33-35: el método para modificar el inventario de un artículo
  • líneas 37-39: el método que elimina todos los artículos de la fuente de datos ODBC
  • líneas 41-43: el método que obtiene la lista de todos los artículos de la fuente ODBC
  • líneas 45-47: el método que permite obtener un artículo específico
  • líneas 49-51: el método que permite modificar ciertos campos de un artículo cuyo número se conoce
  • líneas 53-55: el método que permite eliminar un artículo cuyo número se conoce
  • líneas 57-60: método de utilidad que permite ejecutar un [SELECT] sobre la fuente de datos y devolver el resultado
  • líneas 62-64: método de utilidad que permite ejecutar un [INSERT, UPDATE, DELETE] sobre la fuente de datos y mostrar el resultado

2.3.1.2. El constructor

         ' creador
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN: nombre de la fuente ODBC
             ' uid: identidad del usuario
             ' contraseña: su contraseña

             'se recupera el nombre de la base de datos pasado como argumento
            Me.DSN = DSN
            Dim connectString As String = String.Format("DSN={0};UID={1};PASSWORD={2}", DSN, uid, password)
             ': se inicia la conexión
            connexion = New OdbcConnection(connectString)
             ' se preparan las consultas SQL
            insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
            updatecommand = New OdbcCommand("update ARTICLES set nom=?, prix=?, stockactuel=?, stockminimum=? where id=?", connexion)
            deleteSomeCommand = New OdbcCommand("delete from ARTICLES where id=?", connexion)
            selectSomeCommand = New OdbcCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=?", connexion)
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)
            selectAllCommand = New OdbcCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New OdbcCommand("delete from ARTICLES", connexion)
        End Sub

Comentarios:

  • línea 2: el constructor recibe la información necesaria para conectarse a una fuente ODBC: el nombre DSN de la fuente, la identidad con la que se debe conectar y la contraseña asociada.
  • línea 8: se almacena el nombre DSN de la fuente para poder mostrarlo en los mensajes de error.
  • línea 9: se instancia el objeto [OdbcConnection]. Una conexión instanciada no es una conexión abierta. El método [open] es el que realiza la apertura.
  • líneas 12-19: preparamos las consultas SQL en los objetos [OdbcCommand]. Esto nos evitará tener que volver a crearlas cada vez que las necesitemos. Los parámetros formales ? de las consultas serán reemplazados por valores reales en el momento de ejecutar la consulta.

2.3.1.3. El método executeQuery

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
             ' ejecución de una consulta SELECT 
             ' declaración del objeto que permite el acceso a todas las filas de la tabla de resultados
            Dim myReader As OdbcDataReader = Nothing
            Try
                 ': se crea una conexión a la BDD
                connexion.Open()
                 ': se ejecuta la consulta
                myReader = query.ExecuteReader()
                 'se declara una lista de artículos para devolverla posteriormente
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'se prepara un artículo con los valores del lector
                    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)
                     ': se agrega el artículo a la lista
                    articles.Add(unArticle)
                End While
                 'se devuelve el resultado
                Return articles
            Finally
                 ' liberación de recursos
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

Comentarios:

  • El método [executeQuery] es un método de utilidad que:
    • ejecuta una consulta [SELECT id, nom, prix, stockactuel, stockminimum from ARTICLES ...] sobre la fuente de datos
    • devuelve el resultado en forma de una lista de objetos [Article]
  • línea 1: el único parámetro del método es el objeto [OdbcCommand] que contiene la consulta [Select] a ejecutar.
  • línea 7: se abre la conexión. Se cerrará en la línea 29, independientemente de si se produjo un error o no.
  • línea 9: se instancia el objeto [OdbcDataReader] necesario para procesar el resultado de [Select]
  • Líneas 13-23: cada línea resultante de [Select] se coloca en un objeto [Article], que se unirá a los demás artículos en un objeto [ArrayList]
  • La lista de artículos se devuelve en la línea 25
  • No se maneja ninguna excepción. El código que llama a este método deberá encargarse de ello.

2.3.1.4. El método executeUpdate

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
             ' ejecución de una solicitud de actualización
            Try
                 ': se crea una conexión con BDD
                connexion.Open()
                 'se ejecuta la consulta
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' liberación de recursos
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

Comentarios:

  • El método recibe un objeto [OdbcCommand] que contiene una consulta SQL de tipo [Insert, Update, Delete].
  • La conexión se abre en la línea 5. Se cerrará en la línea 10, independientemente de si se produjo una excepción o no.
  • La consulta de actualización se ejecuta en la línea 7. Se devuelve inmediatamente el resultado, que es el número de filas de la tabla ARTICLES modificadas por la consulta.

2.3.1.5. El método ajouteArticle

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de inserción
                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
                     'se ejecuta
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'error en la consulta
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

Comentarios:

  • línea 1: el método recibe el artículo que se debe agregar a la fuente de datos ODBC. Devuelve el número de filas afectadas por esta operación, c.a.d. 1 o 0
  • líneas 3 y 20: el método está sincronizado. Este será el caso de todos los métodos de acceso a datos. Esto implica que solo un hilo a la vez podrá trabajar en la fuente de datos. Probablemente sea demasiado conservador. Existen mejores alternativas, en particular la de incluir estas operaciones en transacciones. En este caso, es el SGBD el que gestiona los accesos concurrentes. No hemos querido introducir el concepto de transacción por el momento. Spring nos ofrece la posibilidad de introducirlas en la capa [domain]. Quizás tengamos la oportunidad de volver sobre este tema en otro artículo.
  • En las líneas 5 a 12, se asignan valores a los parámetros formales de la consulta del objeto [insertCommand] inicializado por el constructor. Recordemos esta consulta:
insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)

Los 5 valores necesarios para la consulta se proporcionan en las líneas 7-11.

  • En las líneas 13 a 19, se ejecuta la consulta. Si todo sale bien, se devuelve el resultado. De lo contrario, se lanza una excepción genérica con un mensaje de error explícito

2.3.1.6. El método modifieArticle

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización
                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
                 ' se ejecuta
                Try
                     'se ejecuta la consulta de inserción
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'error de consulta
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

Comentarios:

  • línea 1: el método recibe el artículo que se va a modificar en la fuente de datos ODBC. Devuelve el número de líneas afectadas por esta operación, c.a.d. 1 o 0
  • Los comentarios del método [ajouteArticle] pueden incluirse aquí

2.3.1.7. El método supprimeArticle

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' sección exclusiva
            SyncLock Me
                 ' Se prepara la consulta de eliminación
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta de eliminación
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'error de consulta
                    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

Comentarios:

  • línea 1: el método recibe el número del artículo que se va a eliminar en la fuente de datos ODBC. Devuelve el número de filas afectadas por esta operación, c.a.d. 1 o 0
  • Los comentarios del método [ajouteArticle] pueden incluirse aquí

2.3.1.8. El método getAllArticles

            Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' sección exclusiva
            SyncLock Me
                Try
                     'se ejecuta la consulta SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'se devuelve la lista
                    Return articles
                Catch ex As Exception
                     'error de consulta
                    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

Comentarios:

  • línea 1: el método no recibe ningún parámetro. Devuelve la lista de todos los artículos de la fuente de datos ODBC
  • La consulta [Select], que solicita todos los artículos, se envía al método [executeQuery] —línea 6
  • La lista obtenida se devuelve en la línea 8
  • En las líneas 9 a 12, se maneja una posible excepción

2.3.1.9. El método getArticleById

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'se verifica si se ha encontrado el artículo
                    If articles.Count = 0 Then Return Nothing
                     'se devuelve el artículo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'error en la consulta
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

Comentarios:

  • Línea 1: el método recibe como parámetro el número del artículo deseado. Devuelve dicho artículo si se encuentra en la fuente ODBC; de lo contrario, devuelve la referencia [nothing].
  • La consulta [Select] que solicita el artículo se inicializa en las líneas 5-8
  • se ejecuta en la línea 12; se obtiene una lista de artículos
  • si esta lista está vacía, se devuelve la referencia [nothing] en la línea 14
  • de lo contrario, se devuelve el único artículo de la lista en la línea 16
  • En las líneas 17 a 20, se maneja una posible excepción

2.3.1.10. El método clearAllArticles

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' sección exclusiva
            SyncLock Me
                Try
                     'se ejecuta la consulta de eliminación
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'error de consulta
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

Comentarios:

  • línea 1: el método no recibe ningún parámetro y no devuelve nada
  • línea 6: se ejecuta la solicitud de eliminación de todos los artículos
  • líneas 7-10: se maneja una posible excepción

2.3.1.11. El método changerStockArticle

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización de inventario
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("mvt1", mouvement))
                    .Add(New OdbcParameter("id", idArticle))
                    .Add(New OdbcParameter("mvt2", mouvement))
                End With
                 'se ejecuta
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'error de consulta
                    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

Comentarios:

  • línea 1: el método recibe como parámetros el número del artículo cuyo inventario se debe modificar, así como el incremento del mismo (positivo o negativo). Devuelve el número de líneas modificadas por la operación c.a.d: 0 o 1.
  • Líneas 5-10: se inicializa la consulta [updateStockCommand]. Recordemos el texto de la consulta SQL:
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)

Cabe señalar que el stock solo se modifica si, una vez modificado, sigue siendo >=0.

  • La consulta para actualizar el inventario del artículo se ejecuta en la línea 13 y se muestra el resultado
  • En las líneas 14 a 18, se maneja una posible excepción

2.3.2. Generación del ensamblado de la capa [dao]

El proyecto de Visual Studio de esta nueva versión de la capa [dao] tiene la siguiente estructura:

Image

El proyecto está configurado para generar un DLL llamado [webarticles-dao.dll]:

2.3.3. Pruebas NUnit de la capa [dao]

2.3.3.1. Creación de una fuente de datos ODBC-Firebird

Para probar nuestra nueva capa [dao] necesitamos una fuente de datos ODBC y, por lo tanto, una base de datos. Utilizamos Firebird SGBD (párrafo 3.5). Con IBExpert (párrafo 3.6), creamos la siguiente base de artículos:

El administrador de esta base de datos será el usuario [SYSDBA] con la contraseña [masterkey]. Creamos algunos artículos:

Image

Ahora creamos la siguiente fuente de Firebird ODBC (véase el párrafo 3.7):

La fuente ODBC creada tiene las siguientes características:

  • nombre DSN: odbc-firebird-artículos
  • identidad de conexión: SYSDBA
  • contraseña asociada: masterkey

2.3.3.2. La clase de prueba de : NUnit

Ya hemos escrito una clase de prueba para la capa [dao] construida inicialmente. Si el lector lo recuerda, esa clase no probaba una clase específica, sino la interfaz [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
         ' el objeto a probar
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' se recupera una instancia del generador de objetos de Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' se solicita la instanciación del objeto articlesdao
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

....

Se observa que en el método de atributo <Setup()>, se le solicita a Spring una referencia al singleton denominado [articlesdao], del tipo [IArticlesDao], es decir, del tipo de la interfaz. El singleton [articlesdao] se definió mediante el siguiente archivo de configuración [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>

Demostremos que la clase de prueba inicial nos permite probar nuestra nueva capa [dao] sin necesidad de modificaciones ni recompilación.

  • Creemos en la carpeta de Visual Studio de nuestra nueva capa [dao] la carpeta [tests] (a la derecha, abajo) copiando la carpeta [bin] del proyecto de pruebas de la capa inicial [dao] (a la izquierda, abajo). Si es necesario, se invita al lector a revisar el proyecto de pruebas de la primera versión de la capa [dao] en la primera parte del artículo.
  • en la carpeta [tests], reemplazamos la DLL y la [webarticles-dao.dll], provenientes de laantigua capa [dao] por DLL y [webarticles-dao.dll], provenientes de la nueva capa [dao]
  • Modificamos el archivo de configuración [spring-config.xml] para instanciar la nueva clase [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>

Comentarios:

  • línea 6, el objeto [articlesdao] ahora está asociado a una instancia de la clase [ ArticlesDaoPlainODBC]
  • esta clase tiene un constructor con tres argumentos:
  • el nombre de la fuente DSN —línea 8—
  • la identidad con la que se accederá a la base de datos —línea 11—
  • la contraseña asociada a esta identidad —línea 14—

Aquí retomamos la información de la fuente ODBC-Firebird que creamos anteriormente.

2.3.3.3. Pruebas

Ahora estamos listos para las pruebas. Con la aplicación [Nunit-Gui], cargamos el archivo DLL [test-webarticles-dao.dll] de la carpeta [tests] mencionada anteriormente y ejecutamos la prueba [testGetAllArticles]:

Image

Al observar la captura de pantalla anterior, es posible que nos arrepintamos del nombre [NUnitTestArticlesDaoArrayList] que se le asignó inicialmente a la clase de prueba. Esto genera confusión. En realidad, la clase que se está probando aquí es la [ArticlesDaoPlainODBC]. La captura de pantalla muestra que hemos recuperado correctamente los artículos que habíamos colocado en la tabla [ARTICLES]. Ahora, realicemos todas las pruebas:

Image

En la ventana de la izquierda se ve la lista de los métodos probados. El color del punto que precede al nombre de cada método indica si el método se ha ejecutado correctamente (verde) o no (rojo). El lector que vea este documento en pantalla podrá comprobar que todas las pruebas se han realizado con éxito.

2.3.3.4. Conclusión

Acabamos de demostrar que:

  • porque la clase de prueba NUnit no hacía referencia a una clase, sino a una interfaz;
  • porque el nombre exacto de la clase de instanciación de la interfaz se proporcionaba en un archivo de configuración y no en el código;
  • porque Spring se encargaba de instanciar la clase y de proporcionar una referencia de la misma al código de prueba;

por lo tanto, el código de prueba escrito para la capa inicial [dao] seguía siendo válido para una nueva implementación de esa misma capa. No necesitamos tener acceso al código de la clase de prueba. Solo utilizamos su versión compilada, la generada durante la prueba de la capa inicial [dao]. Llegaremos a conclusiones similares cuando sea necesario integrar la nueva capa [dao] en la aplicación [webarticles].

2.3.4. Integración de la nueva capa [dao] en la aplicación [webarticles]

2.3.4.1. Las pruebas de integración

Recordemos que la versión inicial de la aplicación [webarticles] se había implementado en la carpeta [runtime] siguiente:

 

Se invita al lector a revisar, si lo considera necesario, el párrafo 2.2.4, que detalla las modalidades de implementación de la aplicación [webarticles]. Realizamos las siguientes modificaciones al contenido de la carpeta [runtime]:

  • en la carpeta [bin], el archivo DLL de la capa anterior [dao] se reemplaza por el archivo DLL de la nueva capa [dao]
  • en [runtime], el archivo de configuración [web.config] se reemplaza por un archivo que toma en cuenta la nueva clase de implementación de la capa [dao]:

El nuevo archivo de configuración [web.config] es el siguiente:

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

Comentarios:

  • las líneas 14 a 24 asocian al singleton [articlesDao] una instancia de la nueva clase [ArticlesDaoPlainODBC]. Esta es la única modificación. Ya la hemos visto durante las pruebas de la nueva capa [dao].

Estamos listos para las pruebas. Configuramos el servidor web [Cassini] de la misma manera que en el párrafo 2.2.4. Inicializamos la tabla de artículos [Firebird] con los siguientes valores:

Image

Asegúrese de que el servidor web Cassini, así como SGBD y [Firebird], estén en ejecución. Con un navegador, accedemos a la URL [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES] en la base de datos [Firebird]:

Image

Los artículos [parapluie] y [bottes] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [chapeau] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencia. Invitamos al lector a realizar pruebas adicionales.

2.3.4.2. Conclusión

¿Qué hicimos?

  • Hemos recuperado la versión de implementación de la versión anterior;
  • hemos reemplazado el DLL de la capa [dao] por una nueva versión. Los DLL de las capas [web] y [domain] se mantuvieron sin cambios;
  • hemos modificado el archivo de configuración [web.config] para que tenga en cuenta la nueva clase de implementación de la capa [dao]

Todo esto es ordenado y ofrece una gran facilidad de evolución a la aplicación web. Estas características importantes nos las brindan dos opciones de arquitectura:

  • el acceso a las capas a través de interfaces
  • la integración y configuración de las capas mediante Spring.

Ahora proponemos una nueva implementación de la capa [dao].

2.4. La clase de implementación [ArticlesDaoSqlServer]

La segunda implementación de la capa [dao] supone que los datos se encuentran en una base de datos SQL Server. Microsoft pone a disposición un SGBD llamado MSDE, que es una versión limitada de SQL Server. En el anexo, en el párrafo 3.12, se explica cómo obtenerlo e instalarlo.

2.4.1. El código

La clase [ArticlesDaoSqlServer] es muy similar a la clase [ArticlesDaoPlainODBC] que analizamos anteriormente. Por lo tanto, solo indicaremos los cambios introducidos con respecto a la versión anterior:

  • las clases necesarias se encuentran en el espacio de nombres [System.Data.SqlClient] en lugar del espacio de nombres [System.Data.Odbc]
  • la conexión de tipo [OdbcConnection] ahora tiene el tipo [SqlConnection]
  • los objetos [OdbcCommand] ahora tienen el tipo [SqlCommand]
  • La sintaxis de las consultas SQL configuradas cambia. La consulta de inserción queda así:
insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)

mientras que antes era:

insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
  • El método [ajouteArticle] queda entonces así:
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de inserción
                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
                     'se ejecuta
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'error en la consulta
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function
  • el constructor también se modifica:
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' servidor: nombre de la instancia SQL a la que se debe conectar
             ' databaseName: nombre de la base de datos a la que se debe acceder
             ' uid: identidad del usuario
             ' contraseña: su contraseña

             'se recupera el nombre de la base de datos pasado como argumento
            Me.databaseName = databaseName
             ': se inicia la conexión
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' se preparan las consultas SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
...
        End Sub

El constructor ahora admite cuatro parámetros:

             ' servidor: nombre de la instancia SQL a la que se debe conectar
             ' databaseName: nombre de la base de datos a la que se debe acceder
             ' uid: identidad del usuario
             ' contraseña: su contraseña

El código completo de la clase [ArticlesDaoSqlServer] es el siguiente:

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

Namespace istia.st.articles.dao

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

         ' campos privados
        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

         ' constructor
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' servidor: nombre de la instancia SQL a la que se debe acceder
             ' databaseName: nombre de la base de datos a la que se debe conectar
             ' uid: identidad del usuario
             ' contraseña: su contraseña

             'se recupera el nombre de la base de datos pasado como argumento
            Me.databaseName = databaseName
             ': se inicia la conexión
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' se preparan las consultas SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
            updatecommand = New SqlCommand("update ARTICLES set nom=@nom, prix=@prix, stockactuel=@sa, stockminimum=@sm where id=@id", connexion)
            deleteSomeCommand = New SqlCommand("delete from ARTICLES where id=@id", connexion)
            selectSomeCommand = New SqlCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=@id", connexion)
            updateStockCommand = New SqlCommand("update ARTICLES set stockactuel=stockactuel+@mvt where id=@id and (stockactuel+@mvt)>=0", connexion)
            selectAllCommand = New SqlCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New SqlCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sección exclusiva
            SyncLock Me
                 ': se prepara la consulta de inserción
                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
                     'se ejecuta
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'error en la consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización del inventario
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@mvt", mouvement))
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 'se ejecuta
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                Try
                     'se ejecuta la consulta de inserción
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                Try
                     'se está ejecutando la consulta SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'se devuelve la lista
                    Return articles
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'se verifica si se ha encontrado el artículo
                    If articles.Count = 0 Then Return Nothing
                     'se devuelve el artículo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'error en la consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización
                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
                 ' se ejecuta
                Try
                     'se ejecuta la consulta de inserción
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' Se prepara la consulta de eliminación
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta de eliminación
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' ejecución de una consulta SELECT 
             ' declaración del objeto que permite el acceso a todas las filas de la tabla de resultados
            Dim myReader As SqlDataReader = Nothing
            Try
                 'se crea una conexión a la BDD
                connexion.Open()
                 ': se ejecuta la consulta
                myReader = query.ExecuteReader()
                 'se declara una lista de artículos para devolverla posteriormente
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'se prepara un artículo con los valores del lector
                    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)
                     ': se agrega el artículo a la lista
                    articles.Add(unArticle)
                End While
                 'se devuelve el resultado
                Return articles
            Finally
                 ': liberación de recursos
                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
             ' ejecución de una solicitud de actualización
            Try
                 'se crea una conexión con el BDD
                connexion.Open()
                 'se ejecuta la consulta
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' liberación de recursos
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Se invita al lector a leer este código a la luz de los comentarios sobre la clase [ArticlesDaoPlainODBC] realizados anteriormente.

2.4.2. Generación del ensamblado de la capa [dao]

El nuevo proyecto de Visual Studio tiene la siguiente estructura:

Image

El proyecto está configurado para generar un archivo DLL denominado [webarticles-dao.dll]:

2.4.3. Pruebas NUnit de la capa [dao]

2.4.3.1. Creación de un servidor de datos de origen SQL

Para probar nuestra nueva capa [dao] necesitamos una fuente de datos SQL Server y, por lo tanto, el SGBD SQL Server. De hecho, utilizaremos el SGBD MSDE (MicroSoft Data Engine) (párrafo 3.12), que es una versión del servidor SQL limitada únicamente por el número de usuarios simultáneos que admite. Con [EMS MS SQL Manager] (párrafo 3.14) creamos la siguiente base de artículos en una instancia de MSDE llamada [portable1_tahe\msde140405]:

Image

La base de datos es propiedad del usuario [mdparticles], con la contraseña [admarticles]. El comando Transact-SQL para crear la tabla [ARTICLES] es el siguiente:

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

Creamos algunos artículos:

Image

2.4.3.2. La clase de prueba NUnit

La clase de prueba NUnit de la clase de implementación [ArticlesDaoSqlServer] es la misma que la de la clase [ArticlesDaoPlainODBC] (véase el párrafo 2.3.3.2). Seguimos un procedimiento similar para preparar la prueba NUnit de la clase:

  • creamos en la carpeta de Visual Studio del proyecto [dao-sqlserver] la carpeta [tests] (a la derecha) copiando la carpeta [tests] del proyecto [dao-odbc] (a la izquierda):
  • en la carpeta [tests] del proyecto [dao-sqlserver], reemplazamos DLL y [webarticles-dao.dll] por DLL y [webarticles-dao.dll], generados a partir del proyecto [dao-sqlserver]
  • modificamos el archivo de configuración [spring-config.xml] para instanciar la nueva clase [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>

Comentarios:

  • en la línea 7, el objeto [articlesdao] ahora está asociado a una instancia de la clase [ ArticlesDaoSqlServeur]
  • esta clase tiene un constructor con cuatro argumentos:
  • el nombre de la instancia MSDE utilizada —línea 9
  • el nombre de la base de datos —línea 12—
  • la identidad con la que se accederá a la base de datos —línea 15—
  • la contraseña asociada a esta identidad —línea 18—

Aquí retomamos la información de la fuente MSDE que creamos anteriormente.

2.4.3.3. Pruebas

Ya estamos listos para las pruebas. Con la aplicación [Nunit-Gui], cargamos el archivo DLL [test-webarticles-dao.dll] de la carpeta [tests] mencionada anteriormente y ejecutamos la prueba [testGetAllArticles]:

Image

A pesar del nombre [NUnitTestArticlesDaoArrayList] que se le dio inicialmente a la clase de prueba y que se ha conservado, ya que utilizamos DLL y [tests-webarticles-dao.dll], que provienen de esta clase, es precisamente la clase [ArticlesDaoSqlserver] la que se está probando aquí. La captura de pantalla muestra que hemos recuperado correctamente los artículos que habíamos colocado en la tabla [ARTICLES]. Ahora, realicemos todas las pruebas:

Image

En la ventana de la izquierda, se ve la lista de los métodos probados. El color del punto que precede al nombre de cada método indica si el método se ha ejecutado correctamente (verde) o ha fallado (rojo). El lector que vea este documento en pantalla podrá comprobar que todas las pruebas se han realizado con éxito.

2.4.4. Integración de la nueva capa [dao] en la aplicación [webarticles]

Seguimos el procedimiento explicado en el párrafo 2.3.4. Realizamos las siguientes modificaciones al contenido de la carpeta [runtime]:

  • en la carpeta [bin], la DLL de laantigua capa [dao] se reemplaza por la DLL de la nueva capa [dao] implementada por la clase [ArticlesDaoSqlServer]
  • en [runtime], el archivo de configuración [web.config] se reemplaza por un archivo que toma en cuenta la nueva clase de implementación:
<?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>

Comentarios:

  • las líneas 15-33 asocian al singleton [articlesDao] una instancia de la nueva clase [ArticlesDaoSqlServer]. Esta es la única modificación. Ya la hemos visto durante las pruebas de la nueva capa [dao]

Estamos listos para las pruebas. Mantenemos la misma configuración del servidor web [Cassini] que antes. Inicializamos la tabla de artículos [MSDE] con los siguientes valores:

Image

Asegúrese de que el servidor web Cassini, así como SGBD y MSDE (en este caso, la instancia portable1_tahe\msde140405), estén en ejecución. Con un navegador, accedemos a URL y [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES] en la base de datos [MSDE]:

Image

Los artículos [ballon foot] y [raquette tennis] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [rollers] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencias. Invitamos al lector a realizar pruebas adicionales.

2.4.5. La clase de implementación [ArticlesDaoOleDb]

2.4.5.1. Los e es de fuentes de datos OleDb

La tercera implementación de la capa [dao] supone que los datos se encuentran en una base de datos accesible a través de un controlador OleDb. El principio de las fuentes OleDb es análogo al de las fuentes ODBC. Un programa que utiliza una fuente OleDb lo hace a través de una interfaz estándar común a todas las fuentes OleDb. Cambiar de fuente OleDb consiste simplemente en cambiar de controlador OleDb. El código no se modifica.

Es posible conocer los controladores OleDb disponibles en su equipo gracias a Visual Studio:

  • abre el explorador de servidores con [Affichage/Explorateur de serveurs]:

Image

  • Para agregar una nueva conexión, haz clic con el botón derecho en [Connexion de données] y selecciona la opción [Ajouter une connexion]. Aparecerá un asistente con el que podrás definir las características de la conexión:

Image

  • El panel [Fournisseur] muestra la lista de controladores OLEDB disponibles. Para la nueva capa [dao], utilizaremos un controlador [Microsoft Jet 4.0 OLE DB Provider] que da acceso a las bases de datos ACCESS.
  • Salgamos momentáneamente de Visual Studio para crear la base de datos ACCESS [articles.mdb], que contiene únicamente la siguiente tabla:

Image

  • La estructura de la tabla es la siguiente:
id
numérico - entero - clave primaria
nom
texto - 20 caracteres -
prix
numérico - real doble
stockactuel
numérico - entero
stockminimum
numérico - entero
  • Volvamos a Visual Studio y creemos una nueva conexión como se explicó anteriormente:

Image

  • Seleccionamos el controlador [Microsoft Jet 4.0] y pasamos al panel [Connexion]:

Image

  • Con el botón [1], selecciona la base de datos ACCESS que acabas de crear y, luego, finaliza la configuración de la conexión con el botón [Terminer]. La conexión creada ahora aparece en la lista de conexiones disponibles:

Image

  • Al hacer doble clic en la tabla [ARTICLES], se puede acceder a su contenido:

Image

  • A continuación, podemos agregar, modificar o eliminar filas en la tabla.
  • Selecciona en el explorador de servidores la nueva conexión para acceder a su hoja de propiedades:

Image

  • Es útil conocer la cadena de conexión. Nos servirá para conectarnos a la base de datos:
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
  • De esta cadena, solo tomaremos en cuenta los siguientes elementos:
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;

2.4.5.2. El código de la clase [ArticlesDaoOleDb]

La clase [ArticlesDaoOleDb] es muy similar a la clase [ArticlesDaoPlainODBC] que analizamos anteriormente. Por lo tanto, solo indicaremos los cambios respecto a la versión anterior:

  • las clases necesarias se encuentran en el espacio de nombres [System.Data.OleDb] en lugar del espacio de nombres [System.Data.Odbc]
  • la conexión de tipo [OdbcConnection] ahora tiene el tipo [OleDbConnection]
  • los objetos [OdbcCommand] ahora tienen el tipo [OleDbCommand]

El constructor de la clase admite como único parámetro la cadena de conexión a la base de datos:

         ' constructor
        Public Sub New(ByVal connectString As String)
             ' connectString: cadena de conexión a la fuente OleDb
             'se instancia la conexión
            connexion = New OleDbConnection(connectString)
             ' se preparan las consultas SQL
...
        End Sub

El código completo de la clase [ArticlesDaoOleDb] es el siguiente:

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

Namespace istia.st.articles.dao

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

         ' campos privados
        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

         ' constructor
        Public Sub New(ByVal connectString As String)
             ' connectString: cadena de conexión a la fuente OleDb
             'se instancia la conexión
            connexion = New OleDbConnection(connectString)
             ' se preparan las consultas SQL
            insertCommand = New OleDbCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
            updatecommand = New OleDbCommand("update ARTICLES set nom=?, prix=?, stockactuel=?, stockminimum=? where id=?", connexion)
            deleteSomeCommand = New OleDbCommand("delete from ARTICLES where id=?", connexion)
            selectSomeCommand = New OleDbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=?", connexion)
            updateStockCommand = New OleDbCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)
            selectAllCommand = New OleDbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New OleDbCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de inserción
                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
                     'se ejecuta
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'error en la consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización de inventario
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("mvt1", mouvement))
                    .Add(New OleDbParameter("id", idArticle))
                    .Add(New OleDbParameter("mvt2", mouvement))
                End With
                 'se ejecuta
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                Try
                     'se ejecuta la consulta de inserción
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                Try
                     'se está ejecutando la consulta SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'se devuelve la lista
                    Return articles
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'se comprueba si se ha encontrado el artículo
                    If articles.Count = 0 Then Return Nothing
                     'se devuelve el artículo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'error en la consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización
                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
                 ' se ejecuta
                Try
                     'se ejecuta la consulta de inserción
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' Se prepara la consulta de eliminación
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta de eliminación
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' ejecución de una consulta SELECT 
             ' declaración del objeto que permite el acceso a todas las filas de la tabla de resultados
            Dim myReader As OleDbDataReader = Nothing
            Try
                 'se crea una conexión a la BDD
                connexion.Open()
                 ': se ejecuta la consulta
                myReader = query.ExecuteReader()
                 'se declara una lista de artículos para devolverla posteriormente
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'se prepara un artículo con los valores del lector
                    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)
                     ': se agrega el artículo a la lista
                    articles.Add(unArticle)
                End While
                 'se devuelve el resultado
                Return articles
            Finally
                 ' liberación de recursos
                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
             ' ejecución de una solicitud de actualización
            Try
                 ': se crea una conexión con BDD
                connexion.Open()
                 'se ejecuta la consulta
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' liberación de recursos
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Se invita al lector a leer este código a la luz de los comentarios sobre la clase [ArticlesDaoPlainODBC] realizados anteriormente.

2.4.5.3. Generación del ensamblado de la capa [dao]

El nuevo proyecto de Visual Studio tiene la siguiente estructura:

Image

El proyecto está configurado para generar un archivo DLL denominado [webarticles-dao.dll]:

2.4.5.4. Pruebas Nunit de la capa [dao]

2.4.5.4.1. La clase de prueba NUnit

La clase de prueba NUnit de la clase de implementación [ArticlesDaoOleDb] es la misma que la de la clase [ArticlesDaoPlainODBC] (véase el párrafo 2.3.3.2). Seguimos un procedimiento similar para preparar la prueba NUnit de la clase:

  • creamos en la carpeta de Visual Studio del proyecto [dao-oledb] la carpeta [tests] (a la derecha) copiando la carpeta [tests] del proyecto [dao-odbc] (a la izquierda):
  • en la carpeta [tests] del proyecto [dao-oledb], reemplazamos DLL y [webarticles-dao.dll] por DLL y [webarticles-dao.dll], que proviene de la generación del proyecto [dao-oledb]
  • modificamos el archivo de configuración [spring-config.xml] para instanciar la nueva clase [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>

Comentarios:

  • línea 7, el objeto [articlesdao] ahora está asociado a una instancia de la clase [ ArticlesDaoOleDb]
  • esta clase tiene un constructor con un argumento: la cadena de conexión a la base de datos OleDb ACCESS - línea 9
2.4.5.4.2. Tests

Estamos listos para las pruebas. Con la ayuda de la aplicación [Nunit-Gui], cargamos el archivo DLL [test-webarticles-dao.dll] de la carpeta [tests] mencionada anteriormente y ejecutamos la prueba [testGetAllArticles]:

Image

A pesar del nombre [NUnitTestArticlesDaoArrayList] que se le asignó inicialmente a la clase de prueba, lo que se está probando aquí es, de hecho, la clase [ArticlesDaoOleDb]. La captura de pantalla muestra que hemos recuperado correctamente los artículos que habíamos colocado en la tabla [ARTICLES]. Ahora, ejecutemos todas las pruebas:

Image

El lector que vea este documento en pantalla podrá comprobar que todas las pruebas se han realizado con éxito (color verde).

2.4.5.5. Integración de la nueva capa [dao] en la aplicación [webarticles]

Seguimos el procedimiento explicado en el párrafo 2.3.4. Realizamos las siguientes modificaciones en el contenido de la carpeta [runtime]:

  • En el archivo [bin], la DLL de laantigua capa [dao] se reemplaza por la DLL de la nueva capa [dao] implementada por la clase [ArticlesDaoOleDb]
  • en [runtime], el archivo de configuración [web.config] se reemplaza por un archivo que toma en cuenta la nueva clase de implementación:
<?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>

Comentarios:

  • las líneas 14-18 asocian al singleton [articlesDao] una instancia de la nueva clase [ArticlesDaoOleDb]. Esta es la única modificación.

Mantenemos la misma configuración del servidor web [Cassini] que antes. Inicializamos la tabla de artículos con los siguientes valores:

Image

Asegúrate de que la base de datos de artículos no esté siendo utilizada por un programa como Visual Studio o ACCESS. Con un navegador, solicitamos el URL [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES] con ACCESS:

Image

Los artículos [pantalon] y [jupe] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [manteau] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencia. Invitamos al lector a realizar pruebas adicionales.

2.5. La clase de implementación [ArticlesDaoFirebirdProvider]

2.5.1. El proveedor de acceso Firebird-net-provider

Ya hemos utilizado una fuente de datos [Firebird] a través de un controlador ODBC. Si bien aportan una gran reutilización al código que los emplea, los controladores ODBC son, sin embargo, menos eficientes que los controladores escritos específicamente para el SGBD al que están dirigidos. El SGBD [Firebird] se puede utilizar mediante una biblioteca de clases específicas que se puede descargar del sitio web de Firebird [http://firebird.sourceforge.net/]. La página de descargas ofrece los siguientes enlaces (abril de 2005):

Image

El enlace [firebird-net-provider] es el que se debe utilizar para descargar las clases .NET de acceso a SGBD Firebird. La instalación del paquete genera una carpeta similar a la siguiente:

Image

Hay dos elementos que nos interesan:

  • [FirebirdSql.Data.Firebird.dll]: el ensamblado que contiene las clases .NET de acceso a SGBD de Firebird
  • [FirebirdNETProviderSDK.chm]: la documentación sobre estas clases

Posteriormente, para que un proyecto de Visual Studio pueda utilizar estas clases, haremos dos cosas:

  • colocaremos el ensamblado [FirebirdSql.Data.Firebird.dll] en la carpeta [bin] del proyecto
  • agregaremos ese mismo ensamblado a las referencias del proyecto

2.5.2. El código de la clase [ArticlesDaoFirebirdProvider]

La clase [ArticlesDaoFirebirdProvider] es muy similar a la clase [ArticlesDaoSqlServer] que analizamos anteriormente. Por lo tanto, solo indicaremos los cambios respecto a esa versión:

  • las clases necesarias se encuentran en el espacio de nombres [FirebirdSql.Data.Firebird] en lugar del espacio de nombres [System.Data.SqlClient]
  • la conexión de tipo [SqlConnection] ahora tiene el tipo [FbConnection]
  • los objetos [SqlCommand] ahora tienen el tipo [FbCommand]
  • los objetos [SqlParameter] ahora tienen el tipo [FbParameter]

El constructor de la clase admite cuatro parámetros, con los cuales construye la cadena de conexión a la base de datos:

         ' constructor
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' servidor: nombre de la máquina host de SGBD
             ' databaseName: ruta de acceso a la base de datos
             ' uid: identidad del usuario que inicia sesión
             ' contraseña: su contraseña
...
        End Sub

El código completo de la clase [ArticlesDaoFirebirdProvider] es el siguiente:

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

Namespace istia.st.articles.dao

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

         ' campos privados
        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

         ' fabricante
        Public Sub New(ByVal serveur As String, ByVal databasePath As String, ByVal uid As String, ByVal password As String)
             ' servidor: nombre de la máquina host de SGBD Firebird
             ' databaseName: ruta de acceso a la base de datos que se va a utilizar
             ' uid: identidad del usuario que se conecta a la base de datos
             ' contraseña: su contraseña

             'se recupera el nombre de la base de datos pasado como argumento
            Me.databasePath = databasePath
             ': se establece la conexión
            Dim connectString As String = String.Format("DataSource={0};Database={1};User={2};Password={3}", serveur, databasePath, uid, password)
            connexion = New FbConnection(connectString)
             ' se preparan las consultas SQL
            insertCommand = New FbCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
            updatecommand = New FbCommand("update ARTICLES set nom=@nom, prix=@prix, stockactuel=@sa, stockminimum=@sm where id=@id", connexion)
            deleteSomeCommand = New FbCommand("delete from ARTICLES where id=@id", connexion)
            selectSomeCommand = New FbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=@id", connexion)
            updateStockCommand = New FbCommand("update ARTICLES set stockactuel=stockactuel+@mvt where id=@id and (stockactuel+@mvt)>=0", connexion)
            selectAllCommand = New FbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New FbCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sección exclusiva
            SyncLock Me
                 ': se prepara la consulta de inserción
                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
                     'se ejecuta
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'error en la consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización del inventario
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@mvt", mouvement))
                    .Add(New FbParameter("@id", idArticle))
                End With
                 'se ejecuta
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                Try
                     'se ejecuta la consulta de inserción
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                Try
                     'se está ejecutando la consulta SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'se devuelve la lista
                    Return articles
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'se verifica si se ha encontrado el artículo
                    If articles.Count = 0 Then Return Nothing
                     'se devuelve el artículo
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'error en la consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' se prepara la consulta de actualización
                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
                 ' se ejecuta
                Try
                     'se ejecuta la consulta de inserción
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' sección exclusiva
            SyncLock Me
                 ' Se prepara la consulta de eliminación
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 'se ejecuta
                Try
                     'se ejecuta la consulta de eliminación
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'error de consulta
                    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
             ' ejecución de una consulta SELECT 
             ' declaración del objeto que permite el acceso a todas las filas de la tabla de resultados
            Dim myReader As FbDataReader = Nothing
            Try
                 'se crea una conexión a la BDD
                connexion.Open()
                 ': se ejecuta la consulta
                myReader = query.ExecuteReader()
                 'se declara una lista de artículos para devolverla posteriormente
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'se prepara un artículo con los valores del lector
                    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)
                     ': se agrega el artículo a la lista
                    articles.Add(unArticle)
                End While
                 'se devuelve el resultado
                Return articles
            Finally
                 ' liberación de recursos
                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
             ' ejecución de una solicitud de actualización
            Try
                 ': se establece una conexión con BDD
                connexion.Open()
                 'se ejecuta la consulta
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' liberación de recursos
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Se invita al lector a leer este código a la luz de los comentarios sobre la clase [ArticlesDaoSqlServer] realizados anteriormente.

2.5.3. Generación del ensamblado de la capa [dao]

El nuevo proyecto de Visual Studio tiene la siguiente estructura:

Image

Cabe destacar la presencia del ensamblado [FirebirdSql.Data.Firebird.dll] en las referencias del proyecto. Este DLL se ha colocado en la carpeta [bin] del proyecto. El proyecto está configurado para generar un DLL llamado [webarticles-dao.dll]:

2.5.4. Pruebas Nunit de la capa [dao]

2.5.4.1. La clase de prueba NUnit

La clase de prueba Nunit de la clase de implementación [ArticlesDaoFirebirdProvider] es la misma que la de la clase [ArticlesDaoPlainODBC] (véase el párrafo 2.3.3.2). Seguimos un procedimiento similar para preparar la prueba Nunit de la clase [ArticlesDaoFirebirdProvider]:

  • creamos en la carpeta de Visual Studio del proyecto [dao-firebird-provider] la carpeta [tests] (a la derecha) copiando la carpeta [bin] del proyecto de pruebas de la capa [dao-odbc] (a la izquierda):
  • en la carpeta [tests] reemplazamos DLL y [webarticles-dao.dll] por DLL y [webarticles-dao.dll], generados a partir del proyecto [dao-firebird-provider]
  • modificamos el archivo de configuración [spring-config.xml] para instanciar la nueva clase [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>

Comentarios:

  • en la línea 7, el objeto [articlesdao] ahora está asociado a una instancia de la clase [ArticlesDaoFirebirdProvider]
  • esta clase tiene un constructor con cuatro argumentos
  • la máquina host de SGBD —línea 9
  • la ruta de acceso a la base de datos Firebird —línea 12
  • el nombre de usuario que inicia sesión —línea 15—
  • su contraseña —línea 18—

2.5.4.2. Pruebas

La tabla [ARTICLES] de la fuente de datos se llena con los siguientes artículos (utilice IBExpert):

Image

Estamos listos para las pruebas. Con la aplicación [Nunit-Gui], cargamos el archivo DLL y [test-webarticles-dao.dll] de la carpeta [tests] mencionada anteriormente y ejecutamos la prueba [testGetAllArticles]:

Image

A pesar del nombre [NUnitTestArticlesDaoArrayList] que se le asignó inicialmente a la clase de prueba, en realidad es la clase [ArticlesDaoFirebirdProvider] la que se está probando aquí. La captura de pantalla muestra que hemos recuperado correctamente los artículos que habíamos colocado en la tabla [ARTICLES]. Ahora, ejecutemos todas las pruebas:

Image

El lector que vea este documento en pantalla podrá observar que todas las pruebas se han superado (color verde). Lo que no puede ver es que las pruebas se han ejecutado mucho más rápido que con la base de artículos a la que se accedía mediante un controlador ODBC de nuestra primera implementación.

2.5.5. Integración de la nueva capa [dao] en la aplicación [webarticles]

Seguimos el procedimiento ya explicado en dos ocasiones, en particular en el párrafo 2.3.4. Realizamos las siguientes modificaciones al contenido de la carpeta [runtime]:

  • en la carpeta [bin], la DLL de laantigua capa [dao] se reemplaza por la DLL de la nueva capa [dao] implementada por la clase [ArticlesDaoFirebirdProvider]. También colocamos allí la clase DLL necesaria para Firebird [FirebirdSql.Data.Firebird.dll]:

Image

  • En [runtime], el archivo de configuración [web.config] se reemplaza por un archivo que toma en cuenta la nueva clase de implementación:
<?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>

Comentarios:

  • las líneas 14-27 asocian al singleton [articlesDao] una instancia de la nueva clase [ArticlesDaoFirebirdProvider]. Esta es la única modificación.

Estamos listos para las pruebas. Configuramos el servidor web [Cassini] como en las pruebas anteriores. Inicializamos la tabla de artículos con los siguientes valores:

Image

Con un navegador solicitamos el URL [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES]:

Image

Los artículos [crayon bille] y [ramette 50 feuilles] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [stylo plume] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencia. Invitamos al lector a realizar pruebas adicionales.

2.5.6. La clase de implementación [ArticlesDaoSqlMap]

2.5.6.1. El producto Ibatis SqlMap

Hemos escrito cuatro implementaciones diferentes de la capa [dao] de nuestra aplicación [webarticles]. En cada caso, pudimos integrar la nueva capa [dao] en la aplicación [webarticles] sin necesidad de recompilar las otras dos capas, [web] y [domain]. Esto se logró, recordemos, gracias a dos decisiones de arquitectura:

  • el acceso a las capas a través de interfaces
  • la integración de las capas mediante Spring

Queremos ir un poco más allá. Aunque son diferentes, nuestras cuatro implementaciones de la capa [dao] presentan similitudes sorprendentes. Una vez escrita la primera implementación, las otras tres se obtuvieron prácticamente mediante copiar y pegar y la sustitución de ciertas palabras clave por otras. La lógica, por su parte, no se modificó. Cabe preguntarse si no sería posible contar con una implementación que nos liberara de los distintos modos de acceso a los datos. Hemos utilizado cuatro:

  • acceso mediante un controlador ODBC a una fuente de datos ODBC
  • acceso directo a una base de datos SQL Server
  • acceso mediante un controlador Ole Db a una fuente de datos Ole Db
  • acceso directo a una base de datos Firebird

La herramienta Ibatis SqlMap [[http://www.ibatis.com/] permite desarrollar capas de acceso a datos que sean independientes de la naturaleza real de la fuente de datos. El acceso a los datos se garantiza mediante:

  • archivos de configuración en los que se almacena la información que define la fuente de datos y las operaciones que se desean realizar sobre ella
  • una biblioteca de clases que se basa en esta información para acceder a los datos

La herramienta Ibatis SqlMap se desarrolló inicialmente para la plataforma Java. Su adaptación a la plataforma .NET es reciente y, al parecer, presenta algunos errores (opinión personal que requeriría una verificación exhaustiva). Sin embargo, dado que la herramienta ha demostrado su eficacia en la plataforma Java, parece interesante presentar su versión .NET.

2.5.6.2. ¿Dónde encontrar , IBATIS y SqlMap?

El sitio web principal de Firebird es [http://www.ibatis.com/]. La página de descargas ofrece los siguientes enlaces:

Image

Elegiremos el enlace [Stable Binaries], que nos lleva a [SourceForge.net]. Seguimos el proceso de descarga hasta el final. Obtenemos un archivo zip que contiene los siguientes archivos:

Image

En un proyecto de Visual Studio que utiliza Ibatis SqlMap, hay que hacer dos cosas:

  • colocar los archivos mencionados anteriormente en la carpeta [bin] del proyecto
  • agregar al proyecto una referencia a cada uno de estos archivos

2.5.6.3. Los archivos de configuración de Ibatis SqlMap

Se definirá una fuente de datos [SqlMap] mediante los siguientes archivos de configuración:

  1. providers.config: define las bibliotecas de clases que se utilizarán para acceder a los datos
  2. sqlmap.config: define las características de la conexión que se establecerá
  3. archivos de mapeo: definen las operaciones que se realizarán sobre los datos

La lógica de estos archivos es la siguiente:

  • Para acceder a los datos, necesitaremos una conexión. Para representarla, ya hemos visto varias clases: OdbcConnection, SqlConnection, OleDbConnection, FbConnection. También necesitaremos un objeto [Command] para enviar las solicitudes SQL: OdbcCommand, SqlCommand, OleDbCommand, FbCommand. Etc. En el archivo [providers.config], definimos todas las clases que necesitamos.
  • El archivo [sqlmap.config] define esencialmente la cadena de conexión a la base de datos que contiene los datos. La conexión a la base de datos se abrirá mediante la instanciación de la clase [Connection] definida en [providers.config], a cuyo constructor se le pasará la cadena de conexión definida en [sqlmap.config].
  • Los archivos de mapeo definen:
    • las asociaciones entre las filas de las tablas de datos y la clase .NET, cuyas instancias contendrán dichas filas
    • las operaciones SQL que se deben ejecutar. Estas se identifican mediante un nombre. El código .NET ejecuta estas operaciones mediante su nombre, lo que tiene como consecuencia la eliminación de todo el código SQL del código .NET.

2.5.6.4. Los archivos de configuración del proyecto [dao-sqlmap]

Veamos, con un ejemplo, la naturaleza exacta de los archivos de configuración de SqlMap. Consideraremos el caso en el que la fuente de datos es la fuente ODBC de Firebird del párrafo 2.3.3.1.

2.5.6.4.1. providers.config

El archivo [providers.config] para una fuente ODBC es el siguiente:

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

Comentarios:

  • Un archivo [providers.config] se distribuye junto con el paquete de [SqlMap]. Ofrece varios proveedores de acceso (provider) estándar. El código anterior proviene directamente de este archivo.
  • Un <provider> tiene un nombre —línea 6— que puede ser cualquiera
  • Un <provider> puede estar activado ([enabled=true]) o no ([enabled=false]). Si está activado, debe ser accesible el DLL al que se hace referencia en la línea 8. Un archivo [providers.config] puede tener varias etiquetas <provider>.
  • línea 8: nombre del ensamblado que contiene las clases definidas líneas 9-15
  • línea 9: clase que se utilizará para crear una conexión
  • línea 10: clase que se utilizará para crear un objeto [Command] para emitir comandos SQL
  • línea 11: clase que se debe utilizar para gestionar los parámetros de un comando SQL configurado
  • línea 12: clase de enumeración de los tipos de datos posibles para los campos de una tabla
  • línea 13: nombre de la propiedad de un objeto [Parameter] que contiene el tipo del valor de este parámetro
  • línea 14: nombre de la clase [Adapter] que permite crear objetos [DataSet] a partir de la fuente de datos
  • línea 15: nombre de la clase [CommandBuilder] que, asociada a un objeto [Adapter], permite generar automáticamente las propiedades [InsertCommand, DeleteCommand, UpdateCommand] de este a partir de su propiedad [SelectCommand]
  • líneas 16 a 19: se define cómo se gestionan los comandos SQL configurados. Según el caso, hay que escribir, por ejemplo:
insert into ARTICLES(id,nom,prix,stockactuel,stockminimum) values (?,?,?,?,?)

o bien

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

En el primer caso, se trata de parámetros posicionales formales. Los valores efectivos de estos deben proporcionarse en el orden de los parámetros formales. En el segundo caso, se trata de parámetros con nombre. Se proporciona un valor a dicho parámetro especificando su nombre. El orden ya no tiene importancia.

  • línea 16: se indica que las fuentes ODBC utilizan parámetros posicionales
  • líneas 17-19: se refieren a los parámetros con nombre. Aquí no hay ninguno.

Esta información le permite a SqlMap saber, por ejemplo, qué clase debe instanciar para crear una conexión. En este caso, será la clase [OdbcConnection] (línea 9).

2.5.6.4.2. sqlmap.config

El archivo [providers.config] define las clases que se deben utilizar para acceder a una fuente ODBC. No indica ninguna fuente ODBC. Es el archivo [sqlmap.config] el que lo hace:

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

Comentarios:

  • línea 3: se define un archivo de propiedades [properties.xml]. Este define pares (clave, valor). Las claves pueden ser cualesquiera. El valor asociado a una clave C se obtiene mediante la notación ${C} en [sqlmap.config]. A continuación se muestra el archivo [properties.xml] que se asociará al archivo [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>

línea 3: se define la clave [provider]. Su valor es el nombre de la etiqueta <provider> que se utilizará en [providers.config]

línea 4: se define la clave [connectionString]. Su valor es la cadena de conexión que se utilizará para establecer una conexión con la fuente de datos ODBC de Firebird.

  • líneas 4-7: parámetros de configuración:
    • línea 5: las consultas SQL se identificarán mediante un nombre que, a su vez, puede formar parte de un espacio de nombres. [useStatementNamespaces="false"] indica que no se utilizarán espacios de nombres.
    • línea 6: SqlMap cuenta con diferentes estrategias de caché para minimizar los accesos a la fuente de datos. [cacheModelsEnabled="false"] indica que no se utilizará ninguna.
  • líneas 9-13: se definen las características de la fuente de datos:
    • línea 10: nombre del <provider> de [providers.config] que se utilizará
    • línea 11: cadena de conexión a la fuente de datos
    • línea 12: administrador de transacciones. Aquí no lo hemos utilizado, pero hemos dejado la línea de todos modos porque estaba en el archivo de distribución estándar.
  • líneas 14-16: lista de los archivos que definen las operaciones SQL que se realizarán en la fuente de datos.
  • línea 15: define el archivo de mapeo [articles.xml]
2.5.6.4.3. articles.xml

Este archivo cumple dos funciones:

  • definir una asignación de objetos de las tablas de la fuente de datos. En los casos más sencillos, esto equivale a asociar una clase a una fila de una tabla.
  • definir operaciones SQL configuradas y asignarles un nombre.

Usaremos el siguiente archivo [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">
     <!-- los 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>
     <!-- las consultas SQL -->
    <statements>
         <!-- obtención de todos los artículos -->
        <select id="getAllArticles" resultMap="article">
                select ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM FROM ARTICLES
            </select> 
         <!-- eliminación de todos los artículos-->
        <delete id="clearAllArticles">
                delete from ARTICLES
            </delete> 
         <!-- inserción de un artículo -->
        <insert id="insertArticle" parameterClass="istia.st.articles.dao.Article">
                insert into ARTICLES (id, nom, prix,stockactuel, stockminimum) values
                ( #(id#, #nombre#, #precio#, #stockactual#, #stockmínimo#)
            </insert>
         <!-- eliminación de un artículo -->
        <delete id="deleteArticle" parameterClass="int">
                delete FROM ARTICLES where ID= #valor#
            </delete>
         <!-- modificación de un artículo -->
        <update id="modifyArticle" parameterClass="istia.st.articles.dao.Article">
                update ARTICLES set NOM= #nombre#, PRIX= #precio#, STOCKACTUEL= #stockactual# ,STOCKMINIMUM= #stockmínimo# donde ID= #id#
            </update>
         <!-- búsqueda de un artículo específico -->
        <select id="getArticleById" resultMap="article" parameterClass="int">
                select ID, NOM, PRIX,    STOCKACTUEL, STOCKMINIMUM FROM ARTICLES where ID= #valor#
            </select>
         <!-- cambio en el stock de un artículo -->
        <update id="changerStockArticle" parameterClass="Hashtable">
                update ARTICLES set STOCKACTUEL=(STOCKACTUEL + #movimiento#) donde ID=#id# y ((STOCKACTUEL + #movimiento#) >=0)
      </update>
    </statements>
</sqlMap>

Comentarios:

  • líneas 4-11: se define una correspondencia entre una fila de la tabla [ARTICLES] de la fuente de datos y la clase [istia.st.articles.dao.Article]. A cada columna (column) de la tabla se le asocia una propiedad (property) de la clase [Article]. Esta correspondencia permite que [SqlMap] genere el resultado de una operación SQL SELECT. Cada fila resultante de SELECT se colocará en un objeto [Article] según las reglas de la asignación.
  • Línea 5: el mapeo se delinea con la etiqueta <resultMap> y se nombra mediante el atributo [id="article"]. La clase asociada se designa mediante el atributo [class="istia.st.articles.dao.Article"].
  • líneas 14-44: se definen las operaciones SQL que se necesitan
  • líneas 16-18: se define una operación SELECT denominada [getAllArticles]
    • línea 16: la operación SELECT se denomina [name= "getAllArticles "] y la asignación que se debe utilizar se define mediante el atributo [resultMap="article"]. Por lo tanto, aquí se hace referencia a la asignación de las líneas 5 a 11
    • línea 17: texto del comando SQL que se debe ejecutar
  • líneas 20-22: se define el comando SQL-Delete [clearAllArticles] destinado a vaciar la tabla de artículos.
  • líneas 24-27: se define el comando SQL-Insert [insertArticle] destinado a agregar un nuevo artículo a la tabla de artículos. Se trata de una consulta parametrizada por los elementos (#id#, #nombre#, #precio#, #stockactual#, #stockmínimo#). Los valores de estos cinco elementos provendrán de un objeto [Article] pasado como parámetro: [parameterClass="istia.st.articles.dao.Article"]. El objeto parámetro debe tener las propiedades (id, nombre, precio, stockactual, stockmínimo) a las que hace referencia el comando SQL configurado.
  • líneas 29-31: se define el comando SQL Delete [deleteArticle], destinado a eliminar un artículo cuyo número #value# se conoce. Este número se pasará como parámetro: [parameterClass="int"]. Esta es una regla general. Cuando el parámetro es único, se hace referencia a él mediante la palabra clave #value# en el texto del comando SQL.
  • líneas 33-35: se define el comando SQL-Update [modifyArticle] destinado a modificar un artículo cuyo número se conoce. Al igual que en el comando [insertArticle], los cinco datos necesarios provendrán de las propiedades de un objeto [istia.st.articles.dao.Article].
  • líneas 37-39: se define el comando SQL-Select [getArticleById], que permite obtener la fila de un artículo cuyo número se conoce.
  • líneas 41-43: se define la instrucción SQL-Update [changerStockArticle], que modifica el campo [stockactuel] de un artículo cuyo número se conoce. Los dos datos necesarios, el n.º #id# del artículo y el incremento #movimiento# del inventario, se encontrarán en un diccionario: [parameterClass="Hashtable"]. Este deberá tener dos claves: id y movimiento. Los valores asociados a estas dos claves serán los que se utilicen en el comando SQL.
2.5.6.4.4. Ubicación de los archivos de configuración

Veremos dos situaciones diferentes:

  • en el caso de una prueba de Nunit, los archivos de configuración de [SqlMap] se colocarán en la misma carpeta que los binarios que se están probando.
  • En el caso de una aplicación web, se colocarán en la raíz de la aplicación.

2.5.6.5. El API de SqlMap

Las clases de SqlMap se encuentran en DLL, que por lo general se coloca en la carpeta [bin] de la aplicación:

Image

Las aplicaciones que utilizan las clases de SqlMap deben importar el espacio de nombres [IBatisNet.DataMapper]:

Imports IBatisNet.DataMapper

Todas las operaciones de SQL se realizan a través de un singleton de tipo [Mapper], una clase del espacio de nombres [IBatisNet.DataMapper ]. El singleton se obtiene de la siguiente manera:

        Dim mappeur As SqlMapper = Mapper.Instance

Para ejecutar el comando SqlMap [getAllArticles], se escribirá:

                    dim articles as IList=mappeur.QueryForList("getAllArticles", Nothing)
  • El método [QueryForList] permite obtener el resultado de un comando SELECT en una lista
  • el primer parámetro es el nombre del comando SQL que se va a ejecutar (véase articles.xml)
  • El segundo parámetro es el parámetro que se debe transmitir a la consulta SQL. Debe corresponder al atributo [parameterClass] del comando SqlMap. En [articles.xml], tenemos [parameterClass=Nothing]. Por lo tanto, aquí se pasa un puntero nulo.
  • El resultado es de tipo IList. Los objetos de esta lista se indican mediante el atributo [resultMap] de la instrucción SQL-select: [resultMap="article"]. «article» es un nombre de mapeo:
<resultMap id="article" class="istia.st.articles.dao.Article">

La clase asociada a esta asignación es [istia.st.articles.dao.Article]. Al final, la variable [articles] definida anteriormente es una lista de objetos [ istia.st.articles.dao.Article]. Así, hemos obtenido la tabla completa [ARTICLES] en una sola instrucción. Si la tabla [ARTICLES] está vacía, se obtiene un objeto [IList] con 0 elementos.

Para ejecutar el comando SqlMap [getArticleById], se escribirá:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • El método [QueryForObject] permite obtener el resultado de un comando SELECT que solo devuelve una línea
  • el primer parámetro es el nombre del comando SqlMap que se va a ejecutar
  • el segundo parámetro es el parámetro que se debe transmitir a la consulta SQL. Debe corresponder al atributo [parameterClass] del comando SqlMap. En [articles.xml], tenemos [parameterClass="int"]. Por lo tanto, aquí se pasa un entero que representa el número del artículo buscado.
  • El resultado es de tipo Object. Si el comando SELECT no devolvió ninguna fila, el resultado es un puntero nulo (nothing).

Para ejecutar el comando SqlMap [insertArticle], escribiremos:

                    mappeur.Insert("insertArticle", unArticle)
  • El método [Insert] permite ejecutar los comandos SQL y INSERT
  • el primer parámetro es el nombre del comando SqlMap que se va a ejecutar
  • el segundo parámetro es el parámetro que se le debe pasar. Debe corresponder al atributo [parameterClass] del comando SqlMap. En [articles.xml], tenemos [parameterClass="istia.st.articles.dao.Article"]. Por lo tanto, aquí se pasa un objeto de tipo [istia.st.articles.dao.Article].

Para ejecutar el comando SqlMap [deleteArticle], se escribirá:

                    dim nbArticles as Integer=mappeur.Delete("deleteArticle", idArticle)
  • El método [Delete] permite ejecutar los comandos SQL y DELETE
  • el primer parámetro es el nombre del comando SQL que se va a ejecutar
  • el segundo parámetro es el parámetro que se le debe pasar. Debe corresponder al atributo [parameterClass] del comando SqlMap. En [articles.xml], tenemos [parameterClass="int"]. Por lo tanto, aquí se pasa el número del artículo que se va a eliminar.
  • El resultado del método [Delete] es el número de líneas eliminadas

De manera análoga, para ejecutar el comando SqlMap [clearAllArticles], se escribirá:

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

Para ejecutar el comando SqlMap [modifyArticle], se escribirá:

                    dim nbArticles as Integer=mappeur.Update("modifyArticle", unArticle)
  • El método [Update] permite ejecutar los comandos SQL y UPDATE
  • el primer parámetro es el nombre del comando SqlMap que se va a ejecutar
  • el segundo parámetro es el parámetro que se le debe pasar. Debe corresponder al atributo [parameterClass] del comando SqlMap. En [articles.xml], tenemos [parameterClass="istia.st.articles.dao.Article"]. Por lo tanto, aquí se pasa un objeto de tipo [istia.st.articles.dao.Article].
  • El resultado del método [Update] es el número de líneas modificadas.

De manera análoga, para ejecutar el comando SqlMap [changerStockArticle], se escribirá:

                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' actualización
                    dim nbLignes as Integer= mappeur.Update("changerStockArticle", paramètres)
  • El segundo parámetro corresponde al atributo [parameterClass] del comando SqlMap. En [articles.xml], tenemos [parameterClass="Hashtable"]. El comando SQL, configurado con [changerStockArticle], utiliza los parámetros de [id, mouvement]. Por lo tanto, aquí se pasa un diccionario que contiene estas dos claves.

2.5.6.6. El código de la clase [ArticlesDaoSqlMap]

Tras las explicaciones anteriores, ahora podemos escribir la siguiente nueva clase de implementación [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

         ' campos privados
        Dim mappeur As SqlMapper = Mapper.Instance

         ' lista de todos los artículos
        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

         ' agregar un artículo
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            SyncLock Me
                Try
                     ' unArticle: artículo para agregar
                     ' inserción
                    mappeur.Insert("insertArticle", unArticle)
                    Return 1
                Catch ex As Exception
                    Throw New Exception("Echec de l'ajout de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' elimina un artículo
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id: identificación del artículo que se va a eliminar
                     ' eliminación
                    Return mappeur.Delete("deleteArticle", idArticle)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la suppression de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' modifica un artículo
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' actualización
                    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

         ' búsqueda de un artículo
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            SyncLock Me
                Try
                     ' id: identificador del artículo buscado
                    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

         ' eliminación de todos los artículos
        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

         ' modificar el inventario de un artículo
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id: id del artículo cuyo stock se modifica
                     ' movimiento: movimiento de inventario
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' actualización
                    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

Se invita al lector a leer este código a la luz de las explicaciones dadas para el API del SqlMap. Cabe destacar que el uso de [SqlMap] ha reducido considerablemente la cantidad de código que hay que escribir.

2.5.6.7. Generación del ensamblado de la capa [dao]

El nuevo proyecto de Visual Studio tiene la siguiente estructura:

Image

Cabe destacar la presencia de los «ensambles» necesarios para SqlMap en las referencias del proyecto. Estos DLL se han colocado en la carpeta [bin] del proyecto. El proyecto está configurado para generar un archivo DLL llamado [webarticles-dao.dll]:

2.5.6.8. Pruebas Nunit de la capa [dao]

2.5.6.8.1. La clase de prueba NUnit

La clase de prueba Nunit de la clase de implementación [ArticlesDaoSqlMap] es la misma que la de la clase [ArticlesDaoPlainODBC] (véase el párrafo 2.3.3.2). Seguimos un procedimiento similar para preparar la prueba Nunit de la clase [ArticlesDaoSqlMap]:

  • creamos en la carpeta de Visual Studio del proyecto [dao-sqlmap] la carpeta [test1] (a la derecha) copiando la carpeta [tests] del proyecto [dao-odbc] (a la izquierda):
  • en la carpeta [tests], reemplazamos DLL y [webarticles-dao.dll] por DLL y [webarticles-dao.dll], generados a partir del proyecto [dao-sqlmap].
  • Agregamos los DLL necesarios para SqlMap, así como los archivos de configuración analizados [providers.config, sqlmap.config, properties.xml, articles.xml].
  • Modificamos el archivo de configuración [spring-config.xml] para instanciar la nueva clase [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>

Comentarios:

  • En la línea 7, el objeto [articlesdao] ahora está asociado a una instancia de la clase [ArticlesDaoSqlMap]
  • esta clase no tiene constructor. Se utilizará el constructor por defecto.
2.5.6.8.2. Tests

La tabla [ARTICLES] de la fuente de datos Firebird se ha llenado con los siguientes elementos:

Image

Estamos listos para las pruebas. Con la ayuda de la aplicación [Nunit-Gui], cargamos el archivo DLL [test-webarticles-dao.dll] de la carpeta [test1] mencionada anteriormente y ejecutamos la prueba [testGetAllArticles]:

Image

A pesar del nombre [NUnitTestArticlesDaoArrayList] que se le asignó inicialmente a la clase de prueba, en realidad es la clase [ArticlesDaoSqlMap] la que se está probando aquí. La captura de pantalla muestra que hemos recuperado correctamente los artículos que habíamos colocado en la tabla [ARTICLES]. Ahora, ejecutemos todas las pruebas:

Image

El lector que vea este documento en pantalla podrá observar que algunas pruebas han tenido éxito (color verde), pero que otras han fallado (color rojo). Las pruebas que fallaron son las pruebas [testArticleAbsent] y [testChangerStockArticle]. Tras una larga investigación, parece que las causas de estos fallos son las siguientes:

  • en [testArticleAbsent], se solicita modificar un artículo que no existe. Para ello se utiliza el método [modifieArticle], que devuelve el número de filas modificadas, es decir, 0 o 1. En este caso, debería ser 0. En cambio, se produce una excepción de tipo [IBatisNet.Common.Exceptions.ConcurrentException].
  • En [changerStockArticle] se presenta nuevamente una operación del tipo [update]. Se trata de reducir un stock en una cantidad mayor que el stock mismo. Para ello, se utiliza el método [changerStockArticle], que devuelve el número de filas modificadas, es decir, 0 o 1. El comando SQL se escribió para evitar una actualización (véase el comando SQL «changerStockArticle» en articles.xml) que haría que el stock fuera negativo. Aquí se espera obtener 0 como resultado del método [changerStockArticle]. Una vez más, se produce una excepción de tipo [IBatisNet.Common.Exceptions.ConcurrentException].

Las posibles causas del error son numerosas:

  1. el código de la clase [ArticlesDaoSqlMap] es erróneo. Es posible. Sin embargo, proviene de una adaptación de una clase de Java que había funcionado correctamente con la versión Java de SqlMap.
  2. la versión .NET de SqlMap tiene un error
  3. El controlador ODBC de Firebird tiene un error
  4. ...

A falta de certezas, vamos a sortear el obstáculo interceptando la famosa excepción [IBatisNet.Common.Exceptions.ConcurrentException]. El nuevo código de la clase [ArticlesDaoSqlMap] queda así:

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

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

         ' campos privados
        Dim mappeur As SqlMapper = Mapper.Instance

         ' lista de todos los artículos
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
...
        End Function

         ' agregar un artículo
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

         ' elimina un artículo
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id: identificación del artículo que se va a eliminar
                     ' eliminación
                    Return mappeur.Delete("deleteArticle", idArticle)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception("Erreur lors de la suppression de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' modifica un artículo
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' actualización
                    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

         ' búsqueda de un artículo
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
...
        End Function

         ' eliminación de todos los artículos
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
....
        End Sub

         ' cambio del inventario de un artículo
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id: identificación del artículo cuyo inventario se modifica
                     ' movimiento: movimiento de inventario
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' actualización
                    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

Los cambios se encuentran en las líneas: 28, 41, 69. Para las operaciones SQL del tipo [UPDATE, DELETE], si se produce una excepción del tipo [IBatisNet.Common.Exceptions.ConcurrentException], se devuelve 0 como resultado, lo que indica que no se ha modificado ni eliminado ninguna línea. Una vez hecho esto, se regenera el archivo DLL del proyecto, se coloca en la carpeta [test1] y se vuelven a ejecutar las pruebas NUnit:

Image

Esta vez sí. A partir de ahora trabajaremos con este DLL.

2.5.6.9. Integración de la nueva capa [dao] en la aplicación [webarticles]

2.5.6.9.1. fuente de datos ODBC

Aquí probamos la fuente de datos ODBC analizada en el párrafo 2.3.3.1. Aquí se utiliza a través de SqlMap.

Seguimos el procedimiento del párrafo 2.3.4. Realizamos las siguientes modificaciones al contenido de la carpeta [runtime]:

  • en la carpeta [bin], la DLL de laantigua capa [dao] se reemplaza por la DLL de la nueva capa [dao] implementada por la clase [ArticlesDaoSqlMap]. A esto le agregamos las clases DLL necesarias para Firebird y SqlMap:

Image

  • En [runtime], colocamos los archivos de configuración de SqlMap y [providers.config, sqlmap.config, properties.xml, articles.xml]:

Image

  • en [runtime], el archivo de configuración [web.config] se reemplaza por un archivo que toma en cuenta la nueva clase de implementación:
<?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>

Comentarios:

  • En la línea 14 se asocia al singleton [articlesDao] una instancia de la nueva clase [ArticlesDaoSqlMap]. Este es el único cambio.

Estamos listos para las pruebas. Configuramos el servidor web [Cassini] como en las pruebas anteriores. Inicializamos la tabla de artículos con los siguientes valores:

Image

Con un navegador solicitamos el URL [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES]:

Image

Los artículos [couteau] y [cuiller] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [fourchette] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencia. Invitamos al lector a realizar pruebas adicionales.

2.5.6.9.2. fuente de datos MSDE

Aquí probamos la fuente de datos MSDE analizada en el párrafo 2.4.3.1. Aquí se utiliza a través de SqlMap. Seguimos el mismo procedimiento que anteriormente. Realizamos las siguientes modificaciones al contenido del archivo [runtime]:

  • el contenido de la carpeta [bin] no cambia
  • en [runtime], los archivos de configuración de SqlMap y [providers.config, properties.xml] cambian. Los archivos de configuración de [sqlmap.config, articles.xml] no cambian.
  • El archivo [providers.config] configura un nuevo <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 las clases .NET para acceder a las fuentes de datos del servidor SQL. Está integrado de forma predeterminada en el archivo de plantilla [providers.config] que se distribuye junto con SqlMap.

  • El archivo [properties.xml] define el <provider> de la fuente MSDE, así como su cadena de conexión:
<?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>
  • En [runtime], el archivo de configuración [web.config] no cambia.

Estamos listos para las pruebas. El servidor web [Cassini] mantiene su configuración habitual. Inicializamos la tabla de artículos de la fuente MSDE con [EMS MS SQL Manager]:

Image

Con un navegador solicitamos el URL [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES] junto con [EMS MS SQL Manager]:

Image

Los artículos [ballon foot] y [raquette tennis] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [rollers] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencia. Invitamos al lector a realizar pruebas adicionales.

2.5.6.9.3. fuente de datos OleDb

Aquí probamos la fuente de datos ACCESS presentada en el párrafo 2.4.5.1. Aquí se utiliza a través de SqlMap. Seguimos el mismo procedimiento que anteriormente. Realizamos las siguientes modificaciones al contenido del archivo [runtime]:

  • el contenido de la carpeta [bin] no cambia
  • en [runtime], los archivos de configuración de SqlMap y [providers.config, properties.xml] cambian. Los archivos de configuración de [sqlmap.config, articles.xml] no cambian.
  • El archivo [providers.config] configura un nuevo <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 las clases .NET para acceder a las fuentes de datos OleDb. Está integrado de forma predeterminada en el archivo de plantilla [providers.config] que se distribuye junto con SqlMap.

  • El archivo [properties.xml] define el <provider> de la fuente OleDb, así como su cadena de conexión:
<?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>
  • En [runtime], el archivo de configuración [web.config] no cambia.

Estamos listos para las pruebas. El servidor web [Cassini] mantiene su configuración habitual. Inicializamos la tabla de artículos de la fuente ACCESS de la siguiente manera:

Image

Con un navegador solicitamos el URL [http://localhost/webarticles/main.aspx]:

Image

Ahora revisemos el contenido de la tabla [ARTICLES] con:

Image

Los artículos [pantalon] y [jupe] se compraron y sus existencias se redujeron en la cantidad comprada. El artículo [manteau] no se pudo comprar porque la cantidad solicitada excedía la cantidad en existencia. Invitamos al lector a realizar pruebas adicionales.

2.5.7. Conclusión

Con esto concluimos este extenso artículo-tutorial. ¿Qué hemos hecho?

  • Hemos implementado la capa [dao] de una aplicación web de tres capas de cuatro maneras diferentes:
    1. utilizando las clases de acceso .NET a las fuentes ODBC
    2. utilizando las clases de acceso .NET para las fuentes SQL Server
    3. utilizando las clases de acceso .NET a las fuentes OleDb
    4. utilizando clases de acceso de terceros para acceder a una base de datos Firebird
  • en cada ocasión, integramos la nueva capa [dao] en la aplicación de tres capas [webarticles] [web, domain, dao] sin necesidad de recompilar ninguna de las capas [web, domain]
  • Finalmente, presentamos la herramienta [SqlMap], que nos permitió crear una capa [dao] capaz de adaptarse a diferentes fuentes de datos de manera transparente para el código. De este modo, con esta nueva capa, pudimos utilizar sucesivamente las fuentes de datos de las implementaciones 1 a 3 anteriores. Esto se logró de manera transparente mediante archivos de configuración.
  • Hemos demostrado la gran flexibilidad que aportan las herramientas Spring y SqlMap a las aplicaciones web de tres capas.