17. Aplicación web MVC en una arquitectura de tres capas – Ejemplo 3 – SGBD Firebird
17.1. La base de datos Firebird
En esta nueva versión, vamos a almacenar 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 la información necesaria para instalar y gestionar este SGBD. A continuación, las capturas de pantalla proceden 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 gestionadas por la aplicación web. Se ha creado con los siguientes comandos 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 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] tiene, además de la tabla [PERSONNES], un objeto denominado generador y denominado [GEN_PERSONNES_ID]. Este generador proporciona 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 (haz doble clic sobre él + F5 para actualizar):
El orden SQL
lo que 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 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 simple proyecto Java, 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 encontrarse en el directorio 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 la generación de 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] de SGBD Firebird, 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 hemos hecho aquí, guiándonos por los errores de clases ausentes señalados por Eclipse y por los nombres de los archivos parciales de Spring. Todos estos archivos de la carpeta [lib] se han colocado en la carpeta 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á formada 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 gestionar 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 tiene 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 trata de añadir o modificar una persona.
- línea 50: el método privado [check] es el de la versión anterior. No volveremos sobre é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 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 creació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 intercala 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], ambos ubicados en la carpeta [lib] del proyecto:
![]() |
La clase [SqlMapClientDaoSupport] encapsula la parte genérica del uso del marco [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 vamos a utilizar la base de datos:
![]()
El objeto [SqlMapClient sqlMapClient] es el objeto [IBATIS] utilizado 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], mientras que 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
- uno o varios archivos de configuración en los que 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 concreta.
Un esbozo de la configuración de nuestra capa [dao] que reflejaría la arquitectura anterior sería el siguiente:
<!-- 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). La inicialización se realiza mediante el método [setSqlMapClient] de la clase [DaoImpl]. Esta clase no tiene 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 denominado «sqlMapClient» que aún está por crear. Este, como ya se ha dicho, 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 lo que queremos es instanciar un objeto que implemente la interfaz [SqlMapClient]. Al parecer, este no es el caso de la clase [SqlMapClientFactoryBean]. Esta clase implementa la interfaz [FactoryBean] (véase más arriba). Dicha interfaz cuenta con el siguiente método [getObject()]:
![]()
Cuando se 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 invoca 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 solicitará conexiones
- uno (o varios) archivos de configuración en los que se externalizan las órdenes SQL que se van a ejecutar
La clase [SqlMapClientFactoryBean] dispone de los métodos «set» para inicializar estas dos propiedades:

Vamos avanzando... Nuestro archivo de configuración se va concretando 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 solicitamos 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 hay que crear. Si observamos el parámetro que espera el método [setDataSource] de [SqlMapClientFactoryBean], vemos que es de tipo [DataSource]:

De nuevo nos encontramos ante 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 forma eficaz, conexiones a una base de datos concreta. 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, nos vemos obligados, en cada intercambio con la base de datos, a:
- abrir una conexión
- iniciar una transacción
- enviar órdenes SQL
- cerrar la transacción
- cerrar la conexión
Abrir y cerrar conexiones de forma repetida requiere mucho tiempo. Para resolver estos dos problemas (limitar tanto el número de conexiones abiertas en un momento dado como el coste de abrirlas y cerrarlas), las clases que implementan la interfaz [DataSource] suelen proceder de la siguiente manera:
- al instanciarse, abren N conexiones con la base de datos en cuestión. 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 en todo momento 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 quedan disponibles. Cuando la aplicación cierra la conexión, esta no se cierra realmente, sino que simplemente se devuelve al grupo de conexiones disponibles.
Existen diversas implementaciones de la interfaz [DataSource] disponibles de forma gratuita. En este caso, utilizaremos la implementación [commons DBCP], disponible en la URL [http://jakarta.apache.org/commons/dbcp/]:

Para utilizar la herramienta [commons DBCP] se necesitan dos archivos [commons-dbcp, commons-pool], ambos ubicados 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, hay que proporcionarle la información que necesita para crear las conexiones del grupo:
- el nombre del controlador JDBC que se va a utilizar —inicializado con [setDriverClassName]—
- el nombre de la URL de la base de datos que se va a utilizar —inicializado con [setUrl]—
- el identificador del usuario 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]. Hay que 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 por defecto 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 aspectos de la 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]. Vamos a analizarlo.
- [personnes-firebird.xml] está referenciado por [sql-map-config-firebird.xml]. Vamos a analizarlo.
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 (que no aparece en la imagen anterior). Esta carpeta forma parte del ClassPath de la aplicación. En definitiva, 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 designar los archivos que contienen las órdenes SQL que se deben ejecutar. A menudo, aunque no es obligatorio, hay un archivo por tabla. Esto permite agrupar en un mismo archivo las órdenes SQL relativas a una tabla concreta. Sin embargo, es frecuente encontrar órdenes SQL que afectan a varias tablas. En ese caso, la descomposición anterior no es válida. Basta con recordar que todos los archivos designados por las etiquetas <sqlMap> se fusionarán. Estos archivos se buscan en el ClassPath de la aplicación.
El archivo [personnes-firebird.xml] describe las órdenes SQL que se van a emitir 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 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>
<!-- obtener una persona en concreto -->
<select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#valor#</select>
<!-- añadir 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#, #apellidos#, #nombre#, #dateNaissance#, #casada#,
#nbEnfants#) </insert>
<!-- actualizar una persona -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#versión#+1, NOM=#apellidos#, 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# </delete>
</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 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 concreta 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 un registro 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 estudiar 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 como se accederá a la base de datos. El tipo [iBATIS] SqlMapClient podría utilizarse directamente, ya que la clase [SqlMapClientDaoSupport] tiene acceso a él:
![]()
El inconveniente de la clase [iBATIS] SqlMapClient es que lanza excepciones de tipo [SQLException], un tipo de excepción controlada, c.a.d. que debe gestionarse mediante un try/catch o declararse en la firma de los métodos que la lanzan. Ahora bien, 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 gestionar nosotros mismos estas excepciones, las confiaremos al tipo Spring [SqlMapClientTemplate], que encapsula el objeto [SqlMapClient] de la capa [iBATIS]. De hecho, [SqlMapClientTemplate] se ha diseñado 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 tener en cuenta 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 enviar una orden [SELECT] y recuperar el resultado en forma de 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 ejecutarse la orden [select], se sustituyen por los valores de dichos campos.
- un diccionario: en este caso, los parámetros de la orden [select] son las claves del diccionario. Al ejecutar la orden [select], estas se sustituyen por sus valores asociados en el diccionario.
- Si [SELECT] no devuelve ninguna línea, el resultado [List] es un objeto vacío de elementos, pero no null (por verificar).
[queryForObject]
![]()
Este método es idéntico en su principio al anterior, pero solo devuelve un único objeto. Si [SELECT] no devuelve ninguna línea, 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 línea que se ha insertado. No es obligatorio utilizar este resultado.
[update]
![]()
Este método permite ejecutar una orden SQL [update] configurada mediante 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: la orden SQL «Personne.getAll» no tiene parámetros (ausencia de 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 va a 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> 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-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 líneas 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 comentado en el análisis del método [getAll].
getOne
Este método permite obtener una persona identificada por 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 concreto -->
<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# designa el valor del parámetro pasado a la orden SQL, cuando dicho parámetro es de tipo simple: Integer, Double, String, etc. 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 ha devuelto ninguna línea, se recupera el puntero null de la línea 4. Esto significa que no se ha encontrado 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 líneas eliminadas.
- líneas 7-8: si la consulta [delete] no ha eliminado ninguna línea, significa que la persona no existe. Se ejecuta una consulta [DaoException] con código 2 (línea 8).
saveOne
Este método permite añadir una nueva persona o modificar una ya existente. Su código es el siguiente:
- línea 4: se comprueba la validez de la persona con el método [check]. Este método ya existía en la versión anterior y, en aquel momento, se había comentado. Lanza un [DaoException] si la persona no es válida. Se deja que este se remita.
- línea 6: si se llega hasta aquí, es que 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 alta (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 añadir 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#, #apellidos#, #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 rellenar las columnas de la fila que se va a insertar en la tabla [PERSONNES] (líneas 5-8). Hay un problema que resolver. Al realizar una inserción, el objeto [Personne] que se va a insertar tiene un ID igual a -1. Es necesario sustituir este valor por una clave primaria válida. Para ello, se utilizan las líneas 2-4 de la etiqueta <selectKey> anterior. En ellas se indica:
- (continuación)
- La consulta SQL que hay que ejecutar para obtener un valor de clave primaria. La que se indica aquí es la que hemos presentado en el apartado 17.1. Hay que tener en cuenta dos aspectos:
- «as "value"» es obligatorio. También se puede escribir «as value», pero «value» es una palabra clave de Firebird que ha tenido que 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 repitiéndolo dos veces.
- El campo del objeto [Personne] que hay que inicializar con el valor recuperado 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 hay que ejecutar para obtener un valor de clave primaria. La que se indica aquí es la que hemos presentado en el apartado 17.1. Hay que tener en cuenta dos aspectos:
- líneas 6-7: a efectos de las pruebas, tendremos que esperar 10 ms antes de realizar la inserción, con el fin de comprobar si hay conflictos entre subprocesos que intenten realizar adiciones al mismo tiempo.
updatePersonne
Este método permite modificar un registro que ya existe en la tabla [PERSONNES]. Su código es el siguiente:
- Una actualización puede fallar por al menos dos motivos:
- 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 usuario -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#versión#+1, NOM=#apellidos#, PRENOM=#nombre#, DATENAISSANCE=#dateNaissance#,
MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# y
VERSION=#versión#</actualización>
- (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.º [id] y la misma versión [version] que el parámetro. Por eso, se aplica 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 anterior).
- línea 9: se recupera el número de líneas actualizadas.
- Líneas 10-11: si este número es cero, se lanza 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 exhaustivas, podemos empezar 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>
<!-- 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 apartado 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] ofrece 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, salvo la [test4], que ha sufrido ligeras modificaciones. La prueba [test6] es nueva. Solo comentaremos estas dos pruebas.
[test4]
[test4] tiene como objetivo probar el método [updatePersonne - DaoImplCommon]. Recordemos su código:
- líneas 4-5: se espera 10 ms. De este modo, 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 una misma persona. Queremos ver cómo se gestionan los conflictos de versión y los conflictos de acceso.
Los hilos se crean en las líneas 8-13. Cada uno incrementará en 1 el número de hijos de la persona creada en las líneas 3-5. Los hilos de actualización [ThreadDaoMajEnfants ] son los siguientes:
Una actualización de una persona puede fallar porque la persona que se quiere modificar no existe o porque ya ha sido actualizada previamente por otro hilo. Estos dos casos se gestionan aquí en las líneas 67-69. De hecho, en ambos casos, el método [updatePersonne] lanza un [DaoException] con código 2. A continuación, el hilo volverá a iniciar 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 su código:
- líneas 6-7: se espera 10 ms para forzar al hilo que ejecuta [insertPersonne] a perder el control del procesador y aumentar así nuestras posibilidades de que aparezcan conflictos debidos a hilos que realizan inserciones al mismo tiempo.
El código de [test6] es el siguiente:
Creamos 100 subprocesos que insertarán al mismo tiempo a 100 personas diferentes. Estos 100 subprocesos obtendrán todos una clave primaria para la persona que deben insertar y, a continuación, serán interrumpidos durante 10 ms (línea 10 – insertPersonne) antes de poder realizar su inserción. Queremos comprobar que todo va bien y, en particular, que obtienen 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 finalicen cada uno de los 100 subprocesos que ha iniciado. Cuando detecta que ha finalizado el subproceso n.º i, elimina a la persona que dicho subproceso 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];
--- Se ha producido un error en personnes-firebird.xml.
--- Se ha producido un error al aplicar una asignación de parámetros.
--- Comprueba el Personne.updateOne-InlineParameterMap.
--- Comprueba la instrucción (la actualización ha fallado).
--- Causa: org.firebirdsql.jdbc.FBSQLException: Excepción GDS. 335544336. Interbloqueo
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:
--- Se ha producido un error en personnes-firebird.xml.
--- El error se produjo al aplicar un mapa de parámetros.
- línea 1: se ha producido una excepción de Spring [org.springframework.jdbc.UncategorizedSQLException]. Se trata de una excepción no controlada que se ha utilizado 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 el código de error 335544336.
- línea 7: indica que se ha producido 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, hay que modificar el código de [ThreadDaoMajEnfants]:
- línea 8: se gestiona una excepción de tipo [DaoException]. Según lo dicho, tendríamos que gestionar la excepción que ha aparecido 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 una serie de SGBD, como Oracle, MySQL, Postgres, DB2, SQL Server, etc., pero no las de Firebird. Asimismo, cualquier excepción lanzada por el controlador JDBC de Firebird queda encapsulada en el tipo Spring [org.springframework.jdbc.UncategorizedSQLException]:

Como se puede ver más arriba, la clase [UncategorizedSQLException] deriva de la clase [DataAccessException] que hemos mencionado en el apartado 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], que 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].
En este caso, tendremos que comprobar 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 recuperar 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 evitarlo. De hecho, deseamos que nuestras pruebas JUnit sigan siendo válidas independientemente del SGBD que se utilice. Para conseguirlo, decidimos que la capa [dao] iniciará un [DaoException] de código 4 cuando se detecte una excepción de tipo «conflicto de actualización», 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 excepción la genera 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-11 deben estar rodeadas por un try / catch. Para el SGBD de Firebird, debemos comprobar que la excepción que ha provocado el fallo de la actualización es de tipo [org.firebirdsql.gds.GDSException] y tiene como código de error 335544336. Si incluimos este tipo de comprobación en [DaoImplCommon], vincularemos esta clase al Firebird SGBD, lo cual, evidentemente, no es deseable. Si queremos que la clase [DaoImplCommon] siga siendo de carácter general, debemos derivarla y gestionar la excepción en una clase específica para Firebird. Eso es lo que vamos a hacer 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 estudiar. Redefine, en las líneas 8-33, el método [updatePersonne] que nos plantea el problema.
- línea 20: interceptamos la excepción de Spring de tipo [UncategorizedSQLException]
- líneas 21-22: comprobamos 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 comprueba que el código de error de esta excepción de Firebird es 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 resulta útil para las pruebas.
Ya estamos listos para las pruebas de la nueva capa [dao].
17.4.3. Pruebas de la implementación [DaoImplFirebird]
Se modifica el archivo de configuración de pruebas [spring-config-test-dao-firebird.xml] 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 dejar 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>
<!-- 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 ha superado 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 finalizar. La línea 3 muestra un conflicto de versiones 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 línea 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.
Por último, en el caso de [test4], se observa 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, se producían conflictos de acceso. En este caso, no hemos tenido que sincronizar la capa [dao]. Simplemente hemos gestionado 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 una 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á formada por las siguientes clases e interfaces:
![]()
- [IService] es la interfaz que presenta la capa [service]
- [ServiceImpl] es una implementación de la misma
La interfaz [IService] es la siguiente:
- La interfaz tiene los mismos cuatro métodos que en la versión 1, pero cuenta con dos más:
- saveMany: permite guardar varias personas a la vez de forma atómica. O se guardan todas, o no se guarda ninguna.
- deleteMany: permite eliminar varias personas a la vez de forma atómica. O se eliminan todas, o no se elimina ninguna.
La aplicación web no utilizará estos dos métodos. Los hemos añadido 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 conseguir la atomicidad deseada.
La clase [ServiceImpl] que implementa esta interfaz será la siguiente:
- Los métodos [getAll, getOne, insertOne, saveOne] recurren 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 matriz 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 hemos dicho 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]
En la línea 11 anterior, vemos que la implementación [ServiceImpl] contiene una referencia a la capa [dao]. Esta, al igual que en la versión 1, será inicializada por Spring en el 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 dejar 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>
<!-- 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>
<!-- gestor 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 apartado 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 solicita 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 unos esquemas sencillos. AOP permite lo siguiente:
- tenemos dos clases, C1 y C2, y C1 utiliza la interfaz [I2] proporcionada por C2:
![]() |
- gracias a AOP, se puede colocar, de forma transparente para ambas clases, un interceptor entre las clases C1 y C2:
![]() |
La clase [C1] se ha compilado 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, puede que queramos generar registros cada vez que se llame a un método M concreto de [C2], con el fin de realizar una auditoría de dicho método. En [intercepteur], se escribirá entonces un método [M] que realice estos registros. La llamada de [C1] a [C2].M se producirá de la siguiente manera (véase el esquema anterior):
- [C1] llama al método M de [C2]. En realidad, lo que se invocará será el método M de [intercepteur]. Esto es posible si [C1] se dirige a una interfaz [I2] en lugar de a una implementación concreta de [I2]. Basta entonces con que [intercepteur] implemente [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], que, si procede, puede añadir algo a lo realizado 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 alguna acción antes y después de la llamada al método M de [C2]. En relación con [C1], por lo tanto, amplía el método M de [C2]. Así pues, podemos considerar la tecnología AOP como una forma de ampliar 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 [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 denominamos 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 propia naturaleza, implementa la interfaz [IService].
- una instancia [ServiceImpl] que, a su vez, 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 adiciones o actualizaciones realizadas por 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. Hemos presentado el método [saveMany] de la clase [ServiceImpl] y hemos señalado que carecía del concepto de transacción. El método [saveMany] de [proxy transactionnel] va a 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. Para ello, debe disponer de la información necesaria, en particular un objeto [DataSource] que le permita establecer una conexión con SGBD. A continuación, 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 llevan a cabo en 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 realiza ninguna acción y deja 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] sobre ella para anular todas las actualizaciones, y, a continuación, deja que la excepción se propague hasta la capa [web], que se encargará de gestionarla.
En el paso 4, hemos supuesto 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]. Volvamos a su configuración:
<!-- gestor 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 gestor de transacciones. Este se define en las líneas 2-7.
- Líneas 2-7: el gestor de transacciones es de tipo [DataSourceTransactionManager]:

[DataSourceTransactionManager] es un gestor 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 único SGBD. Por lo tanto, este gestor 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-6: es la misma fuente de datos que la utilizada por la capa [dao] (véase el apartado 17.5.2).
- líneas 14-19: el atributo «target» indica la clase que debe interceptarse, en este caso la clase [ServiceImpl]. Esta información es necesaria por dos razones:
- la clase [ServiceImpl] debe instanciarse, ya que es la encargada 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
En este caso, los métodos [getOne] y [getAll] de [ServiceImpl] se ejecutarán en una transacción, cuando en realidad no es necesario. En todos los casos se trata de una operación compuesta por una única orden SELECT. No vemos la utilidad de incluir este SELECT en una transacción.
- Línea 24: los métodos cuyo nombre comienza por «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 forma 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 ha descrito en el apartado 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 comprueba las capas [dao] y [service] configuradas por el archivo [spring-config-test-service-firebird.xml], el que se ha analizado en la sección anterior.
- Las pruebas [test1] a [test6] son idénticas en su esencia 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] se ejecutan ahora en una transacción.
- El método [test7] tiene como objetivo probar los métodos [saveMany] y [deleteMany]. Queremos comprobar que se ejecutan 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 hay 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 añadirán a la tabla [PERSONNES]. La persona p3 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 comprueba 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 en una transacción. En las líneas 86-87, se comprueba que el número de personas de la lista no ha variado y que, por lo tanto, no se han producido las inserciones de p1 y p2.
- Líneas 88-103: se añaden únicamente las personas p1 y p2 y se comprueba que, a continuación, hay 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 añadir y por una persona inexistente (id = -1). Para ello se utiliza el método [deleteMany], 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 comprueba 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 en una transacción. En las líneas 116-117, se comprueba que el número de personas de la lista no ha variado y que, por lo tanto, no se han producido 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 correctamente. El resto del método comprueba que así sea.
La ejecución de las pruebas arroja los siguientes resultados:

Las siete pruebas se han superado. Consideraremos que nuestra capa [service] está operativa.
17.7. La capa [web]
Recordemos la arquitectura general de la aplicación web que vamos a desarrollar:
![]() |
Acabamos de desarrollar las capas [dao] y [service], que permiten trabajar con una base de datos Firebird. Hemos escrito 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], creada en aquella 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 probó 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, a continuación, en Eclipse los pegamos en la carpeta [WEB-INF/lib] del proyecto [mvc-personnes-03B], donde sustituirán a 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 la ruta de clases 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 dejar 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>
<!-- gestor 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]
Desplegamos 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 continuación, se inicia Tomcat. Con un navegador, accedemos a la URL [http://localhost:8080/mvc-personnes-03B]:

Añadimos una nueva persona mediante el enlace [Ajout]:
![]() | ![]() |
Comprobamos la incorporación en la base de datos:

Se invita al lector a realizar otras pruebas: [modification, suppression].
Ahora realicemos la prueba de conflictos de versión que se llevó a cabo 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 accede a la edición del 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 vuelve a la lista de personas mediante el enlace [Annuler] del formulario:

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

El nombre de la persona n.º 899 aparece en mayúsculas tras la modificación realizada por U1.
17.8. Conclusión
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 la base de datos. Por lo tanto, ahora es persistente. Ahora nos proponemos analizar el impacto que tiene en nuestra aplicación el cambio de SGBD. Para ello, vamos a crear tres nuevas versiones de nuestra aplicación web:
![]() |
- versión 3: el SGBD utiliza Postgres
- versión 4: SGBD es MySQL
- versión 5: 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, se sustituirá, respectivamente, por las clases [DaoImplPostgres], [DaoImplMySQL] y [DaoImplSqlExpress].
- El archivo de mapeo [personnes-firebird.xml] de iBATIS para el Firebird SGBD va a ser sustituido, 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 lo demás permanece igual. A continuación, describimos estas nuevas versiones centrándonos únicamente en las novedades que aporta cada una de ellas.

























