17. Aplicación web MVC en una arquitectura de tres capas – Ejemplo 3 – Sistema de gestión de bases de datos Firebird
17.1. La base de datos Firebird
En esta nueva versión, instalaremos la lista de personas en una tabla de la base de datos Firebird. En el documento [http://tahe.developpez.com/divers/sql-firebird/] se encuentra información para instalar y administrar este SGBD. A continuación, las capturas de pantalla provienen de IBExpert, un cliente de administración de SGBD para Interbase y Firebird.
La base de datos se llama [dbpersonnes.gdb]. Contiene una tabla [PERSONNES]:

La tabla [PERSONNES] contendrá la lista de personas administradas por la aplicación web. Se creó con las siguientes órdenes SQL:
- líneas 2-10: la estructura de la tabla [PERSONNES], destinada a almacenar objetos de tipo [Personne], refleja la estructura de dicho objeto. Dado que el tipo booleano no existe en Firebird, el campo [MARIE] (línea 8) se ha declarado como de tipo [SMALLINT], un entero. Su valor será 0 (soltero) o 1 (casado).
- líneas 13-16: restricciones de integridad que reflejan las del validador de datos [ValidatePersonne].
- línea 19: el campo ID es la clave primaria de la tabla [PERSONNES]
La tabla [PERSONNES] podría tener el siguiente contenido:

La base de datos [dbpersonnes.gdb] cuenta, además de la tabla [PERSONNES], con un objeto denominado generador y denominado [GEN_PERSONNES_ID]. Este generador produce números enteros sucesivos que utilizaremos para asignar un valor a la clave primaria [ID] de la clase [PERSONNES]. Veamos un ejemplo para ilustrar su funcionamiento:
![]() |
![]() |
Se puede observar que el valor del generador [GEN_PERSONNES_ID] ha cambiado (haga doble clic en él + F5 para actualizar):
El orden SQL
por lo tanto, permite obtener el siguiente valor del generador [GEN_PERSONNES_ID]. GEN_ID es una función interna de Firebird y [RDB$DATABASE], una tabla del sistema de este SGBD.
17.2. El proyecto de Eclipse de las capas [dao] y [service]
Para desarrollar las capas [dao] y [service] de nuestra aplicación con base de datos, utilizaremos el siguiente proyecto de Eclipse [mvc-personnes-03]:

El proyecto es un proyecto Java simple, no un proyecto web de Tomcat. Recordemos que la versión 2 de nuestra aplicación utilizará la capa [web] de la versión 1. Por lo tanto, no es necesario escribir esta capa.
Carpeta [src]
Esta carpeta contiene los códigos fuente de las capas [dao] y [service]:

En él se encuentran diferentes paquetes:
- [istia.st.mvc.personnes.dao]: contiene la capa [dao]
- [istia.st.mvc.personnes.entites]: contiene la clase [Personne]
- [istia.st.mvc.personnes.service]: contiene la clase [service]
- [istia.st.mvc.personnes.tests]: contiene las pruebas JUnit de las capas [dao] y [service]
así como los archivos de configuración que deben estar en el ClassPath de la aplicación.
Carpeta [database]
Esta carpeta contiene la base de datos Firebird de personas:
![]()
- [dbpersonnes.gdb] es la base de datos.
- [dbpersonnes.sql] es el script SQL para generar la base de datos:
Carpeta [lib]
Esta carpeta contiene los archivos necesarios para la aplicación:
![]() |
Cabe destacar la presencia del controlador JDBC [firebirdsql-full.jar] del Firebird SGBD, así como de varios archivos [spring-*.jar]. Podríamos haber utilizado el único archivo [spring.jar] que se encuentra en la carpeta [dist] de la distribución y que contiene todas las clases de Spring. También se pueden utilizar únicamente los archivos necesarios para el proyecto. Eso es lo que hicimos aquí, guiándonos por los errores de clases ausentes señalados por Eclipse y los nombres de los archivos parciales de Spring. Todos estos archivos de la carpeta [lib] se colocaron en el archivo Classpath del proyecto.
Carpeta [dist]
Esta carpeta contendrá los archivos resultantes de la compilación de las clases de la aplicación:
![]()
- [personnes-dao.jar]: archivo de la capa [dao]
- [personnes-service.jar]: archivo de la capa [service]
17.3. La capa [dao]
17.3.1. Los componentes de la capa [dao]
La capa [dao] está compuesta por las siguientes clases e interfaces:

- [IDao] es la interfaz que presenta la capa [dao]
- [DaoImplCommon] es una implementación de esta en la que el grupo de personas se encuentra en una tabla de base de datos. [DaoImplCommon] agrupa funcionalidades independientes de SGBD.
- [DaoImplFirebird] es una clase derivada de [DaoImplCommon] para administrar específicamente una base de datos Firebird.
- [DaoException] es el tipo de las excepciones no controladas, lanzadas por la capa [dao]. Esta clase corresponde a la versión 1.
La interfaz [IDao] es la siguiente:
- La interfaz cuenta con los mismos cuatro métodos que en la versión anterior.
La clase [DaoImplCommon] que implementa esta interfaz será la siguiente:
- líneas 8-9: la clase [DaoImpl] implementa la interfaz [IDao] y, por lo tanto, los cuatro métodos [getAll, getOne, saveOne, deleteOne].
- líneas 27-37: el método [saveOne] utiliza dos métodos internos, [insertPersonne] y [updatePersonne], dependiendo de si se debe agregar o modificar una persona.
- línea 50: el método privado [check] es el de la versión anterior. No volveremos a él.
- línea 8: para implementar la interfaz [IDao], la clase [DaoImpl] deriva de la clase Spring [SqlMapClientDaoSupport].
17.3.2. La capa de acceso a datos [iBATIS]
La clase de Spring [SqlMapClientDaoSupport] utiliza un marco de trabajo de terceros [Ibatis SqlMap] disponible en la URL [http://ibatis.apache.org/]:

[iBATIS] es un proyecto de Apache que facilita la construcción de capas [dao] basadas en bases de datos. Con [iBATIS], la arquitectura de la capa de acceso a datos es la siguiente:
![]() |
[iBATIS] se inserta entre la capa [dao] de la aplicación y el controlador JDBC de la base de datos. Existen alternativas a [iBATIS], como, por ejemplo, la alternativa [Hibernate]:

![]() |
El uso del marco [iBATIS] requiere dos archivos [ibatis-common, ibatis-sqlmap], los cuales se han colocado en la carpeta [lib] del proyecto:
![]() |
La clase [SqlMapClientDaoSupport] encapsula la parte genérica del uso de los marcos [iBATIS] y c.a.d. de las partes de código que se encuentran en todas las capas [dao] que utilizan la herramienta [iBATIS]. Para escribir la parte no genérica del código, es decir, lo que es específico de la capa [dao] que estamos escribiendo, basta con derivar la clase [SqlMapClientDaoSupport]. Eso es lo que hacemos aquí.
La clase [SqlMapClientDaoSupport] se define de la siguiente manera:

Entre los métodos de esta clase, uno de ellos permite configurar el cliente [iBATIS] con el que se va a utilizar la base de datos:
![]()
El objeto [SqlMapClient sqlMapClient] es el objeto [IBATIS] que se utiliza para acceder a una base de datos. Por sí solo, implementa la capa [iBATIS] de nuestra arquitectura:
![]() |
Una secuencia típica de acciones con este objeto es la siguiente:
- solicitar una conexión a un grupo de conexiones
- abrir una transacción
- ejecutar una serie de órdenes SQL almacenadas en un archivo de configuración
- cerrar la transacción
- devolver la conexión al grupo
Si nuestra implementación [DaoImplCommon] trabajara directamente con [iBATIS], tendría que repetir esta secuencia una y otra vez. Solo la operación 3 es específica de una capa [dao]; las demás operaciones son genéricas. La clase Spring [SqlMapClientDaoSupport] se encargará por sí misma de las operaciones 1, 2, 4 y 5, delegando la operación 3 a su clase derivada, en este caso la clase [DaoImplCommon].
Para poder funcionar, la clase [SqlMapClientDaoSupport] necesita una referencia al objeto iBATIS [SqlMapClient sqlMapClient], que se encargará de la comunicación con la base de datos. Este objeto necesita dos cosas para funcionar:
- un objeto [DataSource] conectado a la base de datos, al que solicitará conexiones
- un (o varios) archivo(s) de configuración donde se externalizan las órdenes SQL que se deben ejecutar. De hecho, estas no se encuentran en el código Java. Se identifican mediante un código en un archivo de configuración y el objeto [SqlMapClient sqlMapClient] utiliza ese código para ejecutar una orden SQL específica.
Un esbozo de la configuración de nuestra capa [dao] que reflejaría la arquitectura anterior sería el siguiente:
<!-- las clases de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
Aquí se inicializa la propiedad [sqlMapClient] (línea 3) de la clase [DaoImplCommon] (línea 2). Esto se realiza mediante el método [setSqlMapClient] de la clase [DaoImpl]. Esta clase no cuenta con ese método. Es su clase padre, [SqlMapClientDaoSupport], la que lo tiene. Por lo tanto, es esta última la que, en realidad, se inicializa aquí.
Ahora, en la línea 4, se hace referencia a un objeto llamado «sqlMapClient» que aún debe crearse. Este, como ya se mencionó, es de tipo [SqlMapClient], un tipo [iBATIS]:

[SqlMapClient] es una interfaz. Spring ofrece la clase [SqlMapClientFactoryBean] para obtener un objeto que implemente esta interfaz:

Recordemos que buscamos instanciar un objeto que implemente la interfaz [SqlMapClient]. Al parecer, este no es el caso de la clase [SqlMapClientFactoryBean]. Esta implementa la interfaz [FactoryBean] (véase más arriba). Dicha interfaz cuenta con el siguiente método [getObject()]:
![]()
Cuando se le solicita a Spring una instancia de un objeto que implemente la interfaz [FactoryBean], este:
- crea una instancia [I] de la clase; en este caso, crea una instancia de tipo [SqlMapClientFactoryBean].
- devuelve al método que lo invocó el resultado del método [I].getObject() —el método [SqlMapClientFactoryBean].getObject() devolverá aquí un objeto que implementa la interfaz [SqlMapClient].
Para poder devolver un objeto que implemente la interfaz [SqlMapClient], la clase [SqlMapClientFactoryBean] necesita dos datos imprescindibles para dicho objeto:
- un objeto [DataSource] conectado a la base de datos, al que le solicitará conexiones
- un (o varios) archivo(s) de configuración donde se externalizan las órdenes SQL que se deben ejecutar
La clase [SqlMapClientFactoryBean] cuenta con los métodos set para inicializar estas dos propiedades:

Vamos avanzando... Nuestro archivo de configuración se va definiendo y queda así:
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- clases de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
- líneas 2-3: el bean «sqlMapClient» es de tipo [SqlMapClientFactoryBean]. Por lo que acabamos de explicar, sabemos que cuando le pedimos a Spring una instancia de este bean, obtenemos un objeto que implementa la interfaz iBATIS [SqlMapClient]. Por lo tanto, es este último objeto el que se obtendrá en la línea 14.
- líneas 7-9: indicamos que el archivo de configuración necesario para el objeto iBATIS [SqlMapClient] se llama «sql-map-config-firebird.xml» y que debe buscarse en el ClassPath de la aplicación. Aquí se utiliza el método [SqlMapClientFactoryBean].setConfigLocation.
- Líneas 4-6: inicializamos la propiedad [dataSource] de [SqlMapClientFactoryBean] con su método [setDataSource].
En la línea 5, hacemos referencia a un bean llamado «dataSource» que aún no se ha creado. Si observamos el parámetro que espera el método [setDataSource] de [SqlMapClientFactoryBean], vemos que es de tipo [DataSource]:

Una vez más, nos encontramos con una interfaz para la que debemos encontrar una clase de implementación. La función de dicha clase es proporcionar a una aplicación, de manera eficiente, conexiones a una base de datos específica. Un SGBD no puede mantener abiertas simultáneamente un gran número de conexiones. Para reducir el número de conexiones abiertas en un momento dado, debemos, en cada intercambio con la base de datos:
- abrir una conexión
- iniciar una transacción
- enviar órdenes SQL
- cerrar la transacción
- cerrar la conexión
Abrir y cerrar conexiones repetidamente requiere mucho tiempo. Para resolver estos dos problemas (limitar tanto el número de conexiones abiertas en un momento dado como el costo de abrir y cerrar dichas conexiones), las clases que implementan la interfaz [DataSource] suelen proceder de la siguiente manera:
- al ser instanciadas, abren N conexiones con la base de datos de destino. N suele tener un valor por defecto y, en la mayoría de los casos, se puede definir en un archivo de configuración. Estas N conexiones permanecerán abiertas todo el tiempo y forman un grupo de conexiones disponibles para los hilos de la aplicación.
- Cuando un hilo de la aplicación solicita abrir una conexión, el objeto [DataSource] le asigna una de las N conexiones abiertas al inicio, si aún quedan disponibles. Cuando la aplicación cierra la conexión, en realidad esta no se cierra, sino que simplemente se devuelve al grupo de conexiones disponibles.
Existen diversas implementaciones de la interfaz [DataSource] disponibles de forma gratuita. Aquí utilizaremos la implementación [commons DBCP], disponible en la URL [http://jakarta.apache.org/commons/dbcp/]:

El uso de la herramienta [commons DBCP] requiere dos archivos [commons-dbcp, commons-pool], los cuales se han colocado en la carpeta [lib] del proyecto:
![]() |
La clase [BasicDataSource] de [commons DBCP] proporciona la implementación [DataSource] que necesitamos:

Esta clase nos proporcionará un grupo de conexiones para acceder a la base de datos Firebird [dbpersonnes.gdb] de nuestra aplicación. Para ello, debemos proporcionarle la información que necesita para crear las conexiones del grupo:
- el nombre del controlador JDBC que se utilizará – inicializado con [setDriverClassName]
- el nombre de la URL de la base de datos que se utilizará —inicializado con [setUrl]—
- el nombre de usuario del propietario de la conexión – inicializado con [setUsername] (y no con setUserName como cabría esperar)
- su contraseña: inicializada con [setPassword]
El archivo de configuración de nuestra capa [dao] podría ser el siguiente:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- Atención: no dejes espacios entre las dos etiquetas <value> de la URL -->
<property name="url">
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- clase de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
- líneas 7-9: el nombre del controlador JDBC de Firebird SGBD
- líneas 11-13: la URL de la base de datos Firebird [dbpersonnes.gdb]. Se debe prestar especial atención a cómo se escribe. No debe haber ningún espacio entre las etiquetas <value> y la URL.
- líneas 14-16: el propietario de la conexión; en este caso, [sysdba], que es el administrador predeterminado de las distribuciones de Firebird
- líneas 17-19: su contraseña, [masterkey], que también es el valor por defecto
Hemos avanzado mucho, pero aún quedan algunos puntos de configuración por aclarar: la línea 28 hace referencia al archivo [sql-map-config-firebird.xml], que debe configurar el cliente [SqlMapClient] de iBATIS. Antes de analizar su contenido, veamos dónde se encuentran estos archivos de configuración en nuestro proyecto de Eclipse:

- [spring-config-test-dao-firebird.xml] es el archivo de configuración de la capa [dao] que acabamos de analizar
- [sql-map-config-firebird.xml] es referenciado por [spring-config-test-dao-firebird.xml]. Lo analizaremos.
- [personnes-firebird.xml] es referenciado por [sql-map-config-firebird.xml]. Lo vamos a analizar.
Los tres archivos anteriores se encuentran en la carpeta [src]. En Eclipse, esto significa que, al ejecutarse, estarán presentes en la carpeta [bin] del proyecto (no mostrada arriba). Esta carpeta forma parte del ClassPath de la aplicación. Al final, los tres archivos anteriores estarán presentes en el ClassPath de la aplicación. Esto es necesario.
El archivo [sql-map-config-firebird.xml] es el siguiente:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
- Este archivo debe tener <sqlMapConfig> como etiqueta raíz (líneas 6 y 8)
- línea 7: la etiqueta <sqlMap> sirve para identificar los archivos que contienen las órdenes SQL que se deben ejecutar. A menudo, aunque no es obligatorio, hay un archivo por tabla. Esto permite reunir las órdenes SQL de una tabla determinada en un mismo archivo. Sin embargo, con frecuencia se encuentran órdenes SQL que involucran varias tablas. En ese caso, la estructura anterior ya no se aplica. Solo hay que recordar que todos los archivos designados por las etiquetas <sqlMap> se fusionarán. Estos archivos se buscan en el archivo ClassPath de la aplicación.
El archivo [personnes-firebird.xml] describe las órdenes SQL que se emitirán en la tabla [PERSONNES] de la base de datos Firebird [dbpersonnes.gdb]. Su contenido es el siguiente:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap>
<!-- alias de la clase [Personne] -->
<typeAlias alias="Personne.classe"
type="istia.st.mvc.personnes.entites.Personne"/>
<!-- tabla de mapeo [PERSONNES] - objeto [Personne] -->
<resultMap id="Personne.map"
class="Personne.classe">
<result property="id" column="ID" />
<result property="version" column="VERSION" />
<result property="nom" column="NOM"/>
<result property="prenom" column="PRENOM"/>
<result property="dateNaissance" column="DATENAISSANCE"/>
<result property="marie" column="MARIE"/>
<result property="nbEnfants" column="NBENFANTS"/>
</resultMap>
<!-- lista de todas las personas -->
<select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
<!-- obtener una persona en particular -->
<select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#valor#</select>
<!-- agregar una persona -->
<insert id="Personne.insertOne" parameterClass="Personne.classe">
<selectKey keyProperty="id">
SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
</selectKey>
insert into
PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS)
VALUES(#id#, #versión#, #apellido#, #nombre#, #dateNaissance#, #esposa#,
#nbEnfants#) </insert>
<!-- actualizar una persona -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#versión#+1, NOM=#apellido#, PRENOM=#nombre#, DATENAISSANCE=#dateNaissance#,
MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# y
VERSION=#versión#</update>
<!-- eliminar a una persona -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#valor# </eliminar>
</sqlMap>
- el archivo debe tener <sqlMap> como etiqueta raíz (líneas 7 y 45)
- líneas 9-10: para facilitar la escritura del archivo, se le asigna el alias (sinónimo) [Personne.classe] a la clase [istia.st.springmvc.personnes.entites.Personne].
- líneas 12-21: establece las correspondencias entre las columnas de la tabla [PERSONNES] y los campos del objeto [Personne].
- líneas 23-24: la orden SQL [select] para obtener todas las personas de la tabla [PERSONNES]
- líneas 26-27: la orden SQL [select] para obtener una persona específica de la tabla [PERSONNES]
- líneas 29-36: la orden SQL [insert], que inserta una persona en la tabla [PERSONNES]
- líneas 38-41: la orden SQL [update] que actualiza una persona de la tabla [PERSONNES]
- líneas 42-44: la orden SQL [delete] que elimina a una persona de la tabla [PERSONNES]
La función y el significado del contenido del archivo [personnes-firebird.xml] se explicarán mediante el análisis de la clase [DaoImplCommon], que implementa la capa [dao].
17.3.3. La clase [DaoImplCommon]
Volvamos a la arquitectura de acceso a los datos:
![]() |
La clase [DaoImplCommon] es la siguiente:
Vamos a analizar los métodos uno por uno.
getAll
Este método permite obtener todas las personas de la lista. Su código es el siguiente:
Recordemos, en primer lugar, que la clase [DaoImplCommon] deriva de la clase Spring [SqlMapClientDaoSupport]. Es esta clase la que tiene el método [getSqlMapClientTemplate()] utilizado en la línea 3 anterior. Este método tiene la siguiente firma:
![]()
El tipo [SqlMapClientTemplate] encapsula el objeto [SqlMapClient] de la capa [iBATIS]. Es a través de él que se tendrá acceso a la base de datos. El tipo [iBATIS] SqlMapClient podría utilizarse directamente, ya que la clase [SqlMapClientDaoSupport] tiene acceso a él:
![]()
La desventaja de la clase [iBATIS] SqlMapClient es que lanza excepciones de tipo [SQLException], un tipo de excepción controlada, c.a.d. que debe ser manejada mediante un try/catch o declarada en la firma de los métodos que la lanzan. Sin embargo, recordemos que la capa [dao] implementa una interfaz [IDao] cuyos métodos no incluyen excepciones en sus firmas. Por lo tanto, los métodos de las clases que implementan la interfaz [IDao] tampoco pueden tener excepciones en sus firmas. Por lo tanto, debemos interceptar cada excepción [SQLException] lanzada por la capa [iBATIS] y encapsularla en una excepción no controlada. El tipo [DaoException] de nuestro proyecto serviría para esta encapsulación.
En lugar de manejar estas excepciones nosotros mismos, las confiaremos al tipo Spring [SqlMapClientTemplate], que encapsula el objeto [SqlMapClient] de la capa [iBATIS]. De hecho, [SqlMapClientTemplate] se diseñó para interceptar las excepciones [SQLException] lanzadas por la capa [SqlMapClient] y encapsularlas en un tipo [DataAccessException] no controlado. Este comportamiento nos conviene. Solo hay que recordar que la capa [dao] ahora puede lanzar dos tipos de excepciones no controladas:
- nuestro tipo propio [DaoException]
- el tipo de Spring [DataAccessException]
El tipo [SqlMapClientTemplate] se define de la siguiente manera:

Implementa la siguiente interfaz [SqlMapClientOperations]:

Esta interfaz define métodos capaces de procesar el contenido del archivo [personnes-firebird.xml]:
[queryForList]
![]()
Este método permite emitir una orden [SELECT] y recuperar el resultado en forma de una lista de objetos:
- [statementName]: el identificador (id) de la orden [select] en el archivo de configuración
- [parameterObject]: el objeto «parámetro» para un [select] configurado. El objeto «parámetro» puede adoptar dos formas:
- un objeto que cumple con el estándar JavaBean: en ese caso, los parámetros de la orden [select] son los nombres de los campos del JavaBean. Al ejecutar la orden [select], estos se reemplazan por los valores de dichos campos.
- un diccionario: los parámetros de la orden [select] son, en este caso, las claves del diccionario. Al ejecutar la orden [select], estas se sustituyen por sus valores asociados en el diccionario.
- Si [SELECT] no devuelve ninguna fila, el resultado [List] es un objeto vacío de elementos, pero no null (por verificar).
[queryForObject]
![]()
Este método es similar en su funcionamiento al anterior, pero solo devuelve un único objeto. Si [SELECT] no devuelve ninguna fila, el resultado es el puntero null.
[insert]
![]()
Este método permite ejecutar una orden SQL [insert] configurada mediante el segundo parámetro. El objeto devuelto es la clave primaria de la fila que se ha insertado. No es obligatorio utilizar este resultado.
[update]
![]()
Este método permite ejecutar una orden SQL [update] configurada por el segundo parámetro. El resultado es el número de líneas modificadas por la orden SQL [update].
[delete]
![]()
Este método permite ejecutar una orden SQL [delete] configurada mediante el segundo parámetro. El resultado es el número de líneas eliminadas por la orden SQL [delete].
Volvamos al método [getAll] de la clase [DaoImplCommon]:
- línea 4: se ejecuta la orden [select] denominada «Personne.getAll». No tiene parámetros, por lo que el objeto «parámetro» es null.
En [personnes-firebird.xml], la orden [select], denominada «Personne.getAll», es la siguiente:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap>
<!-- alias de clase [Personne] -->
<typeAlias alias="Personne.classe"
type="istia.st.mvc.personnes.entites.Personne"/>
<!-- tabla de correspondencias [PERSONNES] - objeto [Personne] -->
<resultMap id="Personne.map"
class="Personne.classe">
<result property="id" column="ID" />
<result property="version" column="VERSION" />
<result property="nom" column="NOM"/>
<result property="prenom" column="PRENOM"/>
<result property="dateNaissance" column="DATENAISSANCE"/>
<result property="marie" column="MARIE"/>
<result property="nbEnfants" column="NBENFANTS"/>
</resultMap>
<!-- lista de todas las personas -->
<select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
- línea 23: el comando SQL «Personne.getAll» no tiene parámetros (no hay parámetros en el texto de la consulta).
- La línea 3 del método [getAll] solicita la ejecución de la consulta [select] denominada «Personne.getAll». Esta se ejecutará. [iBATIS] se basa en JDBC. Por lo tanto, sabemos que el resultado de la consulta se obtendrá en forma de un objeto [ResultSet]. En la línea 23, el atributo [resultMap] de la etiqueta <select> le indica a [iBATIS] qué «resultMap » debe utilizar para transformar cada línea del [ResultSet] obtenido en un objeto. Es el «resultMap» [Personne.map], definido en las líneas 12 a 21, el que indica cómo pasar de una línea de la tabla [PERSONNES] a un objeto de tipo [Personne]. [iBATIS] utilizará estas correspondencias para proporcionar una lista de objetos [Personne] a partir de las filas del objeto [ResultSet].
- La línea 3 del método [getAll] devuelve entonces una colección de objetos [Personne]
- El método [queryForList] puede lanzar una excepción de Spring [DataAccessException]. Dejamos que se propague.
Explicaremos los demás métodos de la clase [AbstractDaoImpl] más brevemente, ya que lo esencial sobre el uso de [iBATIS] ya se ha abordado en el análisis del método [getAll].
getOne
Este método permite identificar a una persona mediante su [id]. Su código es el siguiente:
- línea 4: solicita la ejecución de la orden [select] denominada «Personne.getOne». Esta es la siguiente en el archivo [personnes-firebird.xml]:
<!-- obtener a una persona en particular -->
<select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM
PERSONNES WHERE ID=#valor#</select>
La orden SQL se configura mediante el parámetro #value# (línea 4). El atributo #value# indica el valor del parámetro pasado a la orden SQL, cuando este parámetro es de tipo simple: Integer, Double, String, ... En los atributos de la etiqueta <select>, el atributo [parameterClass] indica que el parámetro es de tipo entero (línea 2). En la línea 5 de [getOne], se observa que este parámetro es el identificador de la persona buscada en forma de un objeto Integer. Este cambio de tipo es obligatorio, ya que el segundo parámetro de [queryForList] debe ser de tipo [Object].
El resultado de la consulta [select] deberá transformarse en un objeto mediante el atributo [resultMap="Personne.map"] (línea 2). De este modo, se obtendrá un tipo [Personne].
- líneas 7-11: si la consulta [select] no devolvió ninguna fila, se recupera el puntero null de la línea 4. Esto significa que no se encontró a la persona buscada. En este caso, se lanza una consulta [DaoException] con código 2 (líneas 9-10).
- línea 13: si no se ha producido ninguna excepción, se devuelve el objeto [Personne] solicitado.
deleteOne
Este método permite eliminar a una persona identificada por su [id]. Su código es el siguiente:
- líneas 4-5: solicita la ejecución de la orden [delete] denominada «Personne.deleteOne». Esta es la siguiente en el archivo [personnes-firebird.xml]:
<!-- eliminar a una persona -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#valor# </delete>
La orden SQL se configura mediante el parámetro #value# (línea 3) de tipo [parameterClass="int"] (línea 2). Este será el identificador de la persona buscada (línea 5 de deleteOne)
- línea 4: el resultado del método [SqlMapClientTemplate].delete es el número de filas eliminadas.
- líneas 7-8: si la consulta [delete] no ha eliminado ninguna línea, significa que la persona no existe. Se ejecuta un [DaoException] con código 2 (línea 8).
saveOne
Este método permite agregar una nueva persona o modificar una persona existente. Su código es el siguiente:
- línea 4: se verifica la validez de la persona con el método [check]. Este método ya existía en la versión anterior y en ese momento se había comentado. Lanza un [DaoException] si la persona no es válida. Se permite que este se remita.
- línea 6: si llegamos hasta aquí, es porque no se ha producido ninguna excepción. Por lo tanto, la persona es válida.
- líneas 6-11: según el ID de la persona, se trata de una adición (ID = -1) o de una actualización (ID ≠ -1). En ambos casos, se invocan dos métodos internos de la clase:
- insertPersonne: para la adición
- updatePersonne: para la actualización
insertPersonne
Este método permite agregar una nueva persona. Su código es el siguiente:
- línea 4: se establece en 1 el número de versión de la persona que se está creando
- línea 9: se realiza la inserción mediante la consulta denominada «Personne.insertOne», que es la siguiente:
<insert id="Personne.insertOne" parameterClass="Personne.classe">
<selectKey keyProperty="id">
SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
</selectKey>
insert into
PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS)
VALUES(#id#, #versión#, #apellido#, #nombre#, #dateNaissance#, #marie#,
#nbEnfants#) </insert>
Se trata de una consulta parametrizada y el parámetro es de tipo [Personne] (parameterClass = «Personne.classe», línea 1). Los campos del objeto [Personne] pasados como parámetro (línea 9 de insertPersonne) se utilizan para completar las columnas de la fila que se insertará en la tabla [PERSONNES] (líneas 5-8). Tenemos un problema que resolver. Al realizar una inserción, el objeto [Personne] que se va a insertar tiene un identificador igual a -1. Es necesario reemplazar este valor por una clave primaria válida. Para ello, se utilizan las líneas 2 a 4 de la etiqueta <selectKey> anterior. Estas indican:
- (continuación)
- la consulta SQL que se debe ejecutar para obtener un valor de clave primaria. La que se indica aquí es la que presentamos en el párrafo 17.1. Hay dos puntos que cabe destacar:
- «as "value"» es obligatorio. También se puede escribir «as value», pero «value» es una palabra clave de Firebird que debe protegerse con comillas.
- La tabla de Firebird se llama en realidad [RDB$DATABASE]. Sin embargo, el carácter $ es interpretado como [iBATIS]. Se ha protegido duplicándolo.
- El campo del objeto [Personne] que se debe inicializar con el valor obtenido por la orden [SELECT], en este caso el campo [id]. Es el atributo [keyProperty] de la línea 2 el que indica este campo.
- la consulta SQL que se debe ejecutar para obtener un valor de clave primaria. La que se indica aquí es la que presentamos en el párrafo 17.1. Hay dos puntos que cabe destacar:
- líneas 6-7: para fines de prueba, tendremos que esperar 10 ms antes de realizar la inserción, con el fin de verificar si existen conflictos entre subprocesos que intenten realizar adiciones al mismo tiempo.
updatePersonne
Este método permite modificar una persona que ya existe en la tabla [PERSONNES]. Su código es el siguiente:
- Una actualización puede fallar por al menos dos razones:
- la persona que se va a actualizar no existe
- la persona que se va a actualizar existe, pero el hilo que quiere modificarla no tiene la versión correcta
- líneas 7-8: se ejecuta la consulta SQL [update] denominada «Personne.updateOne». Es la siguiente:
<!-- actualizar un perfil -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#versión#+1, NOM=#apellido#, PRENOM=#nombre#, DATENAISSANCE=#dateNaissance#,
MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# y
VERSION=#versión#</update>
- (continuación)
- línea 2: la consulta está configurada y admite como parámetro un tipo [Personne] (parameterClass = «Personne.classe»). Este es el registro que se va a modificar (línea 8 – updatePersonne).
- Solo queremos modificar la persona de la tabla [PERSONNES] que tenga el mismo número [id] y la misma versión [version] que el parámetro. Por eso, tenemos la restricción [WHERE ID=#id# and VERSION=#version#]. Si se encuentra esta persona, se actualiza con la persona del parámetro y su versión se incrementa en 1 (línea 3 arriba).
- línea 9: se recupera el número de líneas actualizadas.
- líneas 10-11: si este número es cero, se ejecuta un [DaoException] con código 2, lo que indica que, o bien la persona que se debe actualizar no existe, o bien ha cambiado de versión entretanto.
17.4. Pruebas de la capa [dao]
17.4.1. Pruebas de la implementación [DaoImplCommon]
Ahora que hemos escrito la capa [dao], nos proponemos probarla con las pruebas JUnit:

Antes de realizar pruebas intensivas, podemos comenzar con un programa sencillo del tipo [main] que mostrará el contenido de la tabla [PERSONNES]. Se trata de la clase [MainTestDaoFirebird]:
El archivo de configuración [spring-config-test-dao-firebird.xml] de la capa [dao], utilizado en las líneas 13 y 14, es el siguiente:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- atención: no dejes espacios entre las dos etiquetas <value> -->
<property name="url">
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- la clase de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
Este archivo es el que se analiza en el párrafo 17.3.2.
Para la prueba, se inicia el Firebird SGBD. El contenido de la tabla [PERSONNES] es el siguiente:

La ejecución del programa [MainTestDaoFirebird] arroja los siguientes resultados en pantalla:

Se ha obtenido correctamente la lista de personas. Podemos pasar a la prueba JUnit.
La prueba JUnit [TestDaoFirebird] es la siguiente:
- Las pruebas [test1] a [test5] son las mismas que en la versión 1, excepto la [test4], que ha cambiado ligeramente. La prueba [test6] es nueva. Solo comentaremos estas dos pruebas.
[test4]
[test4] tiene como objetivo probar el método [updatePersonne - DaoImplCommon]. Recordemos el código de este último:
- líneas 4-5: se espera 10 ms. De esta manera, se obliga al hilo que ejecuta [updatePersonne] a perder el control del procesador, lo que puede aumentar nuestras posibilidades de observar conflictos de acceso entre hilos concurrentes.
[test4] lanza N=100 subprocesos encargados de incrementar, al mismo tiempo, en 1 el número de hijos de la misma persona. Queremos ver cómo se manejan los conflictos de versión y los conflictos de acceso.
Los hilos se crean en las líneas 8 a 13. Cada uno incrementará en 1 el número de hijos de la persona creada en las líneas 3 a 5. Los hilos de actualización [ThreadDaoMajEnfants ] son los siguientes:
Una actualización de una persona puede fallar porque la persona que se desea modificar no existe o porque ya fue actualizada previamente por otro hilo. Estos dos casos se manejan aquí en las líneas 67 a 69. De hecho, en ambos casos, el método [updatePersonne] inicia un [DaoException] con código 2. El hilo volverá entonces a reiniciar el procedimiento de actualización desde el principio (bucle while, línea 34).
[test6]
[test6] tiene como objetivo probar el método [insertPersonne - DaoImplCommon]. Recordemos el código de este último:
- líneas 6-7: esperamos 10 ms para forzar que el hilo que ejecuta [insertPersonne] pierda el control del procesador y así aumentar nuestras posibilidades de que surjan conflictos debido a hilos que realizan inserciones al mismo tiempo.
El código de [test6] es el siguiente:
Creamos 100 hilos que insertarán al mismo tiempo a 100 personas diferentes. Estos 100 hilos obtendrán todos una clave primaria para la persona que deben insertar y luego serán interrumpidos durante 10 ms (línea 10 – insertPersonne) antes de poder realizar su inserción. Queremos verificar que todo funcione correctamente y, en particular, que obtengan valores de clave primaria diferentes.
- líneas 7-11: se crea una tabla con 100 personas. Todas estas personas son copias de la persona p creada en las líneas 4-5.
- líneas 14-17: se inician los 100 subprocesos de inserción. Cada uno de ellos se encarga de insertar a una de las 100 personas creadas anteriormente.
- líneas 19-23: [test6] espera a que finalice cada uno de los 100 hilos que ha iniciado. Cuando detecta que ha finalizado el hilo n.º i, elimina a la persona que ese hilo acaba de insertar.
El hilo de inserción [ThreadDaoInsertPersonne] es el siguiente:
- líneas 19-22: el constructor del hilo almacena la persona que debe insertar y la capa [dao] que debe utilizar para realizar dicha inserción.
- línea 30: se inserta la persona. Si se produce una excepción, esta se transmite a [test6].
Pruebas
En las pruebas, se obtienen los siguientes resultados:
![]() |
Por lo tanto, la prueba [test4] falla. El número de hijos ha pasado a 69 en lugar de los 100 esperados. ¿Qué ha pasado? Analicemos los registros de pantalla. Muestran la existencia de excepciones lanzadas por Firebird:
Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];
--- El error ocurrió en personnes-firebird.xml.
--- El error ocurrió al aplicar un mapa de parámetros.
--- Revisa el Personne.updateOne-InlineParameterMap.
--- Revisa la instrucción (la actualización falló).
--- Causa: org.firebirdsql.jdbc.FBSQLException: Excepción GDS. 335544336. interbloqueo
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:
--- El error ocurrió en personnes-firebird.xml.
--- El error ocurrió al aplicar un mapa de parámetros.
- línea 1 – se produjo una excepción de Spring [org.springframework.jdbc.UncategorizedSQLException]. Se trata de una excepción no controlada que se utilizó para encapsular una excepción lanzada por el controlador JDBC de Firebird, descrita en la línea 6.
- línea 6 – el controlador JDBC de Firebird lanzó una excepción de tipo [org.firebirdsql.jdbc.FBSQLException] y con código de error 335544336.
- línea 7: indica que se produjo un conflicto de acceso entre dos subprocesos que intentaban actualizar al mismo tiempo la misma fila de la tabla [PERSONNES].
No se trata de un error irrecuperable. El hilo que intercepta esta excepción puede volver a intentar la actualización. Para ello, es necesario modificar el código de [ThreadDaoMajEnfants]:
- línea 8: se maneja una excepción de tipo [DaoException]. Según lo dicho, deberíamos manejar la excepción que apareció en las pruebas, el tipo [org.springframework.jdbc.UncategorizedSQLException]. Sin embargo, no basta con gestionar este tipo, que es un tipo genérico de Spring destinado a encapsular excepciones que no reconoce. Spring reconoce las excepciones generadas por los controladores JDBC de varios SGBD, como Oracle, MySQL, Postgres, DB2, SQL Server, ... pero no a las de Firebird. Asimismo, cualquier excepción lanzada por el controlador JDBC de Firebird se encapsula en el tipo de Spring [org.springframework.jdbc.UncategorizedSQLException]:

Como se ve arriba, la clase [UncategorizedSQLException] deriva de la clase [DataAccessException] que mencionamos en el párrafo 17.3.3. Es posible conocer la excepción que se ha encapsulado en [UncategorizedSQLException] gracias a su método [getSQLException]:
![]()
Esta excepción de tipo [SQLException] es la lanzada por la capa [iBATIS], la cual a su vez encapsula la excepción lanzada por el controlador JDBC de la base de datos. La causa exacta de la excepción de tipo [SQLException] se puede obtener mediante el método:
![]()
Se obtiene el objeto de tipo [Throwable] que fue lanzado por el controlador JDBC:

El tipo [Throwable] es la clase padre de [Exception].
Aquí tendremos que verificar que el objeto de tipo [Throwable], lanzado por el controlador JDBC de Firebird y causa de laexcepción [SQLException] lanzada por la capa [iBATIS] sea efectivamente una excepción de tipo [org.firebirdsql.gds.GDSException] y con el código de error 335544336. Para obtener el código de error, podemos utilizar el método [getErrorCode()] de la clase [org.firebirdsql.gds.GDSException].
Si utilizamos en el código de [ThreadDaoMajEnfants] la excepción [org.firebirdsql.gds.GDSException], entonces este hilo solo podrá trabajar con el Firebird SGBD. Lo mismo ocurrirá con la prueba [test4] que utiliza este hilo. Queremos evitar esto. De hecho, deseamos que nuestras pruebas JUnit sigan siendo válidas independientemente del SGBD que se utilice. Para lograr este resultado, decidimos que la capa [dao] iniciará un [DaoException] de código 4 cuando se detecte una excepción de tipo «conflicto de actualización», y esto, independientemente del SGBD subyacente. De este modo, el hilo [ThreadDaoMajEnfants] podrá reescribirse de la siguiente manera:
- líneas 34-36: se intercepta la excepción de tipo [DaoException] con código 4. El hilo [ThreadDaoMajEnfants] se verá obligado a reiniciar el procedimiento de actualización desde el principio (línea 10)
Por lo tanto, nuestra capa [dao] debe ser capaz de reconocer una excepción de tipo «conflicto de actualización». Esta es generada por un controlador JDBC y es específica de él. Esta excepción debe gestionarse en el método [updatePersonne] de la clase [DaoImplCommon]:
Las líneas 7 a 11 deben estar rodeadas por un try / catch. Para el controlador SGBD de Firebird, debemos verificar que la excepción que causó el fallo de la actualización sea del tipo [org.firebirdsql.gds.GDSException] y tenga como código de error 335544336. Si colocamos este tipo de prueba en [DaoImplCommon], vincularemos esta clase al Firebird SGBD, lo cual, obviamente, no es deseable. Si queremos mantener un carácter generalista en la clase [DaoImplCommon], debemos derivarla y manejar la excepción en una clase específica para Firebird. Eso es lo que haremos ahora.
17.4.2. La clase [DaoImplFirebird]
Su código es el siguiente:
- línea 5: la clase [DaoImplFirebird] deriva de [DaoImplCommon], la clase que acabamos de analizar. En las líneas 8 a 33, redefine el método [updatePersonne] que nos está causando problemas.
- líneas 20: interceptamos la excepción de Spring de tipo [UncategorizedSQLException]
- líneas 21-22: verificamos que la excepción subyacente de tipo [SQLException], lanzada por la capa [iBATIS], se debe a una excepción de tipo [org.firebirdsql.jdbc.FBSQLException]
- línea 25: además, se verifica que el código de error de esta excepción de Firebird sea 335544336, el código de error del «deadlock».
- líneas 26-27: si se cumplen todas estas condiciones, se lanza una [DaoException] con código 4.
- líneas 36-44: el método [wait] permite detener el hilo actual durante N milisegundos. Solo es útil para las pruebas.
Estamos listos para las pruebas de la nueva capa [dao].
17.4.3. Pruebas de la implementación [DaoImplFirebird]
El archivo de configuración de pruebas [spring-config-test-dao-firebird.xml] se modifica para utilizar la implementación [DaoImplFirebird]:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- atención: no dejes espacios entre las dos etiquetas <value> -->
<property name="url">
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- la clase de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
- línea 32: la nueva implementación [DaoImplFirebird] de la capa [dao].
Los resultados de la prueba [test4], que anteriormente había fallado, son los siguientes:

[test4] se ejecutó con éxito. Las últimas líneas de los registros de pantalla son las siguientes:
La última línea indica que el hilo n.º 36 fue el último en terminar. La línea 3 muestra un conflicto de versión que obligó al hilo n.º 36 a reiniciar su procedimiento de actualización del usuario (línea 4). Otros registros muestran conflictos de acceso durante las actualizaciones:
La línea 2 muestra que el hilo n.º 75 falló durante su actualización debido a un conflicto de actualización: cuando se emitió el comando SQL [update] en la tabla [PERSONNES], la fila que debía actualizarse estaba bloqueada por otro hilo. Este conflicto de acceso obligará al hilo n.º 75 a volver a intentar la actualización.
Para concluir con [test4], cabe destacar una diferencia notable con respecto a los resultados de la misma prueba en la versión 1, donde había fallado debido a problemas de sincronización. Dado que los métodos de la capa [dao] de la versión 1 no estaban sincronizados, surgían conflictos de acceso. En este caso, no fue necesario sincronizar la capa [dao]. Simplemente gestionamos los conflictos de acceso señalados por Firebird.
Ahora ejecutemos la prueba completa JUnit de la capa [dao]:

Por lo tanto, parece que tenemos una capa [dao] válida. Para declararla válida con alta probabilidad, tendríamos que realizar más pruebas. No obstante, la consideraremos operativa.
17.5. La capa [service]
17.5.1. Los componentes de la capa [service]
La capa [service] está compuesta por las siguientes clases e interfaces:
![]()
- [IService] es la interfaz que presenta la capa [service]
- [ServiceImpl] es una implementación de esta
La interfaz [IService] es la siguiente:
- La interfaz cuenta con los mismos cuatro métodos que en la versión 1, pero tiene dos adicionales:
- saveMany: permite guardar varias personas al mismo tiempo de manera atómica. O se guardan todas, o no se guarda ninguna.
- deleteMany: permite eliminar varias personas al mismo tiempo de manera atómica. O se eliminan todas, o ninguna.
La aplicación web no utilizará estos dos métodos. Los hemos agregado para ilustrar el concepto de transacción en una base de datos. De hecho, ambos métodos deberán ejecutarse dentro de una transacción para lograr la atomicidad deseada.
La clase [ServiceImpl] que implementa esta interfaz será la siguiente:
- Los métodos [getAll, getOne, insertOne, saveOne] invocan a los métodos de la capa [dao] del mismo nombre.
- líneas 42-47: el método [saveMany] guarda, una por una, las personas de la tabla pasada como parámetro.
- líneas 50-55: el método [deleteMany] elimina, una por una, las personas cuyo array se le ha pasado como parámetro desde id
Ya mencionamos que los métodos [saveMany] y [deleteMany] deben ejecutarse dentro de una transacción para garantizar el carácter «todo o nada» de estos métodos. Podemos observar que el código anterior ignora por completo este concepto de transacción. Este solo aparecerá en el archivo de configuración de la capa [service].
17.5.2. Configuración de la capa [service]
Arriba, en la línea 11, vemos que la implementación [ServiceImpl] tiene una referencia a la capa [dao]. Esta, al igual que en la versión 1, será inicializada por Spring al momento de la instanciación de la capa [service - ServiceImpl]. El archivo de configuración que permitirá la instanciación de la capa [service] será el siguiente:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<property name="url">
<!-- Atención: no dejes espacios entre las dos etiquetas <value> -->
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- la clase de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
<!-- administrador de transacciones -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- clase de acceso a la capa [service] -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- líneas 1-36: configuración de la capa [dao]. Esta configuración se explicó al analizar la capa [dao] en el párrafo 17.3.2.
- líneas 38-64: configuran la capa [service]
En la línea 46, se puede observar que la implementación de la capa [service] se realiza mediante el tipo [TransactionProxyFactoryBean]. Esperábamos encontrar el tipo [ServiceImpl]. [TransactionProxyFactoryBean] es un tipo predefinido de Spring. ¿Cómo es posible que un tipo predefinido pueda implementar la interfaz [IService], que a su vez es específica de nuestra aplicación?
Veamos primero la clase [TransactionProxyFactoryBean]:

Vemos que implementa la interfaz [FactoryBean]. Ya nos hemos encontrado con esta interfaz. Sabemos que cuando una aplicación le pide a Spring una instancia de un tipo que implemente [FactoryBean], Spring no devuelve una instancia [I] de ese tipo, sino el objeto devuelto por el método [I].getObject():
![]()
En nuestro caso, la capa [service] será implementada por el objeto devuelto por [TransactionProxyFactoryBean].getObject(). ¿Cuál es la naturaleza de este objeto? No vamos a entrar en detalles porque son complejos. Pertenecen a lo que se conoce como Spring AOP (programación orientada a aspectos). Intentaremos aclarar las cosas con diagramas sencillos. AOP permite lo siguiente:
- tenemos dos clases, C1 y C2, donde C1 utiliza la interfaz [I2] presentada por C2:
![]() |
- gracias a AOP, se puede colocar, de manera transparente para ambas clases, un interceptor entre las clases C1 y C2:
![]() |
La clase [C1] se compiló para funcionar con la interfaz [I2] que implementa [C2]. En el momento de la ejecución, AOP coloca la clase [intercepteur] entre [C1] y [C2]. Para que esto sea posible, es necesario, por supuesto, que la clase [intercepteur] presente a [C1] la misma interfaz [I2] que [C2].
¿Para qué puede servir esto? La documentación de Spring ofrece algunos ejemplos. Por ejemplo, es posible que se desee generar registros cada vez que se llame a un método M específico de [C2], con el fin de realizar una auditoría de dicho método. En [intercepteur], se escribirá entonces un método [M] que genere esos registros. La llamada de [C1] a [C2].M se llevará a cabo de la siguiente manera (véase el diagrama anterior):
- [C1] llama al método M de [C2]. En realidad, se llamará al método M de [intercepteur]. Esto es posible si [C1] se dirige a una interfaz [I2] en lugar de a una implementación específica de [I2]. Entonces basta con que [intercepteur] implemente a [I2].
- El método M de [intercepteur] genera los registros y llama al método M de [C2], al que inicialmente apuntaba [C1].
- El método M de [C2] se ejecuta y devuelve su resultado al método M de [intercepteur], el cual puede, si es necesario, agregar algo a lo que se hizo en el paso 2.
- El método M de [intercepteur] devuelve un resultado al método llamante de [C1]
Se observa que el método M de [intercepteur] puede realizar acciones antes y después de la llamada al método M de [C2]. En relación con [C1], por lo tanto, enriquece el método M de [C2]. Así pues, podemos considerar la tecnología AOP como una forma de enriquecer la interfaz que presenta una clase.
¿Cómo se aplica este concepto a nuestra capa [service]? Si implementamos la capa [service] directamente con una instancia de [ServiceImpl], nuestra aplicación web tendrá la siguiente arquitectura:
![]() |
Si implementamos la capa [service] con una instancia [TransactionProxyFactoryBean], tendremos la siguiente arquitectura:
![]() |
Podemos decir que la capa [service] se instancia con dos objetos:
- el objeto que llamamos anteriormente [proxy transactionnel] y que, de hecho, es el objeto devuelto por el método [getObject] de [TransactionProxyFactoryBean]. Es este objeto el que servirá de interfaz entre la capa [service] y la capa [web]. Por su diseño, implementa la interfaz [IService].
- una instancia [ServiceImpl] que también implementa la interfaz [IService]. Solo ella sabe cómo trabajar con la capa [dao], por lo que es necesaria.
Imaginemos que la capa [web] llama al método [saveMany] de la interfaz [IService]. Sabemos que, desde el punto de vista funcional, las inserciones y actualizaciones que realiza este método deben llevarse a cabo dentro de una transacción. O bien se realizan todas con éxito, o bien no se realiza ninguna. Anteriormente presentamos el método [saveMany] de la clase [ServiceImpl] y señalamos que no contaba con el concepto de transacción. El método [saveMany] de [proxy transactionnel] incorporará este concepto de transacción al método [saveMany] de la clase [ServiceImpl]. Sigamos el esquema anterior:
- la capa [web] llama al método [saveMany] de la interfaz [IService].
- Se ejecuta el método [saveMany] de [proxy transactionnel]. Este inicia una transacción. Debe contar con la información suficiente para hacerlo, en particular un objeto [DataSource] para obtener una conexión con SGBD. Luego invoca el método [saveMany] de [ServiceImpl].
- Esta se ejecuta. Llama repetidamente a la capa [dao] para ejecutar las inserciones o actualizaciones. Las órdenes SQL ejecutadas en este momento se realizan dentro de la transacción iniciada en el paso 2.
- Supongamos que una de estas operaciones falla. La capa [dao] permitirá que se propague una excepción hacia la capa [service], en este caso, el método [saveMany] de la instancia [ServiceImpl].
- Esta no hace nada y permite que la excepción se transmita hasta el método [saveMany] de [proxy transactionnel].
- Al recibir la excepción, el método [saveMany] de [proxy transactionnel], que es el propietario de la transacción, ejecuta un [rollback] de esta para anular todas las actualizaciones, y luego permite que la excepción se propague hasta la capa [web], que se encargará de gestionarla.
En el paso 4, supusimos que una de las inserciones o actualizaciones fallaba. Si no es así, en [5] no se eleva ninguna excepción. Lo mismo ocurre en [6]. En ese caso, el método [saveMany] de [proxy transactionnel] realiza un [commit] de la transacción para validar todas las actualizaciones.
Ahora tenemos una idea más precisa de la arquitectura implementada por el bean [TransactionProxyFactoryBean]. Repasemos su configuración:
<!-- administrador de transacciones -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- clase de acceso a la capa [service] -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
Analicemos esta configuración a la luz de la arquitectura que se ha configurado:
![]() |
- [proxy transactionnel] se encargará de gestionar las transacciones. Spring ofrece varias estrategias para gestionarlas. [proxy transactionnel] necesita una referencia al gestor de transacciones elegido.
- líneas 11–13: definen el atributo [transactionManager] del bean [TransactionProxyFactoryBean] con una referencia a un administrador de transacciones. Este se define en las líneas 2–7.
- líneas 2–7: el administrador de transacciones es del tipo [DataSourceTransactionManager]:

[DataSourceTransactionManager] es un administrador de transacciones adaptado a los SGBD a los que se accede a través de un objeto [DataSource]. Solo puede gestionar transacciones en un único SGBD. No puede gestionar transacciones distribuidas en varios SGBD. En este caso, solo tenemos un SGBD. Por lo tanto, este administrador de transacciones es adecuado. Cuando [proxy transactionnel] inicie una transacción, lo hará en una conexión vinculada al hilo. Es esta conexión la que se utilizará en todas las capas que conducen a la base de datos: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].
La clase [DataSourceTransactionManager] necesita conocer la fuente de datos a la que debe solicitar una conexión para vincularla al hilo. Esta se define en las líneas 4 a 6: es la misma fuente de datos que la utilizada por la capa [dao] (véase el párrafo 17.5.2).
- líneas 14-19: el atributo «target» indica la clase que debe ser interceptada, en este caso la clase [ServiceImpl]. Esta información es necesaria por dos razones:
- la clase [ServiceImpl] debe instanciarse, ya que es la que se encarga de la comunicación con la capa [dao]
- [TransactionProxyFactoryBean] debe generar un proxy que presente a la capa [web] la misma interfaz que [ServiceImpl].
- líneas 21-27: indican qué métodos de [ServiceImpl] debe interceptar el proxy. El atributo [transactionAttributes], en la línea 21, indica qué métodos de [ServiceImpl] requieren una transacción y cuáles son los atributos de esta:
- línea 23: los métodos cuyo nombre comienza por get [getOne, getAll] se ejecutan en una transacción con el atributo [PROPAGATION_REQUIRED,readOnly]:
- PROPAGATION_REQUIRED: el método se ejecuta en una transacción si ya hay una asociada al hilo; de lo contrario, se crea una nueva y el método se ejecuta en ella.
- readOnly: transacción de solo lectura
Aquí, los métodos [getOne] y [getAll] de [ServiceImpl] se ejecutarán en una transacción, cuando en realidad no es necesario. En cada caso se trata de una operación que consta de una sola orden SELECT. No vemos la utilidad de incluir este SELECT en una transacción.
- línea 24: los métodos cuyo nombre comienza con «save», como [saveOne, saveMany], se ejecutan en una transacción con el atributo [PROPAGATION_REQUIRED].
- línea 25: los métodos [deleteOne] y [deleteMany] de [ServiceImpl] están configurados de manera idéntica a los métodos [saveOne, saveMany].
En nuestra capa [service], solo los métodos [saveMany] y [deleteMany] necesitan ejecutarse en una transacción. La configuración podría haberse reducido a las siguientes líneas:
<property name="transactionAttributes">
<props>
<prop key="saveMany">PROPAGATION_REQUIRED</prop>
<prop key="deleteMany">PROPAGATION_REQUIRED</prop>
</props>
</property>
17.6. Pruebas de la capa [service]
Ahora que hemos escrito y configurado la capa [service], nos proponemos probarla con las pruebas JUnit:

El archivo de configuración [spring-config-test-service-firebird.xml] de la capa [service] es el que se describió en el párrafo 17.5.2.
La prueba JUnit [TestServiceFirebird] es la siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
- líneas 19-22: el programa prueba las capas [dao] y [service] configuradas por el archivo [spring-config-test-service-firebird.xml], el que se analizó en la sección anterior.
- Las pruebas de [test1] a [test6] son idénticas en su concepto a sus homólogas del mismo nombre en la clase de prueba [TestDaoFirebird] de la capa [dao]. La única diferencia es que, por configuración, los métodos [saveOne] y [deleteOne] ahora se ejecutan en una transacción.
- El método [test7] tiene como objetivo probar los métodos [saveMany] y [deleteMany]. Queremos verificar que se ejecuten correctamente dentro de una transacción. Comentaremos el código de este método:
- líneas 62-63: se cuenta el número de personas [nbPersonnes1] que se encuentran actualmente en la lista
- líneas 67-72: se crean tres personas
- líneas 73-83: estas tres personas se guardan mediante el método [saveMany] – línea 77. Las dos primeras personas, p1 y p2, que tienen un id igual a -1, se agregarán a la tabla [PERSONNES]. La persona p3, por su parte, tiene un id igual a -2. Por lo tanto, no se trata de una inserción, sino de una actualización. Esta fallará porque no hay ninguna persona con un id igual a -2 en la tabla [PERSONNES]. Por lo tanto, la capa [dao] lanzará una excepción que se propagará hasta la capa [service]. La existencia de esta excepción se verifica en la línea 83.
- Debido a la excepción anterior, la capa [service] debería generar un [rollback] a partir del conjunto de órdenes SQL emitidas durante la ejecución del método [saveMany], ya que este método se ejecuta dentro de una transacción. En las líneas 86 y 87, se verifica que el número de personas en la lista no haya cambiado y que, por lo tanto, no se hayan realizado las inserciones de p1 y p2.
- Líneas 88-103: se agregan únicamente las personas p1 y p2 y se verifica que, a continuación, haya dos personas más en la lista.
- Líneas 106-114: se elimina un grupo de personas formado por las personas p1 y p2 que acabamos de agregar y por una persona inexistente (id = -1). Para ello se utiliza el método [deleteMany], en la línea 108. Este método fallará porque no hay ninguna persona con un id igual a –1 en la tabla [PERSONNES]. Por lo tanto, la capa [dao] lanzará una excepción que se propagará hasta la capa [service]. La existencia de esta excepción se verifica en la línea 114.
- Debido a la excepción anterior, la capa [service] debería generar un [rollback] a partir del conjunto de órdenes SQL emitidas durante la ejecución del método [deleteMany], ya que este método se ejecuta dentro de una transacción. En las líneas 116 y 117, se verifica que el número de personas en la lista no haya cambiado y que, por lo tanto, no se hayan realizado las eliminaciones de p1 y p2.
- Línea 122: se elimina un grupo formado únicamente por las personas p1 y p2. Esto debería funcionar. El resto del método verifica que así sea.
La ejecución de las pruebas arroja los siguientes resultados:

Las siete pruebas se han realizado con éxito. Consideraremos que nuestra capa [service] está operativa.
17.7. La capa [web]
Recordemos la arquitectura general de la aplicación web que se va a desarrollar:
![]() |
Acabamos de desarrollar las capas [dao] y [service], que permiten trabajar con una base de datos Firebird. Escribimos una versión 1 de esta aplicación en la que las capas [dao] y [service] trabajaban con una lista de personas en memoria. La capa [web], escrita en esa ocasión, sigue siendo válida. De hecho, se dirigía a una capa [service] que implementaba la interfaz [IService]. Dado que la nueva capa [service] implementa esta misma interfaz, no es necesario modificar la capa [web].
En el artículo anterior, se había probado la versión 1 de la aplicación con el proyecto Eclipse [mvc-personnes-02B], en el que las capas [web, service, dao, entites] se habían guardado en archivos .jar:
![]() |
La carpeta [src] estaba vacía. Las clases de las capas se encontraban en los archivos [personnes-*.jar ]:
![]() |
Para probar la versión 2, en Eclipse duplicamos la carpeta Eclipse [mvc-personnes-02B] en [mvc-personnes-03B] (copiar/pegar):

En el proyecto [mvc-personnes-03], exportamos las capas [dao] y [service], respectivamente, a los archivos [personnes-dao.jar] y [personnes-service.jar] de la carpeta [dist] del proyecto:

Copiamos estos dos archivos y, luego, en Eclipse, los pegamos en la carpeta [WEB-INF/lib] del proyecto [mvc-personnes-03B], donde reemplazarán los archivos del mismo nombre de la versión anterior.
![]() |
También copiamos y pegamos los archivos [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] de la carpeta [lib] del proyecto [mvc-personnes-03] en la carpeta [WEB-INF/lib] del proyecto [mvc-personnes-03B]. Estos archivos son necesarios para las nuevas capas [dao] y [service].
Una vez hecho esto, incluimos los nuevos archivos en el Classpath del proyecto: [clic droit sur projet -> Properties -> Java Build Path -> Add Jars].
La carpeta [src] contiene los archivos de configuración de las capas [dao] y [service]:

El archivo [spring-config.xml] configura las capas [dao] y [service] de la aplicación web. En la nueva versión, es idéntico al archivo [spring-config-test-service-firebird.xml] que se utilizó para configurar la prueba de la capa de servicio en el proyecto [mvc-personnes-03]. Por lo tanto, se realiza un copiar y pegar de uno a otro:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<property name="url">
<!-- Atención: no dejes espacios entre las dos etiquetas <value> -->
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- la clase de acceso a la capa [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
<!-- administrador de transacciones -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- clase de acceso a la capa [service] -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- línea 12: la URL de la base de datos Firebird. Seguimos utilizando la base de datos que se utilizó para las pruebas de las capas [dao] y [service]
Implementamos el proyecto web [mvc-personnes-03B] en Tomcat:
![]() | ![]() |
Estamos listos para las pruebas de . Se inicia el Firebird SGBD. El contenido de la tabla [PERSONNES] es entonces el siguiente:

A su vez, se inicia Tomcat. Con un navegador, solicitamos la URL [http://localhost:8080/mvc-personnes-03B]:

Agregamos una nueva persona mediante el enlace [Ajout]:
![]() | ![]() |
Verificamos la adición en la base de datos:

Se invita al lector a realizar otras pruebas con [modification, suppression].
Ahora realicemos la prueba de conflictos de versión que se había hecho en la versión 1. [Firefox] será el navegador del usuario U1. Este solicita la URL [http://localhost:8080/mvc-personnes-03B]:

[IE] será el navegador del usuario U2. Este solicita la misma URL:

El usuario U1 ingresa para modificar el perfil de la persona [Perrichon]:

El usuario U2 hace lo mismo:

El usuario U1 realiza modificaciones y las valida:
![]() |
El usuario U2 hace lo mismo:
![]() |
El usuario U2 regresa a la lista de personas mediante el enlace [Annuler] del formulario:

Encuentra a la persona [Perrichon] tal como la modificó U1 (el nombre ahora está en mayúsculas).
¿Y qué pasa con la base de datos en todo esto? Veamos:

La persona n.º 899 tiene efectivamente su nombre en mayúsculas tras la modificación realizada por U1.
17.8. Conclusion
Recordemos lo que queríamos hacer. Teníamos una aplicación web con la siguiente arquitectura de tres capas:
donde las capas [dao] y [service] trabajaban con una lista de datos en memoria que, por lo tanto, se perdía al apagar el servidor web. Esa era la versión 1. En la versión 2, las capas [service] y [dao] se reescribieron para que la lista de personas se almacenara en una tabla de base de datos. Por lo tanto, ahora es persistente. Ahora nos proponemos analizar el impacto que tiene en nuestra aplicación el cambio en SGBD. Para ello, construiremos tres nuevas versiones de nuestra aplicación web:
![]() |
- versión 3: el SGBD utiliza Postgres
- versión 4: SGBD es MySQL
- versión 5: el SGBD es SQL Server Express 2005
Los cambios se realizan en los siguientes lugares:
- la clase [DaoImplFirebird] implementa funcionalidades de la capa [dao] relacionadas con SGBD Firebird. Si esta necesidad persiste, será reemplazada, respectivamente, por las clases [DaoImplPostgres], [DaoImplMySQL] y [DaoImplSqlExpress].
- El archivo de mapeo [personnes-firebird.xml] de iBATIS para el SGBD de Firebird será reemplazado, respectivamente, por los archivos de mapeo [personnes-postgres.xml], [personnes-mysql.xml] y [personnes-sqlexpress.xml], respectivamente.
- La configuración del objeto [DataSource] de la capa [dao] es específica de un SGBD. Por lo tanto, cambiará con cada versión.
- El controlador JDBC del SGBD también cambia en cada versión
Aparte de estos puntos, todo permanece igual. A continuación, describimos estas nuevas versiones centrándonos únicamente en las novedades que aporta cada una de ellas.

























