Skip to content

7. [TD]: Implementazione del livello [DAO] del TD con il API JDBC

Parole chiave: database relazionali, API, JDBC, SQLException.

Torniamo all’architettura a livelli della nostra applicazione:

I dati necessari per le elezioni sono memorizzati in un database MySQL [dbelections]

7.1. Support

La cartella [support / chap-07] [1] contiene:

  • i progetti Eclipse di questo capitolo [2];
  • lo script SQL per la creazione del database MySQL, [dbelections], [3];

7.2. Il database [dbelections]


Compito da svolgere: creare il database MySQL e [dblelections] seguendo la procedura descritta al paragrafo 6.4.2.


Il database [dbelections] è un database MySQL con due tabelle:

  

La tabella [conf] contiene le informazioni di configurazione dell’elezione:

 
  • [id]: chiave primaria autoincrementale;
  • [version]: numero di versione del record – in questo caso può essere ignorato;
  • [sap]: numero di seggi da assegnare;
  • [seuilelectoral]: la soglia al di sotto della quale un elenco viene eliminato;

Il suo contenuto è il seguente:

 

La tabella [listes] contiene le liste candidate alle elezioni:

 
  • [id]: chiave primaria autoincrementale;
  • [version]: numero di versione del record – in questo caso può essere ignorato;
  • [nom]: nome della lista;
  • [voix]: voce della lista - è nota solo dopo l'inserimento da parte dell'utente nel livello [présentation];
  • [sieges]: numero di seggi ottenuti - è noto solo dopo il calcolo del livello [métier];
  • [elimine]: pari a 1 se la lista è stata eliminata, pari a 0 in caso contrario - è noto solo dopo il calcolo del livello [métier];

Il contenuto della tabella [listes] è il seguente:

 

Lo script SQL per generare la base [dbelections] si chiama [dbelections.sql] e si trova sul server. Il suo codice è il seguente:


-- phpMyAdmin SQL Dump
-- versione 4.0.4
-- http://www.phpmyadmin.net
--
-- Cliente: localhost
-- Generato il: mercoledì 11 marzo 2015 alle 12:20
-- Versione del server: 5.6.12-log
-- Versione di 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 */;

--
-- Database: `dbelections`
--
CREATE DATABASE IF NOT EXISTS `dbelections` DEFAULT CHARACTER SET utf8 COLLATE utf8_swedish_ci;
USE `dbelections`;

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

--
-- Struttura della tabella `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 ;

--
-- Contenuto della tabella `conf`
--

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

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

--
-- Struttura della tabella `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 ;

--
-- Contenuto della tabella `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. Il progetto Eclipse

Il progetto Eclipse del livello [DAO] sarà il seguente:

  
  • il pacchetto [elections.dao.entities] contiene gli oggetti gestiti dal livello [DAO];
  • il pacchetto [elections.dao.service] contiene l'implementazione del livello [DAO];
  • il pacchetto [elections.dao.config] contiene la configurazione del livello [DAO]
  • il pacchetto [elections.dao.junit] contiene una classe di test JUnit del progetto;
  • il pacchetto [elections.dao.console] contiene una classe di test eseguibile;

7.4. Configurazione del progetto Maven

 

Il file [pom.xml] che configura il progetto Maven è il seguente:


<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>
        <!-- libreria 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>
        <!-- Test di Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Registrazione in Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- plugin -->
    <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>
            <!-- per l'installazione dell'artefatto del progetto nel repository Maven locale -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>

Questo file è analogo a quello descritto al paragrafo 6.5.1. Sono state apportate le seguenti modifiche:

  • righe 8-12: è stato definito un progetto Maven padre. Il progetto [spring-boot-starter-parent] (riga 10) definisce un numero molto elevato di dipendenze con le relative versioni. Quando se ne utilizza una (righe 19-57), non è necessario specificarne la versione, poiché questa è già definita nel progetto Maven padre;
  • righe 40-51: dipendenze necessarie per la classe di test del progetto. Queste dipendenze hanno l’attributo [<scope>test</scope>], il che significa che sono necessarie solo per le classi della cartella [src / test / java]. Queste dipendenze non saranno incluse nell’archivio finale del progetto;
  • righe 53-56: la libreria [spring-boot-starter-logging] verrà utilizzata da Spring per generare log sullo schermo;
  • righe 14-17: proprietà di configurazione Maven:
    • riga 15: indica che i file sorgente sono scritti in UTF-8;
    • riga 16: indica che il compilatore da utilizzare deve essere di livello 1.8;

7.5. Le entità del livello [DAO]

  
  • [ElectionsConfig] è il modello oggetto associato a una riga della tabella [CONF];
  • [ListeElectorale] è il modello oggetto associato a una riga della tabella [LISTES];
  • [AbstactEntity] è la classe padre delle due classi precedenti. Essa raggruppa i campi [id, version] comuni alle due classi;
  • [ElectionsException] è una classe di eccezione;

7.5.1. La classe [ElectionsException]

  

La classe [ElectionsException] è stata descritta al paragrafo 4.3. Ne riportiamo nuovamente il codice:


package ...;

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

// classe di eccezione per l'applicazione Elezioni
// l'eccezione è non controllata

public class ElectionsException extends RuntimeException implements Serializable {

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

    // campi locali
    private int code;
    private List<String> erreurs;

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

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

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

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

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

    // elenco dei messaggi di errore di un'eccezione
    private List<String> getErreursForException(Throwable th) {
        // si recupera l'elenco dei messaggi di errore dell'eccezione
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // si recupera il messaggio solo se !=null e non è vuoto
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa seguente
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getter e setter
...
}

Un oggetto [ElectionsException] è caratterizzato da due informazioni:

  • riga 16: un codice di errore;
  • riga 17: un elenco di messaggi di errore associati alla pila delle eccezioni che si sono verificate;
  • la classe ha 5 costruttori (righe 20, 24, 32, 40, 50);
  • righe 59-76: il metodo [getErreursForException] consente di recuperare i messaggi di errore dallo stack delle eccezioni;

7.5.2. La classe [AbstractEntity]

  

La classe [AbstractEntity] è la seguente:


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;

    // campi
    protected Long id;
    protected Long version;

    // costruttori
    public AbstractEntity() {

    }

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

    // firma
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // getter e setter
...
}

Essa memorizza i campi [ID, NOM] delle righe delle tabelle [CONF] e [LISTES] (righe 8-9).

  • riga 8: la classe presenta l'attributo [Abstract], che indica che non può essere istanziata. Può solo essere derivata;
  • righe 26-33: firma jSON dell'oggetto;
  • riga 28: viene restituita la stringa jSON da [this]. Se al momento dell'esecuzione [this] rappresenta un oggetto derivato da [AbstactEntity], si ottiene la stringa jSON dell'oggetto derivato. Le classi derivate non dovranno quindi definire un metodo [toString]. È sufficiente quello della classe padre;

7.5.3. La classe [ElectionsConfig]

  

La classe [ElectionsConfig] è la seguente:


package elections.dao.entities;


public class ElectionsConfig extends AbstractEntity {

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

    // costruttori
    public ElectionsConfig() {
    }

    public ElectionsConfig(Long id, Long version, int nbSiegesAPourvoir, double seuilElectoral) {
        // genitore
        super(id, version);
        // campi locali
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // getter e setter
...
}
  • riga 4: la classe estende la classe [AbstractEntity];
  • righe 8-9: memorizzano le informazioni delle colonne [SAP, SEUILELECTORAL] della tabella [CONF];

7.5.4. La classe [ListeElectorale]

  

La classe [ListeElectorale] è la seguente:


package elections.dao.entities;

public class ListeElectorale extends AbstractEntity {

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

    // costruttori
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        // genitore
        super();
        // campi locali
        initNom(nom);
        initVoix(voix);
        initSieges(sieges);
        this.elimine=elimine;
    }

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

    // metodi privati
    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");
        }
    }

    // getter e setter

    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;
    }

}
  • riga 3: la classe estende la classe [AbstractEntity];
  • righe 6-9: la classe memorizza le colonne [NOM, VOIX, SIEGES, ELIMINE] della tabella [LISTES];

7.6. Configurazione Spring del livello [DAO]

 

La classe [AppConfig] è una classe di configurazione Spring che configura l'accesso al database nel modo seguente:


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 {

    // costanti
    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() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione dell'accesso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // una connessione inizialmente aperta
        dataSource.setInitialSize(1);
        // risultato
        return dataSource;
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("electionsConfig");
    }
}
  • righe 25-38: l'accesso a BD avverrà tramite una fonte di dati [tomcat-jdbc]. Questo tipo di fonte è stato utilizzato e spiegato nel paragrafo 6.5;
  • righe 17-23: un insieme di costanti statiche accessibili a tutte le classi del progetto;
  • riga 13: l'annotazione [@Configuration] rende la classe [AppConfig] una classe di configurazione Spring;
  • riga 11: l'annotazione [@ComponentScan] indica i pacchetti in cui è possibile trovare oggetti Spring. In questo caso, definiremo un oggetto Spring nel pacchetto [dao]. L'annotazione [@ComponentScan] rende la classe una classe di configurazione, evitando di dover inserire l'annotazione [@Configuration];
  • la riga 12 attiva la gestione di una cache. Il principio è il seguente:
    • si applica l’annotazione [@Cacheable('nom_du_cache')] a un metodo M. Il ‘nome_della-cache’ è il nome utilizzato alla riga 41;
    • quando il metodo M viene chiamato per la prima volta, i suoi risultati vengono restituiti e inseriti anche nella cache indicata dall’annotazione;
    • quando il metodo M viene richiamato nelle volte successive con gli stessi parametri della prima volta, non viene eseguito e Spring si limita a restituire i valori memorizzati nella cache;

7.7. Configurazione dei log

Le librerie di log sono definite dalla seguente dipendenza in [pom.xml]:


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

Questa dipendenza include le seguenti librerie:

  

La libreria [logback] si occuperà dei log. È configurata da due file:

  • [logback.xml] per il ramo principale del codice;
  • [logback-test.xml] per il ramo di test del codice. In assenza di quest'ultimo, viene utilizzato il file precedente;

Entrambi i file devono trovarsi nella cartella [Classpath] del progetto. Per questo motivo, sono collocati nella cartella:

  • [src / main / ressources] per il ramo principale del codice;
  • [src / test / ressources] per il ramo di test del codice;

Il contenuto dei file è lo stesso in entrambi i casi:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- agli encoder viene assegnato per impostazione predefinita il tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- controllo del livello dei log -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Tutto avviene alla riga 12, dove si imposta il livello di dettaglio desiderato:

  • [debug]: il livello più dettagliato;
  • [off]: nessun log;
  • [info]: livello normale dei log;
  • [warn]: come il livello [info] più i messaggi di avviso (warning). Questi messaggi indicano la possibilità di un errore;

Passare alla modalità [debug] non appena compaiono errori durante l'esecuzione.

7.8. Implementazione del livello [DAO]

  

L'interfaccia [IDao] del livello [DAO] è la seguente:


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 {
    // Configurazione delle elezioni
    public ElectionsConfig getElectionsConfig();

    // le liste in lizza
    public ListeElectorale[] getListesElectorales();

    // salvataggio dei risultati elettorali
    public void setListesElectorales(ListeElectorale[] listesElectorales);

}

Lo scheletro della classe [ElectionsDaoJdbc] che implementa il livello [dao] con un database sarà il seguente:


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")
    // recupero della configurazione elettorale
    public ElectionsConfig getElectionsConfig() {
        throw new RuntimeException("[getElectionsConfig] not yet implemented");
    }

    // ottenimento delle liste
    public ListeElectorale[] getListesElectorales() {
        throw new RuntimeException("[getListesElectorales] not yet implemented");
    }

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

    // -------------------- metodi privati

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

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

            }
        }
        // chiudere la connessione
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // viene restituito un [ElectionsException]
                return new ElectionsException(code, e3);
            }
        }
        // nessuna eccezione
        return null;
    }

    // gestione del catch
    private ElectionsException doCatchException(int code1, int code2, Connection connexion, Throwable th) {
        // viene generato un [ElectionsException]
        ElectionsException ex1 = new ElectionsException(code1, th);
        // annullamento della transazione
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
        }
        // si restituisce l'eccezione
        return ex1;
    }
}
  • riga 24: l'annotazione [@Cacheable] è un'annotazione Spring che richiede di memorizzare in cache i risultati del metodo [getElectionsConfig]. È possibile farlo in questo caso perché il contenuto della tabella [CONF] non cambia mai. Non sarebbe possibile applicare questa annotazione al metodo [getListesElectorales] poiché il contenuto della tabella [LISTES] cambia nel tempo;
  • i metodi [doCatchException] e [doFinally] restituiscono un tipo [ElectionsException]. Il metodo [doFinally] restituisce un puntatore null se il rilascio delle risorse è avvenuto senza errori;

Compito da svolgere: scrivere la classe [ElectionDaoJdbc] ispirandosi alla classe [IntroJdbc02] studiata nel paragrafo 6.5.2.


7.9. La classe di test [Main]

  

La classe [Main] è la seguente:


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 {

    // fonte dati
    private static IElectionsDao dao;

    public static void main(String[] args) {
        // si recupera il riferimento al livello [DAO] dopo aver istanziato il contesto Spring
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // recupero della fonte dati
            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);
            }
            // fine
            return;
        }
        // lettura di BD
        ElectionsConfig electionsConfig = null;
        ListeElectorale[] listes;
        try {
            // contenuto delle due tabelle
            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);
            }
            // fine
            return;
        }
        // tutto è andato bene - visualizzazione
        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);
        }
    }

    // metodi privati ------------------
    private static List<String> getErreursForThrowable(Throwable th) {
        // si recupera l'elenco dei messaggi di errore dell'eccezione
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // si recupera il messaggio solo se !=null e non è vuoto
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa successiva
            cause = cause.getCause();
        }
        return erreurs;
    }

}
  • righe 21-31: recupero di un riferimento sul livello [DAO];
  • riga 21: si utilizza una sintassi denominata try_with_resources. La sua sintassi è la seguente:

try (T ressource=expression) {
            // utilizzo delle risorse
...
}
  • (continua)
    • il tipo T della riga 1 deve implementare l'interfaccia [java.lang.AutoCloseable];
    • all'uscita dal blocco delle righe 1-4, la risorsa di tipo [java.lang.AutoCloseable] viene automaticamente liberata, indipendentemente dal fatto che si sia verificata o meno un'eccezione. Chi conosce il linguaggio C# riconoscerà in questo un equivalente sintattico e funzionale della clausola using (T risorsa=espressione);
  • righe 40-49: utilizzo del livello [DAO] per ottenere il contenuto delle tabelle [CONF] e [LISTES];
  • righe 41-47: si verifica la cache [electionsconfig]. Questa è stata definita in due punti:
    • nella classe [ElectionsDaoJdbc]:

  @Cacheable("electionsConfig")
  public ElectionsConfig getElectionsConfig() {
  • (continua)
    • nella classe di configurazione [AppConfig]:

@EnableCaching
public class AppConfig {
...
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("electionsConfig");
  }
}
  • riga 59: chiusura del contesto Spring;
  • righe 62-67: visualizzazione delle informazioni ottenute;

I risultati ottenuti con un livello [DAO] implementato sono i seguenti:

...
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}
  • righe 2-4: mostrano l'influenza della cache:
    • la richiesta 1 dura 80 millisecondi;
    • la richiesta 2 dura 1 millisecondo;

Quando il database è disattivato, si ottengono i seguenti risultati:

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. Test JUnit della classe [ElectionsDaoJdbc]

  

La classe [Test01] è la classe di test successiva alla [JUnit]:


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 {

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

    @Before
    public void init() {
        // si pulisce la tabella [LISTES]
        // liste in competizione
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // si azzerano i voti e i seggi e si imposta «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);
        }
        // si rendono persistenti questi dati tramite il livello [dao]
        electionsDao.setListesElectorales(listes);
    }

    @Test
    public void testElections01() {
        ...
    }
}
  • riga 17: l'annotazione [RunWith], che è un'annotazione [JUnit] (riga 6), garantisce l'integrazione con Spring tramite la classe [SpringJUnit4ClassRunner];
  • riga 16: l'annotazione [SpringApplicationConfiguration] è un'annotazione [Spring] (riga 8) che consente di indicare le classi di configurazione del test JUnit. Qui si indica la classe [AppConfig] utilizzata per configurare il progetto. Si dispone quindi di tutti gli oggetti Spring definiti da questa classe di configurazione. In questo modo è possibile iniettare, alle righe 21-22, un riferimento al livello [DAO] che verrà testato;
  • riga 25: l’annotazione [Before] indica un metodo che deve essere eseguito prima di ogni test;
  • righe 26-41: il metodo [init] azzera i voti e i seggi delle liste presenti nella tabella [LISTES] e imposta il valore booleano [elimine] su [false];

L'unico metodo di test è il seguente:


@Test
    public void testElections01() {
        System.out.println("testElections01-------------------------------------");
        // recupero della configurazione delle elezioni
        ElectionsConfig electionsConfig = electionsDao.getElectionsConfig();
        int nbSiegesAPourvoir = electionsConfig.getNbSiegesAPourvoir();
        double seuilElectoral = electionsConfig.getSeuilElectoral();
        Assert.assertEquals(6, nbSiegesAPourvoir);
        Assert.assertEquals(0.05, seuilElectoral, 1E-6);

        // liste in lizza
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // visualizzazione dei valori letti
        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]);
        }

        // assegnazione di voti e seggi alle liste
        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;
        }

        // si rendono permanenti questi dati tramite il livello [dao]
        electionsDao.setListesElectorales(listes);

        // si rileggono i dati
        ListeElectorale[] listesElectorales2 = electionsDao.getListesElectorales();
        // si verificano i dati letti
        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]);
        }
    }
  • righe 5-9: ci si assicura di poter recuperare il contenuto della tabella [CONF];
  • righe 11-19: si visualizza il contenuto della tabella [LISTES]. Qui non vengono effettuati test, ma solo un controllo visivo;
  • righe 21-35: si modifica nella base di dati la tabella [LISTES] assegnando alle liste i valori relativi ai campi [voix, sieges, elimine];
  • righe 37-51: si rilegge la tabella [LISTES] e si verifica che il risultato ottenuto corrisponda effettivamente a quanto inserito;
  • righe 52-55: verifica visiva;

I risultati in console ottenuti con un livello [DAO] implementato sono i seguenti:

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}
  • righe 1-23: log di Spring Test;
  • righe 25-26: il contenuto della tabella [CONF];
  • righe 27-34: il contenuto iniziale della tabella [LISTES];
  • righe 35-42: il contenuto della tabella [LISTES] dopo l'assegnazione dei valori ai campi [voix, sieges, elimine];

Inoltre, il test JUnit ha esito positivo:

 

7.11. Creazione dell'archivio [with-dependencies] dal livello [dao]

Il progetto finale presenta la seguente architettura:

Ricordiamo la configurazione Maven del progetto:


<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>
        <!-- libreria 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>
        <!-- Test di Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Registrazione in Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- plugin -->
    <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>
            <!-- per l'installazione dell'artifact del progetto nel repository Maven locale -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  

Genereremo un unico file jar che conterrà le classi di tutti i file jar sopra indicati più quelle del progetto del livello [DAO]. Ciò avviene apportando una modifica al file [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>

    <!-- plugin -->
    <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>
  • righe 25-37: configurano un plugin Maven per generare il file JAR;
  • riga 15: indica la versione di Java da utilizzare per la compilazione;

Una volta apportata questa modifica, è possibile generare il file jar nel modo seguente [1-10]:

  • in [5], selezionare la cartella del progetto utilizzando il pulsante [6];
  • in [7], assegnare un nome alla configurazione di esecuzione;
  • in [8], inserire l'elenco degli obiettivi Maven da eseguire:
    • [clean]: svuota la cartella [target] del progetto in cui verrà inserito il file jar generato;
    • [compile]: compila il progetto;
    • [assembly:single]: genera un unico file JAR contenente tutte le classi del progetto e delle sue dipendenze;
  • in [9-10], assicurarsi di disporre di un JDK (Java Development Kit) e non di un JRE (Java Runtime Environment). La differenza sta nel fatto che il JDK include un compilatore, mentre il JRE no. Se non disponete di un JDK, dovete aggiungerne uno in [10] insieme a [11]. A tal fine, seguire la procedura descritta al paragrafo 3.1;
  • in [17], eseguite la configurazione di esecuzione;
  • in [13], l'archivio generato;

È possibile aprire questo archivio con un programma di decompressione:

 

7.12. Test dell'archivio del livello [DAO]

Creiamo un progetto Eclipse standard (non Maven) [1]:

Copiamo e incolliamo il pacchetto [dao.console] dal progetto [elections-dao-jdbc-01] nel progetto [elections-dao-jdbc-02] [2]. Vengono visualizzati numerosi errori poiché la classe [Main] fa riferimento a classi che non sono presenti nel suo [Classpath]. Modificheremo quest’ultimo.

Per prima cosa creiamo una cartella [lib] nel nuovo progetto:

In [9], inseriamo l'archivio creato nel passaggio precedente nella cartella [lib], quindi modifichiamo il Build Path del progetto:

  • in [18], abbiamo importato l’archivio del livello [DAO] creato in precedenza;
  • in [19], il progetto non presenta più errori;

È quindi possibile eseguire la classe [Main]. Si ottengono gli stessi risultati di prima.