Skip to content

19. Aplicación web MVC en una arquitectura de tres capas – Ejemplo 5, MySQL

19.1. La base de datos MySQL

En esta versión, vamos a instalar la lista de personas en una tabla de la base de datos MySQL 4.x. Hemos utilizado el paquete [Apache – MySQL – PHP] disponible en la URL [http://www.easyphp.org]. A continuación, las capturas de pantalla proceden del cliente EMS, MySQL Manager Lite y [http://www.sqlmanager.net/fr/products/mysql/manager], un cliente de administración gratuito de SGBD y MySQL.

La base de datos se llama [dbpersonnes]. Contiene una tabla [PERSONNES]:

Image

La tabla [PERSONNES] contendrá la lista de personas gestionadas por la aplicación web. Se ha creado con las siguientes órdenes SQL:

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

MySQL 4.x parece más sencilla que las dos SGBD anteriores. No he podido aplicar restricciones (checks) a la tabla.

  • Línea 10: la tabla debe tener el tipo [InnoDB] y no el tipo [MyISAM], que no admite transacciones.
  • Línea 2: la clave primaria es de tipo auto_increment. Si se inserta una fila sin valor para la columna ID de la tabla, MySQL generará automáticamente un número entero para dicha columna. Esto nos evitará tener que generar las claves primarias nosotros mismos.

La tabla [PERSONNES] podría tener el siguiente contenido:

Image

Sabemos que, al insertar un objeto [Personne] mediante nuestra capa [dao], el campo [id] de dicho objeto es igual a -1 antes de la inserción y tiene un valor distinto de -1 después, siendo este valor la clave primaria asignada a la nueva fila insertada en la tabla [PERSONNES]. Veamos con un ejemplo cómo podemos conocer este valor.

El pedido SQL

SELECT LAST_INSERT_ID()

permite conocer el último valor introducido en el campo ID de la tabla. Debe ejecutarse tras la inserción. Esto difiere de las órdenes SGBD, [Firebird] y [Postgres], en las que se solicitaba el valor de la clave primaria de la persona añadida antes de la inserción. Lo utilizaremos en el archivo [personnes-mysql.xml], que recopila las órdenes SQL emitidas en la base de datos.

19.2. El proyecto Eclipse de las capas [dao] y [service]

Para desarrollar las capas [dao] y [service] de nuestra aplicación con la base de datos MySQL, utilizaremos el siguiente proyecto de Eclipse [mvc-personnes-05]:

Image

El proyecto es un simple proyecto Java, no un proyecto web de Tomcat.


Carpeta [src]


Esta carpeta contiene el código fuente de las capas [dao] y [service], así como los archivos de configuración de estas dos capas:

Image

Todos los archivos que incluyen [mysql] en su nombre pueden haber sufrido o no modificaciones con respecto a las versiones de Firebird y Postgres. A continuación, describimos aquellos que sí han sido modificados.


Carpeta [database]


Esta carpeta contiene el script de creación de la base de datos MySQL de personas:

Image

# EMS MySQL Manager Lite 3.2.0.1
# ---------------------------------------
# Host     : localhost
# Puerto: 3306
# Base de datos: dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

CREATE DATABASE `dbpersonnes`
    CHARACTER SET 'latin1'
    COLLATE 'latin1_swedish_ci';

USE `dbpersonnes`;

#
# Estructura de la tabla «personnes»: 
#

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

#
# Datos de la tabla «personnes»  (LIMIT 0,500)
#

INSERT INTO `personnes` (`ID`, `VERSION`, `NOM`, `PRENOM`, `DATENAISSANCE`, `MARIE`, `NBENFANTS`) VALUES 
  (1,1,'Major','Joachim','1984-01-13',1,2),
  (2,1,'Humbort','Mélanie','1985-01-12',0,1),
  (3,1,'Lemarchand','Charles','1986-01-01',0,0);

COMMIT;

Carpeta [lib]


Esta carpeta contiene los archivos necesarios para la aplicación:

Cabe destacar la presencia del controlador JDBC del SGBD MySQL. Todos estos archivos forman parte del Classpath del proyecto Eclipse.

19.3. La capa [dao]

La capa [dao] es la siguiente:

Image

Solo presentamos los cambios respecto a la versión [Firebird].

El archivo de mapeo [personne-mysql.xml] 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 clase [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabla de correspondencias [PERSONNES] - objeto [Personne] -->
    <resultMap id="Personne.map" 
        class="istia.st.mvc.personnes.entites.Personne">
        <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">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#versión#, #apellidos#, #nombre#, #dateNaissance#, #marie#, 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- 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#</update>
    <!-- eliminar a una persona -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#valor# </delete>
    <!-- obtener el valor de la clave primaria [id] de la última persona insertada -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

El contenido es el mismo que el de [personnes-firebird.xml], salvo por los siguientes detalles:

  • la orden SQL «Personne.insertOne» ha cambiado en las líneas 29-37:
  • la orden de inserción SQL se ejecuta antes que la orden SELECT, lo que permitirá recuperar el valor de la clave primaria de la línea insertada
  • la orden de inserción SQL no tiene ningún valor para la columna ID de la tabla [PERSONNES]

Esto refleja el ejemplo de inserción que hemos comentado en el apartado 19.1.

Cabe señalar que aquí puede haber una posible fuente de problemas entre subprocesos concurrentes. Imaginemos dos subprocesos, Th1 y Th2, que realizan una inserción al mismo tiempo. Hay un total de cuatro órdenes SQL que emitir. Supongamos que se realizan en el siguiente orden:

  1. inserción I1 de Th1
  2. inserción I2 de Th2
  3. selección S1 de Th1
  4. selección de S2 de Th2

En el paso 3, Th1 recupera la clave primaria generada durante la última inserción, es decir, la de Th2 y no la suya propia. No sé si el método [insert] de iBATIS está protegido para este caso concreto. Vamos a suponer que lo gestiona correctamente. Si no fuera así, tendríamos que derivar la clase de implementación [DaoImplCommon] de la capa [dao] en una clase [DaoImplMySQL] en la que el método [insertPersonne] estaría sincronizado. Esto solo resolvería el problema para los hilos de nuestra aplicación. Si, en el ejemplo anterior, Th1 y Th2 son hilos de dos aplicaciones diferentes, habría que resolver el problema tanto con transacciones como con un nivel de aislamiento adecuado entre transacciones. El nivel [serializable], en el que las transacciones se ejecutan como si lo hicieran de forma secuencial, sería el adecuado.

Cabe señalar que este problema no existe con Firebird y Postgres, que ejecutan el SELECT antes que el INSERT. Si, por ejemplo, tenemos la secuencia:

  1. select S1 de Th1
  2. select S2 de Th2
  3. inserción I1 de Th1
  4. inserción de I2 desde Th2

En los pasos 1 y 2, Th1 y Th2 obtienen valores de clave primaria del mismo generador. Esta operación suele ser atómica, y Th1 y Th2 obtendrán dos valores diferentes. Si la operación no fuera atómica y Th1 y Th2 obtuvieran dos valores idénticos, la inserción realizada en el paso 4 por Th2 fallaría debido a una duplicación de la clave primaria. Se trata de un error totalmente recuperable y Th2 puede volver a intentar la inserción.

Dejaremos la operación «Personne.insertOne» tal y como está actualmente en el archivo [personnes-mysql.xml], pero el lector debe ser consciente de que ahí existe potencialmente un problema.

La clase de implementación [DaoImplCommon] de la capa [dao] es la misma que la de las dos versiones anteriores.

La configuración de la capa [dao] se ha adaptado a SGBD y [MySQL]. Así pues, el archivo de configuración [spring-config-test-dao-mysql.xml] 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>com.mysql.jdbc.Driver</value>
        </property>
        <property name="url">
            <value>jdbc:mysql://localhost/dbpersonnes</valor>
        </property>
        <property name="username">
            <value>root</value>
        </property>
        <property name="password">
            <value></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-mysql.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 5-19: el bean [dataSource] ahora hace referencia a la base de datos [MySQL] [dbpersonnes], cuyo administrador es [root] sin contraseña. El lector deberá modificar esta configuración según su propio entorno.
  • Línea 31: la clase [DaoImplCommon] es la clase de implementación de la capa [dao]

Una vez realizadas estas modificaciones, se puede pasar a las pruebas.

19.4. Pruebas de las capas [dao] y [service]

Las pruebas de las capas [dao] y [service] son las mismas que para la versión [Firebird]. Los resultados obtenidos son los siguientes:

Se observa que las pruebas se han superado con éxito con la implementación [DaoImplCommon]. No tendremos que derivar esta clase, como había sido necesario hacer con las versiones SGBD y [Firebird].

19.5. Pruebas de la aplicación [web]

Para probar la aplicación web con SGBD y [MySQL], creamos un proyecto Eclipse [mvc-personnes-05B] de forma similar a la utilizada para crear el proyecto [mvc-personnes-03B] con la base de datos Firebird (véase el apartado 17.7). Sin embargo, al igual que con Postgres, no es necesario volver a crear los archivos [personnes-dao.jar] y [personnes-service.jar], ya que no hemos modificado ninguna clase.

Implementamos el proyecto web [mvc-personnes-05B] en Tomcat:

Se ejecuta SGBD MySQL. El contenido de la tabla [PERSONNES] es entonces el siguiente:

Image

A continuación, se inicia Tomcat. Con un navegador, solicitamos la URL [http://localhost:8080/mvc-personnes-05B]:

Image

Añadimos una nueva persona mediante el enlace [Ajout]:

Comprobamos la incorporación en la base de datos:

Image

Se invita al lector a realizar otras pruebas: [modification, suppression].