Skip to content

7. [TD] : Implémentation de la couche [DAO] du TD avec l'API JDBC

Mots clés : bases de données relationnelles, API JDBC, SQLException.

Revenons sur l'architecture en couches de notre application :

Les données nécessaires à l'élection sont enregistrées dans une base de données MySQL [dbelections]

7.1. Support

Le dossier [support / chap-07] [1] contient :

  • les projets Eclipse de ce chapitre [2] ;
  • le script SQL de création de la base MySQL [dbelections] [3] ;

7.2. La base de données [dbelections]


Travail à faire : créez la base MySQL [dblelections] en suivant la procédure suivie au paragraphe 6.4.2.


La base [dbelections] est une base MySQL avec deux tables :

  

La table [conf] contient les informations de configuration de l'élection :

 
  • [id] : clé primaire auto-incrémentée ;
  • [version] : n° de version de l'enregistrement - peut être ignoré ici ;
  • [sap] : nombre de sièges à pourvoir ;
  • [seuilelectoral] : la barre au-dessous de laquelle une liste est éliminée ;

Son contenu est le suivant :

 

La table [listes] contient les listes candidates de l'élection :

 
  • [id] : clé primaire auto-incrémentée ;
  • [version] : n° de version de l'enregistrement - peut être ignoré ici ;
  • [nom] : nom de la liste ;
  • [voix] : voix de la liste - n'est connu qu'après saisie de l'utilisateur dans la couche [présentation] ;
  • [sieges] : nombre de sièges obtenus - n'est connu qu'après calcul de la couche [métier] ;
  • [elimine] : à 1 si la liste est éliminée, à 0 sinon - n'est connu qu'après calcul de la couche [métier] ;

Le contenu de la table [listes] est le suivant :

 

Le script SQL pour générer la base [dbelections] s'appelle [dbelections.sql] et est sur le serveur. Son code est le suivant :


-- phpMyAdmin SQL Dump
-- version 4.0.4
-- http://www.phpmyadmin.net
--
-- Client: localhost
-- Généré le: Mer 11 Mars 2015 à 12:20
-- Version du serveur: 5.6.12-log
-- Version de PHP: 5.4.12

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Base de données: `dbelections`
--
CREATE DATABASE IF NOT EXISTS `dbelections` DEFAULT CHARACTER SET utf8 COLLATE utf8_swedish_ci;
USE `dbelections`;

-- --------------------------------------------------------

--
-- Structure de la table `conf`
--

CREATE TABLE IF NOT EXISTS `conf` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `version` int(11) NOT NULL DEFAULT '1',
  `sap` tinyint(4) NOT NULL,
  `seuilelectoral` double NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=2 ;

--
-- Contenu de la table `conf`
--

INSERT INTO `conf` (`id`, `version`, `sap`, `seuilelectoral`) VALUES
(1, 1, 6, 0.05);

-- --------------------------------------------------------

--
-- Structure de la table `listes`
--

CREATE TABLE IF NOT EXISTS `listes` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `version` int(11) NOT NULL DEFAULT '1',
  `nom` varchar(20) COLLATE utf8_swedish_ci NOT NULL,
  `voix` int(11) NOT NULL,
  `sieges` int(11) NOT NULL,
  `elimine` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `nom` (`nom`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=8 ;

--
-- Contenu de la table `listes`
--

INSERT INTO `listes` (`id`, `version`, `nom`, `voix`, `sieges`, `elimine`) VALUES
(1, 21, 'A', 10, 1, 0),
(2, 22, 'B', 20, 2, 0),
(3, 21, 'C', 30, 3, 0),
(4, 13, 'D', 40, 3, 0),
(5, 17, 'E', 50, 6, 0),
(6, 18, 'F', 60, 1, 0),
(7, 19, 'G', 70, 2, 0);

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

7.3. Le projet Eclipse

Le projet Eclipse de la couche [DAO] sera le suivant :

  
  • le package [elections.dao.entities] contient les objets manipulés par la couche [DAO] ;
  • le package [elections.dao.service] contient l'implémentation de la couche [DAO] ;
  • le package [elections.dao.config] contient la configuration de la couche [DAO]
  • le package [elections.dao.junit] contient une classe de test JUnit du projet ;
  • le package [elections.dao.console] contient une classe exécutable de test ;

7.4. Configuration du projet Maven

 

Le fichier [pom.xml] qui configure le projet Maven est le suivant :


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-dao-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- bibliothèque jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- pour l'installation de l'artifact du projet dans le dépôt local Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>

Ce fichier est analogue à celui décrit au paragraphe 6.5.1. On y a apporté les modifications suivantes :

  • lignes 8-12 : on a défini un projet Maven parent. Le projet [spring-boot-starter-parent] (ligne 10) définit un très grand nombre de dépendances avec leurs versions. Lorsqu'on utilise l'une d'elles (lignes 19-57), il est alors inutile d'en préciser la version, celle-ci étant définie dans le projet Maven parent ;
  • lignes 40-51 : dépendances nécessaires à la classe de test du projet. Ces dépendances ont l'attribut [<scope>test</scope>] qui signifie qu'elles ne sont nécessaires que pour les classes du dossier [src / test / java]. Ces dépendances ne seront pas incluses dans l'archive finale du projet ;
  • lignes 53-56 : la bibliothèque [spring-boot-starter-logging] sera utilisée par Spring pour faire des logs à l'écran ;
  • lignes 14-17 : des propriétés de configuration Maven :
    • ligne 15 : indique que les fichiers source sont codés en UTF-8 ;
    • ligne 16 : indique que le compilateur à utiliser doit être de niveau 1.8 ;

7.5. Les entités de la couche [DAO]

  
  • [ElectionsConfig] est le modèle objet associé à une ligne de la table [CONF] ;
  • [ListeElectorale] est le modèle objet associé à une ligne de la table [LISTES] ;
  • [AbstactEntity] est la classe parent des deux classes précédentes. Elle factorise les champs [id, version] communs aux deux classes ;
  • [ElectionsException] est une classe d'exception ;

7.5.1. La classe [ElectionsException]

  

La classe [ElectionsException] a été décrite au paragraphe 4.3. Nous redonnons son code :


package ...;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// classe d'exception pour l'application Elections
// l'exception est non contrôlée

public class ElectionsException extends RuntimeException implements Serializable {

    // serial ID
    private static final long serialVersionUID = 1L;

    // champs locaux
    private int code;
    private List<String> erreurs;

    // constructeurs
    public ElectionsException() {
        super();
    }

    public ElectionsException(int code, Throwable e) {
        // parent
        super(e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message, Throwable e) {
        // parent
        super(message,e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message) {
        // parent
        super(message);
        // local
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public ElectionsException(int code, List<String> erreurs) {
        // parent
        super();
        // local
        this.code = code;
        this.erreurs = erreurs;
    }

    // liste des messages d'erreur d'une exception
    private List<String> getErreursForException(Throwable th) {
        // on récupère la liste des messages d'erreur de l'exception
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // on récupère le message seulement s'il est !=null et non blanc
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // cause suivante
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getters et setters
...
}

Un objet [ElectionsException] est caractérisé par deux informations :

  • ligne 16 : un code d'erreur ;
  • ligne 17 : une liste de messages d'erreur associés à la pile des exceptions qui se sont produites ;
  • la classe a 5 constructeurs (lignes 20, 24, 32, 40, 50) ;
  • lignes 59-76 : la méthode [getErreursForException] permet de récupérer les messages d'erreur de la pile d'exceptions ;

7.5.2. La classe [AbstractEntity]

  

La classe [AbstractEntity] est la suivante :


package elections.dao.entities;

import java.io.Serializable;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    // champs
    protected Long id;
    protected Long version;

    // constructeurs
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // signature
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // getters et setters
...
}

Elle mémorise les champs [ID, NOM] des lignes des tables [CONF] et [LISTES] (lignes 8-9).

  • ligne 8 : la classe a l'attribut [Abstract] indiquant qu'elle ne peut être instanciée. Elle ne peut être que dérivée ;
  • lignes 26-33 : signature jSON de l'objet ;
  • ligne 28 : on retourne la chaîne jSON de [this]. Si au moment de l'exécution, [this] représente un objet dérivé de [AbstactEntity], c'est la chaîne jSON de l'objet dérivé qui est obtenue. Les classes dérivées n'auront ainsi pas besoin de définir une méthode [toString]. Celle de la classe parent est suffisante ;

7.5.3. La classe [ElectionsConfig]

  

La classe [ElectionsConfig] est la suivante :


package elections.dao.entities;


public class ElectionsConfig extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // champs
    private int nbSiegesAPourvoir;
    private double seuilElectoral;

    // constructeurs
    public ElectionsConfig() {
    }

    public ElectionsConfig(Long id, Long version, int nbSiegesAPourvoir, double seuilElectoral) {
        // parent
        super(id, version);
        // champs locaux
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // getters et setters
...
}
  • ligne 4 : la classe étend la classe [AbstractEntity] ;
  • lignes 8-9 : mémorisent les informations des colonnes [SAP, SEUILELECTORAL] de la table [CONF] ;

7.5.4. La classe [ListeElectorale]

  

La classe [ListeElectorale] est la suivante :


package elections.dao.entities;

public class ListeElectorale extends AbstractEntity {

    // champs
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;

    // constructeurs
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        // parent
        super();
        // champs locaux
        initNom(nom);
        initVoix(voix);
        initSieges(sieges);
        this.elimine=elimine;
    }

    public ListeElectorale(Long id, Long version, String nom, int voix, int sieges, boolean elimine) {
        // parent
        super(id, version);
        // champs locaux
        initNom(nom);
        initVoix(voix);
        initSieges(sieges);
        this.elimine=elimine;
    }

    // méthodes privées
    private void initNom(String nom) {
        this.nom = nom.trim();
        if ("".equals(nom)) {
            throw new ElectionsException(10, "Le nom ne peut être vide");
        }
    }

    private void initVoix(int voix) {
        this.voix = voix;
        if (voix < 0) {
            throw new ElectionsException(11, "Le nombre de voix ne peut être <0");
        }
    }

    private void initSieges(int sieges) {
        this.sieges = sieges;
        if (sieges < 0) {
            throw new ElectionsException(12, "Le nombre de sièges ne peut être <0");
        }
    }

    // getters et setters

    public String getNom() {
        return nom;
    }

    public void setNom(String nom) {
        initNom(nom);
    }

    public int getVoix() {
        return voix;
    }

    public void setVoix(int voix) {
        initVoix(voix);
    }

    public int getSieges() {
        return sieges;
    }

    public void setSieges(int sieges) {
        initSieges(sieges);
    }

    public boolean isElimine() {
        return elimine;
    }

    public void setElimine(boolean elimine) {
        this.elimine = elimine;
    }

}
  • ligne 3 : la classe étend la classe [AbstractEntity] ;
  • lignes 6-9 : la classe mémorise les colonnes [NOM, VOIX, SIEGES, ELIMINE] de la table [LISTES] ;

7.6. Configuration Spring de la couche [DAO]

 

La classe [AppConfig] est une classe de configuration Spring qui configure l'accès à la base de données de la façon suivante :


package elections.dao.config;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@ComponentScan(basePackages = { "elections.dao.service" })
@EnableCaching
public class AppConfig {

    // constantes
    public final static String URL = "jdbc:mysql://localhost:3306/dbelections";
    public final static String USER = "root";
    public final static String PASSWD = "";
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String SELECT_LISTES = "SELECT ID, VERSION, NOM, VOIX, SIEGES, ELIMINE FROM LISTES";
    public final static String SELECT_CONF = "SELECT ID, VERSION, SAP, SEUILELECTORAL, SAP FROM CONF";
    public final static String UPDATE_LISTES = "UPDATE LISTES SET VOIX=?, SIEGES=?, ELIMINE=? WHERE ID=?";

    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("electionsConfig");
    }
}
  • lignes 25-38 : l'accès à la BD se fera via une source de données [tomcat-jdbc]. Ce type de source a été utilisée et expliquée au paragraphe 6.5 ;
  • lignes 17-23 : un ensemble de constantes statiques accessibles par toutes les classes du projet ;
  • ligne 13 : l'annotation [@Configuration] fait de la classe [AppConfig] une classe de configuration Spring ;
  • ligne 11 : l'annotation [@ComponentScan] indique les packages où on peut trouver des objets Spring. Ici, nous allons définir un objet Spring dans le package [dao]. L'annotation [@ComponentScan] fait de la classe une classe de configuration qui nous épargne de mettre l'annotation [@Configuration] ;
  • la ligne 12 active la gestion d'un cache. Le principe est la suivant :
    • on met l'annotation [@Cacheable('nom_du_cache')] à une méthode M. Le 'nom_du-cache' est le nom utilisé ligne 41 ;
    • lorsque la méthode M est appelée la 1ère fois, ses résultats sont rendus et également mis dans le cache nommé par l'annotation ;
    • lorsque la méthode M est appelée les fois suivantes avec les mêmes paramètres que la 1ère fois, elle n'est pas exécutée et Spring se contente de rendre les valeurs mises en cache ;

7.7. Configuration des logs

Les bibliothèques de logs sont définies par la dépendance suivante dans [pom.xml] :


        <!-- Spring Boot Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

Cette dépendance amène les bibliothèques suivantes :

  

C'est la bibliothèque [logback] qui va assurer les logs. Elle est configurée par deux fichiers :

  • [logback.xml] pour la branche principale du code ;
  • [logback-test.xml] pour la branche de test du code. En l'absence de ce fichier, le fichier précédent est alors utilisé ;

Ces deux fichiers doivent se trouver dans le [Classpath] du projet. Pour cette raison, ils sont placés dans le dossier :

  • [src / main / ressources] pour la branche principale du code ;
  • [src / test / ressources] pour la branche de test du code ;

Le contenu des fichiers est ici le même :


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- contrôle niveau des logs -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Tout se passe ligne 12 où on fixe le niveau d'information désiré :

  • [debug] : le niveau le plus détaillé ;
  • [off] : pas de logs ;
  • [info] : le niveau normal des logs ;
  • [warn] : idem niveau [info] plus les messages d'avertissement (warning). Ces messages indiquent une possibilité d'erreur ;

Passez en mode [debug] dès que des erreurs apparaissent à l'exécution.

7.8. Implémentation de la couche [DAO]

  

L'interface [IDao] de la couche [DAO] est la suivante :


package istia.st.elections.webapi.client.dao;

import istia.st.elections.webapi.client.entities.ElectionsConfig;
import istia.st.elections.webapi.client.entities.ListeElectorale;

public interface IDao {
    // configuration des élections
    public ElectionsConfig getElectionsConfig();

    // les listes en compétition
    public ListeElectorale[] getListesElectorales();

    // sauvegarde des résultats de l'élection
    public void setListesElectorales(ListeElectorale[] listesElectorales);

}

Le squelette de la classe [ElectionsDaoJdbc] implémentant la couche [dao] avec une base de données sera le suivant :


package elections.dao.service;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.dao.entities.ListeElectorale;

@Component
@SuppressWarnings("unused")
public class ElectionDaoJdbc implements IElectionsDao {

    @Autowired
    private DataSource dataSource;

    @Cacheable("electionsConfig")
    // obtention conf de l'élection
    public ElectionsConfig getElectionsConfig() {
        throw new RuntimeException("[getElectionsConfig] not yet implemented");
    }

    // obtention des listes
    public ListeElectorale[] getListesElectorales() {
        throw new RuntimeException("[getListesElectorales] not yet implemented");
    }

    // modification des listes [voix, sieges, elimine]
    public void setListesElectorales(ListeElectorale[] listesElectorales) {
        throw new RuntimeException("[setListesElectorales] not yet implemented");
    }

    // -------------------- méthodes privées

    // gestion du finally
    private ElectionsException doFinally(int code, ResultSet rs, PreparedStatement ps, Connection connexion) {
        // fermeture ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // fermeture [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // fermer la connexion
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // on retourne une [ElectionsException]
                return new ElectionsException(code, e3);
            }
        }
        // pas d'exception
        return null;
    }

    // gestion du catch
    private ElectionsException doCatchException(int code1, int code2, Connection connexion, Throwable th) {
        // on génère une [ElectionsException]
        ElectionsException ex1 = new ElectionsException(code1, th);
        // annulation transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
        }
        // on retourne l'exception
        return ex1;
    }
}
  • ligne 24 : l'annotation [@Cacheable] est une annotation Spring qui demande de mettre en mémoire ('cacher') les résultats de la méthode [getElectionsConfig]. Il est possible de faire cela ici parce que le contenu de la table [CONF] ne change jamais. Il ne serait pas possible de mettre cette annotation sur la méthode [getListesElectorales] car le contenu de la table [LISTES] change au cours du temps ;
  • les méthodes [doCatchException] et [doFinally] retournent un type [ElectionsException]. La méthode [doFinally] rend un pointeur null si la libération des ressources s'est faite sans erreurs ;

Travail à faire : écrivez la classe [ElectionDaoJdbc] en vous inspirant de la classe [IntroJdbc02] étudiée au paragraphe 6.5.2.


7.9. La classe de test [Main]

  

La classe [Main] est la suivante :


package elections.dao.console;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import elections.dao.config.AppConfig;
import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.dao.entities.ListeElectorale;
import elections.dao.service.IElectionsDao;

public class Main {

    // source de données
    private static IElectionsDao dao;

    public static void main(String[] args) {
        // on récupère la référence de la couche [DAO] après avoir instancié le contexte Spring
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // récupération de la source de données
            dao = ctx.getBean(IElectionsDao.class);
        } catch (Exception e) {
            System.out.println("Les erreurs suivantes se sont produites lors de l'initialisation du contexte Spring -------");
            for (String erreur : getErreursForThrowable(e)) {
                System.out.println(erreur);
            }
            // fin
            return;
        }
        // lecture de la BD
        ElectionsConfig electionsConfig = null;
        ListeElectorale[] listes;
        try {
            // contenu des deux tables
            electionsConfig = dao.getElectionsConfig();
            listes = dao.getListesElectorales();
        } catch (ElectionsException e) {
            System.out.println("Les erreurs suivantes se sont produites lors de la lecture des tables ----------");
            for (String erreur : e.getErreurs()) {
                System.out.println(erreur);
            }
            // fin
            return;
        }
        // tout s'est bien passé - affichage
        System.out.println(String.format("Nombre de sièges à pourvoir : %d", electionsConfig.getNbSiegesAPourvoir()));
        System.out.println(String.format("Seuil électoral : %5.2f", electionsConfig.getSeuilElectoral()));
        System.out.println("Listes candidates----------------");
        for (ListeElectorale liste : listes) {
            System.out.println(liste);
        }
    }

    // méthodes privées ------------------
    private static List<String> getErreursForThrowable(Throwable th) {
        // on récupère la liste des messages d'erreur de l'exception
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // on récupère le message seulement s'il est !=null et non blanc
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // cause suivante
            cause = cause.getCause();
        }
        return erreurs;
    }

}
  • lignes 21-31 : récupération d'une référence sur la couche [DAO] ;
  • ligne 21 : on utilise une syntaxe appelée try_with_resources. Sa syntaxe est la suivante :

try (T ressource=expression) {
            // exploitation de ressource
...
}
  • (suite)
    • le type T de la ligne 1 doit implémenter l'interface [java.lang.AutoCloseable] ;
    • à la sortie du bloc des lignes 1-4, la ressource de type [java.lang.AutoCloseable] est automatiquement libérée qu'il y ait eu exception ou pas. Ceux qui connaissent le langage C# reconnaîtront là un frère syntaxique et fonctionnel de la clause using (T ressource=expression) ;
  • lignes 40-49 : utilisation de la couche [DAO] pour obtenir le contenu des tables [CONF] et [LISTES] ;
  • lignes 41-47 : on teste le cache [electionsconfig]. Celui-ci a été défini à deux endroits :
    • dans la classe [ElectionsDaoJdbc] :

  @Cacheable("electionsConfig")
  public ElectionsConfig getElectionsConfig() {
  • (suite)
    • dans la classe de configuration [AppConfig] :

@EnableCaching
public class AppConfig {
...
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("electionsConfig");
  }
}
  • ligne 59 : fermeture du contexte Spring ;
  • lignes 62-67 : affichage des informations obtenues ;

Les résultats obtenus avec une couche [DAO] implémentée sont les suivants :

...
début requête 1 : 11:09:29:752
fin requête 1 et début requête 2: 11:09:30:132
fin requête 2 : 11:09:30:133
...
Nombre de sièges à pourvoir : 6
Seuil électoral :  0,05
Listes candidates----------------
{"id":1,"version":9,"nom":"A","voix":32000,"sieges":2,"elimine":false}
{"id":2,"version":13,"nom":"B","voix":25000,"sieges":2,"elimine":false}
{"id":3,"version":14,"nom":"C","voix":16000,"sieges":1,"elimine":false}
{"id":4,"version":13,"nom":"D","voix":12000,"sieges":1,"elimine":false}
{"id":5,"version":14,"nom":"E","voix":8000,"sieges":0,"elimine":false}
{"id":6,"version":13,"nom":"F","voix":4500,"sieges":0,"elimine":true}
{"id":7,"version":13,"nom":"G","voix":2500,"sieges":0,"elimine":true}
  • lignes 2-4 : montrent l'influence du cache :
    • la requête 1 dure 80 millisecondes ;
    • la requête 2 dure 1 milliseconde ;

Lorsque la base de données est coupée on obtient les résultats suivants :

1
2
3
4
5
Les erreurs suivantes se sont produites lors de la lecture des tables ----------
Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
Connection refused: connect

7.10. Tests JUnit de la classe [ElectionsDaoJdbc]

  

La classe [Test01] est la classe de test [JUnit] suivante :


package elections.dao.junit;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import elections.dao.config.AppConfig;
import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ListeElectorale;
import elections.dao.service.IElectionsDao;


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // couche [DAO]
    @Autowired
    private IElectionsDao electionsDao;

    @Before
    public void init() {
        // on nettoie la table [LISTES]
        // listes en compétition
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // on met à 0 les voix et sièges et elimine à false
        int voix = 0;
        int sièges = 0;
        boolean elimine = false;
        for (ListeElectorale liste : listes) {
            liste.setVoix(voix);
            liste.setSieges(sièges);
            liste.setElimine(elimine);
        }
        // on rend ces données persistantes grâce à la couche [dao]
        electionsDao.setListesElectorales(listes);
    }

    @Test
    public void testElections01() {
        ...
    }
}
  • ligne 17 : l'annotation [RunWith] qui est une annotation [JUnit] (ligne 6) assure l'intégration avec Spring via la classe [SpringJUnit4ClassRunner] ;
  • ligne 16 : l'annotation [SpringApplicationConfiguration] est une annotation [Spring] (ligne 8) qui permet de désigner les classes de configuration du test JUnit. Ici on désigne la classe [AppConfig] utilisée pour configurer le projet. On dispose alors de tous les objets Spring définis par cette classe de configuration. C'est ainsi qu'on peut injecter lignes 21-22 une référence sur la couche [DAO] qui va être testée ;
  • ligne 25 : l'annotation [Before] désigne une méthode qui doit être exécutée avant chaque test ;
  • lignes 26-41 : la méthode [init] met les voix et sièges des listes de la table [LISTES] à zéro et le booléen [elimine] à [false] ;

L'unique méthode de test est la suivante :


@Test
    public void testElections01() {
        System.out.println("testElections01-------------------------------------");
        // récupération de la configuration des élections
        ElectionsConfig electionsConfig = electionsDao.getElectionsConfig();
        int nbSiegesAPourvoir = electionsConfig.getNbSiegesAPourvoir();
        double seuilElectoral = electionsConfig.getSeuilElectoral();
        Assert.assertEquals(6, nbSiegesAPourvoir);
        Assert.assertEquals(0.05, seuilElectoral, 1E-6);

        // listes en compétition
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // affichage valeurs lues
        System.out.println("Nombre de sièges à pourvoir : " + nbSiegesAPourvoir);
        System.out.println("Seuil électoral : " + seuilElectoral);
        System.out.println("Listes en compétition ---------------------");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }

        // on affecte des voix et des sièges aux listes
        int voix = 0;
        int sièges = 0;
        boolean elimine = false;
        for (ListeElectorale liste : listes) {
            liste.setVoix(voix);
            liste.setSieges(sièges);
            liste.setElimine(elimine);
            voix += 10;
            sièges += 1;
            elimine = !elimine;
        }

        // on rend ces données persistantes grâce à la couche [dao]
        electionsDao.setListesElectorales(listes);

        // on relit les données
        ListeElectorale[] listesElectorales2 = electionsDao.getListesElectorales();
        // on vérifie les données lues
        Assert.assertEquals(7, listesElectorales2.length);
        voix = 0;
        sièges = 0;
        elimine = false;
        for (ListeElectorale liste : listesElectorales2) {
            Assert.assertEquals(voix, liste.getVoix());
            Assert.assertEquals(sièges, liste.getSieges());
            Assert.assertEquals(elimine, liste.isElimine());
            voix += 10;
            sièges += 1;
            elimine = !elimine;
        }
        System.out.println("Listes en compétition ---------------------");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }
    }
  • lignes 5-9 : on s'assure qu'on est capable de récupérer le contenu de la table [CONF] ;
  • lignes 11-19 : on affiche le contenu de la table [LISTES]. Il n'y a pas de tests ici, seulement un contrôle visuel ;
  • lignes 21-35 : on modifie en base, la table [LISTES] en donnant aux listes des valeurs pour leurs champs [voix, sieges, elimine] ;
  • lignes 37-51 : on relit la table [LISTES] et on vérifie que ce qui est obtenu est bien égal à ce qui a été mis ;
  • lignes 52-55 : vérification visuelle ;

Les résultats console obtenus avec une couche [DAO] implémentée sont les suivants :

mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper getDefaultTestExecutionListenerClassNames
INFOS: Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper instantiateListeners
INFOS: Could not instantiate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener]. Specify custom listener classes or make the default listener classes (and their required dependencies) available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper instantiateListeners
INFOS: Could not instantiate TestExecutionListener [org.springframework.test.context.web.ServletTestExecutionListener]. Specify custom listener classes or make the default listener classes (and their required dependencies) available. Offending class: [javax/servlet/ServletContext]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper instantiateListeners
INFOS: Could not instantiate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]. Specify custom listener classes or make the default listener classes (and their required dependencies) available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper getTestExecutionListeners
INFOS: Using TestExecutionListeners: [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@483bf400, org.springframework.test.context.support.DirtiesContextTestExecutionListener@21a06946]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

[2015-03-11 16:50:01.272] - 11696 INFOS [main] --- org.eclipse.jdt.internal.junit.runner.RemoteTestRunner: Starting RemoteTestRunner on Gportpers3 with PID 11696 (started by ST in D:\data\istia-1415\eclipse\intro-jdbc\elections-jdbc-01)
[2015-03-11 16:50:01.317] - 11696 INFOS [main] --- org.springframework.context.annotation.AnnotationConfigApplicationContext: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@74ad1f1f: startup date [Wed Mar 11 16:50:01 CET 2015]; root of context hierarchy
mars 11, 2015 4:50:01 PM org.eclipse.jdt.internal.junit.runner.RemoteTestRunner logStarted
INFOS: Started RemoteTestRunner in 0.775 seconds (JVM running for 1.433)
testElections01-------------------------------------
Nombre de sièges à pourvoir : 6
Seuil électoral : 0.05
Listes en compétition ---------------------
{"id":1,"version":21,"nom":"A","voix":0,"sieges":0,"elimine":false}
{"id":2,"version":22,"nom":"B","voix":0,"sieges":0,"elimine":false}
{"id":3,"version":21,"nom":"C","voix":0,"sieges":0,"elimine":false}
{"id":4,"version":13,"nom":"D","voix":0,"sieges":0,"elimine":false}
{"id":5,"version":17,"nom":"E","voix":0,"sieges":0,"elimine":false}
{"id":6,"version":18,"nom":"F","voix":0,"sieges":0,"elimine":false}
{"id":7,"version":19,"nom":"G","voix":0,"sieges":0,"elimine":false}
Listes en compétition ---------------------
{"id":1,"version":21,"nom":"A","voix":0,"sieges":0,"elimine":false}
{"id":2,"version":22,"nom":"B","voix":10,"sieges":1,"elimine":true}
{"id":3,"version":21,"nom":"C","voix":20,"sieges":2,"elimine":false}
{"id":4,"version":13,"nom":"D","voix":30,"sieges":3,"elimine":true}
{"id":5,"version":17,"nom":"E","voix":40,"sieges":4,"elimine":false}
{"id":6,"version":18,"nom":"F","voix":50,"sieges":5,"elimine":true}
{"id":7,"version":19,"nom":"G","voix":60,"sieges":6,"elimine":false}
  • lignes 1-23 : logs de Spring Test ;
  • lignes 25-26 : le contenu de la table [CONF] ;
  • lignes 27-34 : le contenu initial de la table [LISTES] ;
  • lignes 35-42 : le contenu de la table [LISTES] après affectation de valeurs aux champs [voix, sieges, elimine] ;

Par ailleurs le test JUnit réussit :

 

7.11. Création de l'archive [with-dependencies] de la couche [dao]

Le projet final a l'architecture suivante :

Rappelons la configuration Maven du projet :


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-dao-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- bibliothèque jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- pour l'installation de l'artifact du projet dans le dépôt local Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  

Nous allons générer un unique jar qui contiendra les classes de tous les jars ci-dessus plus celles du projet de la couche [DAO]. Cela se fait à l'aide d'une modification dans le fichier [pom.xml] :


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        ...
    </dependencies>

    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>console.Main</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • lignes 25-37 : configurent un plugin Maven pour générer le jar ;
  • ligne 15 : indique la version de Java à utiliser pour la compilation ;

Une fois cette modification faite, on peut générer le jar de la façon suivante [1-10] :

  • en [5], mettre le dossier du projet en utilisant le bouton [6] ;
  • en [7], donner un nom à la configuration d'exécution ;
  • en [8], mettre la liste des buts Maven à exécuter :
    • [clean] : vide le dossier [target] du projet dans lequel va être placé le jar généré ;
    • [compile] : compile le projet ;
    • [assembly:single] : génère un unique jar comprenant toutes les classes du projet et de ses dépendances ;
  • en [9-10], vérifiez que vous avez un JDK (Java Development Kit) et non un JRE (Java Runtime Environment). La différence est que le JDK a un compilateur et pas le JRE. Si vous n'avez pas un JDK, il vous faut en ajouter un dans [10] avec [11]. Pour cela, suivre la procédure décrite au paragraphe 3.1 ;
  • en [17], exécutez la configuration d'exécution ;
  • en [13], l'archive générée ;

On peut ouvrir cette archive avec un dézippeur :

 

7.12. Test de l'archive de la couche [DAO]

Créons un projet Eclipse standard (pas Maven) [1] :

Faisons un copier / coller du package [dao.console] du projet [elections-dao-jdbc-01] dans le projet [elections-dao-jdbc-02] [2]. De nombreuses erreurs apparaissent car la classe [Main] référence des classes qui ne sont pas dans son [Classpath]. Nous allons modifier celui-ci.

Nous créons tout d'abord [2-8] un dossier [lib] dans le nouveau projet :

En [9], nous mettons l'archive créée à l'étape précédente dans le dossier [lib] puis nous modifions le Build Path du projet :

  • en [18], on a importé l'archive de la couche [DAO] créée précédemment ;
  • en [19], le projet ne présente plus d'erreurs ;

On peut alors exécuter la classe [Main]. On obtient les mêmes résultats que précédemment.