Skip to content

19. Application web MVC dans une architecture 3tier – Exemple 5, MySQL

19.1. La base de données MySQL

Dans cette version, nous allons installer la liste des personnes dans une table de base de données MySQL 4.x. Nous avons utilisé le paquetage [Apache – MySQL – PHP] disponible à l'url [http://www.easyphp.org]. Dans ce qui suit, les copies d’écran proviennent du client EMS MySQL Manager Lite [http://www.sqlmanager.net/fr/products/mysql/manager], un client d’administration gratuit du SGBD MySQL.

La base de données s’appelle [dbpersonnes]. Elle contient une table [PERSONNES] :

Image

La table [PERSONNES] contiendra la liste des personnes gérée par l’application web. Elle a été construite avec les ordres SQL suivants :

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 semble plus pauvre que les deux SGBD précédents. Je n'ai pas pu mettre de contraintes (checks) à la table.

  • ligne 10 : la table doit avoir le type [InnoDB] et non le type [MyISAM] qui ne supporte pas les transactions.
  • ligne 2 : la clé primaire est de type auto_increment. Si on insère une ligne sans valeur pour la colonne ID de la table, MySQL générera automatiquement un nombre entier pour cette colonne. Cela va nous éviter de générer les clés primaires nous-mêmes.

La table [PERSONNES] pourrait avoir le contenu suivant :

Image

Nous savons que lors de l'insertion d'un objet [Personne] par notre couche [dao], le champ [id] de cet objet est égal à -1 avant l'insertion et a une valeur différente de -1 ensuite, cette valeur étant la clé primaire affectée à la nouvelle ligne insérée dans la table [PERSONNES]. Voyons sur un exemple comment nous allons pouvoir connaître cette valeur.

L’ordre SQL

SELECT LAST_INSERT_ID()

permet de connaître la dernière valeur insérée dans le champ ID de la table. Elle est à émettre après l'insertion. C'est une différence avec les SGBD [Firebird] et [Postgres] où on demandait la valeur de la clé primaire de la personne ajoutée avant l'insertion. Nous l’utiliserons dans le fichier [personnes-mysql.xml] qui rassemble les ordres SQL émis sur la base de données.

19.2. Le projet Eclipse des couches [dao] et [service]

Pour développer les couches [dao] et [service] de notre application avec la base de données MySQL, nous utiliserons le projet Eclipse [mvc-personnes-05] suivant :

Image

Le projet est un simple projet Java, pas un projet web Tomcat.


Dossier [src]


Ce dossier contient les codes source des couches [dao] et [service] ainsi que les fichiers de configuration de ces deux couches :

Image

Tous les fichiers ayant [mysql] dans leur nom ont pu subir ou non une modification vis à vis des versions Firebird et Postgres. Dans ce qui suit, nous décrivons ceux qui ont été modifiés.


Dossier [database]


Ce dossier contient le script de création de la base de données MySQL des personnes :

Image

# EMS MySQL Manager Lite 3.2.0.1
# ---------------------------------------
# Host     : localhost
# Port     : 3306
# Database : dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

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

USE `dbpersonnes`;

#
# Structure for the `personnes` table : 
#

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;

#
# Data for the `personnes` table  (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;

Dossier [lib]


Ce dossier contient les archives nécessaires à l’application :

On notera la présence du pilote jdbc du SGBD MySQL. Toutes ces archives font partie du Classpath du projet Eclipse.

19.3. La couche [dao]

La couche [dao] est la suivante :

Image

Nous ne présentons que ce qui change vis à vis de la version [Firebird].

Le fichier de mapping [personne-mysql.xml] est le suivant :


<?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 classe [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - objet [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>
    <!-- liste de toutes les personnes -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- obtenir une personne en particulier -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#value#</select>
    <!-- ajouter une personne -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- mettre à jour une personne -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
        VERSION=#version#</update>
    <!-- supprimer une personne -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
    <!-- obtenir la valeur de la clé primaire [id] de la dernière personne insérée -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

C’est le même contenu que [personnes-firebird.xml] aux détails près suivants :

  • l’ordre SQL " Personne.insertOne " a changé lignes 29-37 :
  • l'ordre SQL d'insertion est exécuté avant l'ordre SELECT qui va permettre de récupérer la valeur de la clé primaire de la ligne insérée
  • l'ordre SQL d'insertion n'a pas de valeur pour la colonne ID de la table [PERSONNES]

Cela reflète l'exemple d'insertion que nous avons commenté paragraphe 19.1.

On notera qu'il y a peut-être là une source possible de problèmes entre threads concurrents. Imaginons deux threads Th1 et Th2 qui font une insertion en même temps. Il y a au total quatre ordres SQL à émettre. Supposons qu'ils soient faits dans l'ordre suivant :

  1. insertion I1 de Th1
  2. insertion I2 de Th2
  3. select S1 de Th1
  4. select S2 de Th2

En 3, Th1 récupère la clé primaire générée lors de la dernière insertion, donc celle de Th2 et non la sienne. Je ne sais pas si la méthode [insert] d'iBATIS est protégée pour ce cas de figure. Nous allons supposer qu'elle le gère proprement. Si ce n'était pas le cas, il nous faudrait dériver la classe d’implémentation [DaoImplCommon] de la couche [dao] en une classe [DaoImplMySQL] où la méthode [insertPersonne] serait synchronisée. Ceci ne résoudrait le problème que pour les threads de notre application. Si ci-dessus, Th1 et Th2 sont des threads de deux applications différentes, il faudrait alors résoudre le problème à la fois avec des transactions et un niveau d'étanchéité adéquat (isolation level) entre transactions. Le niveau [serializable] dans lequel les transactions sont exécutées comme si elles s'exécutaient séquentiellement serait approprié.

On notera que ce problème n'existe pas avec les SGBD Firebird et Postgres qui eux font le SELECT avant l'INSERT. Si on a par exemple la séquence :

  1. select S1 de Th1
  2. select S2 de Th2
  3. insertion I1 de Th1
  4. insertion I2 de Th2

Aux étapes 1 et 2, Th1 et Th2 récupèrent des valeurs de clé primaire auprès du même générateur. Cette opération est normalement atomique, et Th1 et Th2 vont récupérer deux valeurs différentes. Si l'opération n'était pas atomique, et que Th1 et Th2 récupéraient deux valeurs identiques, l'insertion faite en 4 par Th2 échouerait pour cause de doublon de clé primaire. C'est une erreur tout à fait récupérable et Th2 peut retenter l'insertion.

On va laisser l'opération " Personne.insertOne " telle qu'elle est actuellement dans le fichier [personnes-mysql.xml] mais le lecteur doit avoir conscience qu'il y a là potentiellement un problème.

La classe d’implémentation [DaoImplCommon] de la couche [dao] est celle des deux versions précédentes.

La configuration de la couche [dao] a été adaptée au SGBD [MySQL]. Ainsi, le fichier de configuration [spring-config-test-dao-mysql.xml] est-il le suivant :


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- la source de donnéees 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</value>
        </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>
    <!-- la classes d'accè à la couche [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • lignes 5-19 : le bean [dataSource] désigne maintenant la base [MySQL] [dbpersonnes] dont l’administrateur est [root] sans mot de passe. Le lecteur modifiera cette configuration selon son propre environnement.
  • ligne 31 : la classe [DaoImplCommon] est la classe d'implémentation de la couche [dao]

Ces modifications faites, on peut passer aux tests.

19.4. Les tests des couches [dao] et [service]

Les tests des couches [dao] et [service] sont les mêmes que pour la version [Firebird]. Les résultats obtenus sont les suivants :

On constate que les tests ont été passés avec succès avec l'implémentation [DaoImplCommon]. Nous n'aurons pas à dériver cette classe comme il avait été nécessaire de le faire avec le SGBD [Firebird].

19.5. Tests de l’application [web]

Pour tester l’application web avec le SGBD [MySQL], nous construisons un projet Eclipse [mvc-personnes-05B] de façon analogue à celle utilisée pour construire le projet [mvc-personnes-03B] avec la base Firebird (cf paragraphe 17.7). Cependant, comme avec Postgres, nous n'avons pas à recréer les archives [personnes-dao.jar] et [personnes-service.jar] puisque nous n'avons modifié aucune classe.

Nous déployons le projet web [mvc-personnes-05B] au sein de Tomcat :

Le SGBD MySQL est lancé. Le contenu de la table [PERSONNES] est alors le suivant :

Image

Tomcat est lancé à son tour. Avec un navigateur, nous demandons l’url [http://localhost:8080/mvc-personnes-05B] :

Image

Nous ajoutons une nouvelle personne avec le lien [Ajout] :

Nous vérifions l’ajout dans la base de données :

Image

Le lecteur est invité à faire d’autres tests [modification, suppression].