Skip to content

7. [TD]: Implementación de la capa [DAO] de TD con API y JDBC

Palabras clave: bases de datos relacionales, API, JDBC, SQLException.

Volvamos a la arquitectura por capas de nuestra aplicación:

Los datos necesarios para las elecciones se almacenan en una base de datos MySQL [dbelections]

7.1. Support

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

  • los proyectos de Eclipse de este capítulo [2];
  • el script SQL para la creación de la base de datos MySQL, [dbelections] y [3];

7.2. La base de datos [dbelections]


Tarea a realizar: cree las bases de datos MySQL y [dblelections] siguiendo el procedimiento descrito en el apartado 6.4.2.


La base [dbelections] es una base MySQL con dos tablas:

  

La tabla [conf] contiene la información de configuración de la elección:

 
  • [id]: clave primaria autoincrementada;
  • [version]: número de versión del registro; aquí se puede omitir;
  • [sap]: número de escaños por cubrir;
  • [seuilelectoral]: el umbral por debajo del cual se descarta una lista;

Su contenido es el siguiente:

 

La tabla [listes] contiene las listas de candidatos de las elecciones:

 
  • [id]: clave primaria autoincrementada;
  • [version]: número de versión del registro; aquí se puede omitir;
  • [nom]: nombre de la lista;
  • [voix]: voz de la lista —solo se conoce tras la introducción de datos por parte del usuario en la capa [présentation]—;
  • [sieges]: número de escaños obtenidos —solo se conoce tras el cálculo de la capa [métier]—;
  • [elimine]: 1 si la lista queda eliminada, 0 en caso contrario; solo se conoce tras el cálculo de la capa [métier];

El contenido de la tabla [listes] es el siguiente:

 

El script SQL para generar la base [dbelections] se llama [dbelections.sql] y se encuentra en el servidor. Su código es el siguiente:


-- phpMyAdmin SQL Volcado
-- versión 4.0.4
-- http://www.phpmyadmin.net
--
-- Cliente: localhost
-- Generado el: miércoles, 11 de marzo de 2015 a las 12:20
-- Versión del servidor: 5.6.12-log
-- Versión 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 datos: `dbelections`
--
CREATE DATABASE IF NOT EXISTS `dbelections` DEFAULT CHARACTER SET utf8 COLLATE utf8_swedish_ci;
USE `dbelections`;

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

--
-- Estructura de la tabla `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 ;

--
-- Contenido de la tabla `conf`
--

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

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

--
-- Estructura de la tabla `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 ;

--
-- Contenido de la tabla `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. El proyecto Eclipse

El proyecto Eclipse de la capa [DAO] será el siguiente:

  
  • el paquete [elections.dao.entities] contiene los objetos manipulados por la capa [DAO];
  • el paquete [elections.dao.service] contiene la implementación de la capa [DAO];
  • el paquete [elections.dao.config] contiene la configuración de la capa [DAO]
  • el paquete [elections.dao.junit] contiene una clase de prueba JUnit del proyecto;
  • el paquete [elections.dao.console] contiene una clase ejecutable de prueba;

7.4. Configuración del proyecto Maven

 

El archivo [pom.xml] que configura el proyecto Maven es el siguiente:


<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>
        <!-- biblioteca 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>
        <!-- Prueba de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Registro de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- complementos -->
    <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>
            <!-- para la instalación del artefacto del proyecto en el repositorio local de Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>

Este archivo es similar al descrito en el apartado 6.5.1. Se le han realizado las siguientes modificaciones:

  • líneas 8-12: se ha definido un proyecto Maven principal. El proyecto [spring-boot-starter-parent] (línea 10) define un gran número de dependencias con sus respectivas versiones. Cuando se utiliza una de ellas (líneas 19-57), no es necesario especificar la versión, ya que esta queda definida en el proyecto Maven principal;
  • líneas 40-51: dependencias necesarias para la clase de prueba del proyecto. Estas dependencias tienen el atributo [<scope>test</scope>], lo que significa que solo son necesarias para las clases de la carpeta [src / test / java]. Estas dependencias no se incluirán en el archivo final del proyecto;
  • líneas 53-56: Spring utilizará la biblioteca [spring-boot-starter-logging] para generar registros en pantalla;
  • líneas 14-17: propiedades de configuración de Maven:
    • línea 15: indica que los archivos fuente están escritos en UTF-8;
    • línea 16: indica que el compilador que se debe utilizar debe ser de nivel 1.8;

7.5. Las entidades de la capa [DAO]

  
  • [ElectionsConfig] es el modelo de objeto asociado a una fila de la tabla [CONF];
  • [ListeElectorale] es el modelo de objetos asociado a una fila de la tabla [LISTES];
  • [AbstactEntity] es la clase padre de las dos clases anteriores. Agrupa los campos [id, version] comunes a ambas clases;
  • [ElectionsException] es una clase de excepción;

7.5.1. La clase [ElectionsException]

  

La clase [ElectionsException] se ha descrito en el apartado 4.3. A continuación, volvemos a indicar su código:


package ...;

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

// Clase de excepción para la aplicación «Elections»
// la excepción es no controlada

public class ElectionsException extends RuntimeException implements Serializable {

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

    // campos locales
    private int code;
    private List<String> erreurs;

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

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

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

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

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

    // lista de mensajes de error de una excepción
    private List<String> getErreursForException(Throwable th) {
        // se recupera la lista de mensajes de error de la excepción
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // se recupera el mensaje solo si !=null y no está vacío
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa siguiente
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getters y setters
...
}

Un objeto [ElectionsException] se caracteriza por dos datos:

  • línea 16: un código de error;
  • línea 17: una lista de mensajes de error asociados a la pila de excepciones que se han producido;
  • la clase tiene 5 constructores (líneas 20, 24, 32, 40, 50);
  • líneas 59-76: el método [getErreursForException] permite recuperar los mensajes de error de la pila de excepciones;

7.5.2. La clase [AbstractEntity]

  

La clase [AbstractEntity] es la siguiente:


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;

    // campos
    protected Long id;
    protected Long version;

    // constructores
    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;
        }
    }
    
    // getters y setters
...
}

Almacena los campos [ID, NOM] de las líneas de las tablas [CONF] y [LISTES] (líneas 8-9).

  • línea 8: la clase tiene el atributo [Abstract], que indica que no se puede instanciar. Solo puede derivarse;
  • líneas 26-33: firma jSON del objeto;
  • línea 28: se devuelve la cadena jSON de [this]. Si, en el momento de la ejecución, [this] representa un objeto derivado de [AbstactEntity], se obtiene la cadena jSON del objeto derivado. De este modo, las clases derivadas no tendrán que definir un método [toString]. Basta con el de la clase padre;

7.5.3. La clase [ElectionsConfig]

  

La clase [ElectionsConfig] es la siguiente:


package elections.dao.entities;


public class ElectionsConfig extends AbstractEntity {

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

    // constructores
    public ElectionsConfig() {
    }

    public ElectionsConfig(Long id, Long version, int nbSiegesAPourvoir, double seuilElectoral) {
        // padre
        super(id, version);
        // campos locales
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // getters y setters
...
}
  • línea 4: la clase hereda de la clase [AbstractEntity];
  • líneas 8-9: almacenan la información de las columnas [SAP, SEUILELECTORAL] de la tabla [CONF];

7.5.4. La clase [ListeElectorale]

  

La clase [ListeElectorale] es la siguiente:


package elections.dao.entities;

public class ListeElectorale extends AbstractEntity {

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

    // constructores
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        // padre
        super();
        // campos locales
        initNom(nom);
        initVoix(voix);
        initSieges(sieges);
        this.elimine=elimine;
    }

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

    // métodos privados
    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 y 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;
    }

}
  • línea 3: la clase hereda de la clase [AbstractEntity];
  • líneas 6-9: la clase almacena las columnas [NOM, VOIX, SIEGES, ELIMINE] de la tabla [LISTES];

7.6. Configuración de Spring de la capa [DAO]

 

La clase [AppConfig] es una clase de configuración de Spring que configura el acceso a la base de datos de la siguiente manera:


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() {
        // fuente de datos TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuración de acceso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // una conexión abierta inicialmente
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("electionsConfig");
    }
}
  • líneas 25-38: el acceso a BD se realizará a través de una fuente de datos [tomcat-jdbc]. Este tipo de fuente se ha utilizado y explicado en el apartado 6.5;
  • líneas 17-23: un conjunto de constantes estáticas accesibles para todas las clases del proyecto;
  • línea 13: la anotación [@Configuration] convierte la clase [AppConfig] en una clase de configuración de Spring;
  • línea 11: la anotación [@ComponentScan] indica los paquetes en los que se pueden encontrar objetos de Spring. En este caso, vamos a definir un objeto de Spring en el paquete [dao]. La anotación [@ComponentScan] convierte la clase en una clase de configuración, lo que nos ahorra tener que incluir la anotación [@Configuration];
  • la línea 12 activa la gestión de una caché. El principio es el siguiente:
    • se aplica la anotación [@Cacheable('nom_du_cache')] a un método M. El «nombre_de_la_caché» es el nombre utilizado en la línea 41;
    • cuando se invoca el método M por primera vez, se devuelven sus resultados y también se almacenan en la caché indicada por la anotación;
    • cuando se invoca el método M las veces siguientes con los mismos parámetros que la primera vez, no se ejecuta y Spring se limita a devolver los valores almacenados en la caché;

7.7. Configuración de los registros

Las bibliotecas de registros se definen mediante la siguiente dependencia en [pom.xml]:


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

Esta dependencia incluye las siguientes bibliotecas:

  

La biblioteca [logback] se encargará de los registros. Se configura mediante dos archivos:

  • [logback.xml] para la rama principal del código;
  • [logback-test.xml] para la rama de pruebas del código. Si no existe este archivo, se utiliza el anterior;

Estos dos archivos deben encontrarse en el directorio [Classpath] del proyecto. Por este motivo, se colocan en la carpeta:

  • [src / main / ressources] para la rama principal del código;
  • [src / test / ressources] para la rama de pruebas del código;

El contenido de los archivos es el mismo en ambos casos:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- a los codificadores se les asigna por defecto el tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- control del nivel de los registros -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Todo se decide en la línea 12, donde se establece el nivel de información deseado:

  • [debug]: el nivel más detallado;
  • [off]: sin registros;
  • [info]: el nivel normal de registros;
  • [warn]: igual que el nivel [info], pero con los mensajes de advertencia (warning) añadidos. Estos mensajes indican la posibilidad de que se produzca un error;

Cambie al modo [debug] tan pronto como aparezcan errores durante la ejecución.

7.8. Implementación de la capa [DAO]

  

La interfaz [IDao] de la capa [DAO] es la siguiente:


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 {
    // Configuración de las elecciones
    public ElectionsConfig getElectionsConfig();

    // las listas que se presentan a las elecciones
    public ListeElectorale[] getListesElectorales();

    // guardado de los resultados de las elecciones
    public void setListesElectorales(ListeElectorale[] listesElectorales);

}

El esqueleto de la clase [ElectionsDaoJdbc] que implementa la capa [dao] con una base de datos será el siguiente:


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")
    // obtención de la configuración de la elección
    public ElectionsConfig getElectionsConfig() {
        throw new RuntimeException("[getElectionsConfig] not yet implemented");
    }

    // obtención de las listas
    public ListeElectorale[] getListesElectorales() {
        throw new RuntimeException("[getListesElectorales] not yet implemented");
    }

    // modificación de las listas [voix, sieges, elimine]
    public void setListesElectorales(ListeElectorale[] listesElectorales) {
        throw new RuntimeException("[setListesElectorales] not yet implemented");
    }

    // -------------------- métodos privados

    // gestión del «finally»
    private ElectionsException doFinally(int code, ResultSet rs, PreparedStatement ps, Connection connexion) {
        // cierre ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

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

            }
        }
        // cerrar la conexión
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // se devuelve un [ElectionsException]
                return new ElectionsException(code, e3);
            }
        }
        // sin excepciones
        return null;
    }

    // gestión de la captura
    private ElectionsException doCatchException(int code1, int code2, Connection connexion, Throwable th) {
        // se genera un [ElectionsException]
        ElectionsException ex1 = new ElectionsException(code1, th);
        // anulación de la transacción
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
        }
        // se devuelve la excepción
        return ex1;
    }
}
  • línea 24: la anotación [@Cacheable] es una anotación de Spring que solicita almacenar en memoria («almacenar en caché») los resultados del método [getElectionsConfig]. Es posible hacerlo aquí porque el contenido de la tabla [CONF] nunca cambia. No sería posible aplicar esta anotación al método [getListesElectorales], ya que el contenido de la tabla [LISTES] cambia con el tiempo;
  • los métodos [doCatchException] y [doFinally] devuelven un tipo [ElectionsException]. El método [doFinally] devuelve un puntero null si la liberación de recursos se ha realizado sin errores;

Tarea: escribe la clase [ElectionDaoJdbc] inspirándote en la clase [IntroJdbc02] estudiada en el apartado 6.5.2.


7.9. La clase de prueba [Main]

  

La clase [Main] es la siguiente:


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 {

    // fuente de datos
    private static IElectionsDao dao;

    public static void main(String[] args) {
        // se recupera la referencia de la capa [DAO] tras instanciar el contexto de Spring
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Recuperación de la fuente de datos
            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;
        }
        // lectura de la tabla BD
        ElectionsConfig electionsConfig = null;
        ListeElectorale[] listes;
        try {
            // contenido de las dos tablas
            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;
        }
        // todo ha ido bien - visualización
        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étodos privados ------------------
    private static List<String> getErreursForThrowable(Throwable th) {
        // se recupera la lista de mensajes de error de la excepción
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // se recupera el mensaje solo si !=null y no está vacío
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa siguiente
            cause = cause.getCause();
        }
        return erreurs;
    }

}
  • líneas 21-31: recuperación de una referencia en la capa [DAO];
  • línea 21: se utiliza una sintaxis denominada try_with_resources. Su sintaxis es la siguiente:

try (T ressource=expression) {
            // uso excesivo de recursos
...
}
  • (continuación)
    • el tipo T de la línea 1 debe implementar la interfaz [java.lang.AutoCloseable];
    • al salir del bloque de las líneas 1-4, el recurso de tipo [java.lang.AutoCloseable] se libera automáticamente, haya habido o no una excepción. Quienes conozcan el lenguaje C# reconocerán aquí un equivalente sintáctico y funcional de la cláusula «using (T recurso = expresión)»;
  • líneas 40-49: uso de la capa [DAO] para obtener el contenido de las tablas [CONF] y [LISTES];
  • líneas 41-47: se comprueba la caché [electionsconfig]. Esta se ha definido en dos lugares:
    • en la clase [ElectionsDaoJdbc]:

  @Cacheable("electionsConfig")
  public ElectionsConfig getElectionsConfig() {
  • (continuación)
    • en la clase de configuración [AppConfig]:

@EnableCaching
public class AppConfig {
...
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("electionsConfig");
  }
}
  • línea 59: cierre del contexto Spring;
  • líneas 62-67: visualización de la información obtenida;

Los resultados obtenidos con una capa [DAO] implementada son los siguientes:

...
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}
  • líneas 2-4: muestran la influencia de la caché:
    • la consulta 1 dura 80 milisegundos;
    • la consulta 2 tarda 1 milisegundo;

Cuando la base de datos está desconectada, se obtienen los siguientes resultados:

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. Pruebas JUnit de la clase [ElectionsDaoJdbc]

  

La clase [Test01] es la siguiente clase de prueba de [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 {

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

    @Before
    public void init() {
        // se limpia la tabla [LISTES]
        // listas en liza
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // se ponen a 0 los votos y los escaños y se eliminan a «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);
        }
        // se hacen persistentes estos datos mediante la capa [dao]
        electionsDao.setListesElectorales(listes);
    }

    @Test
    public void testElections01() {
        ...
    }
}
  • línea 17: la anotación [RunWith], que es una anotación [JUnit] (línea 6), garantiza la integración con Spring a través de la clase [SpringJUnit4ClassRunner];
  • línea 16: la anotación [SpringApplicationConfiguration] es una anotación [Spring] (línea 8) que permite designar las clases de configuración de la prueba JUnit. Aquí se designa la clase [AppConfig] utilizada para configurar el proyecto. De este modo, se dispone de todos los objetos Spring definidos por esta clase de configuración. Así es como se puede inyectar, en las líneas 21-22, una referencia a la capa [DAO] que se va a someter a prueba;
  • línea 25: la anotación [Before] designa un método que debe ejecutarse antes de cada prueba;
  • líneas 26-41: el método [init] pone a cero los votos y escaños de las listas de la tabla [LISTES] y establece el valor booleano [elimine] en [false];

El único método de prueba es el siguiente:


@Test
    public void testElections01() {
        System.out.println("testElections01-------------------------------------");
        // recuperación de la configuración de las elecciones
        ElectionsConfig electionsConfig = electionsDao.getElectionsConfig();
        int nbSiegesAPourvoir = electionsConfig.getNbSiegesAPourvoir();
        double seuilElectoral = electionsConfig.getSeuilElectoral();
        Assert.assertEquals(6, nbSiegesAPourvoir);
        Assert.assertEquals(0.05, seuilElectoral, 1E-6);

        // listas en liza
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // Visualización de los valores leídos
        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]);
        }

        // se asignan votos y escaños a las listas
        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;
        }

        // se guardan estos datos de forma permanente mediante la capa [dao]
        electionsDao.setListesElectorales(listes);

        // se vuelven a leer los datos
        ListeElectorale[] listesElectorales2 = electionsDao.getListesElectorales();
        // se comprueban los datos leídos
        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]);
        }
    }
  • líneas 5-9: se comprueba que se puede recuperar el contenido de la tabla [CONF];
  • líneas 11-19: se muestra el contenido de la tabla [LISTES]. Aquí no hay pruebas, solo una comprobación visual;
  • líneas 21-35: se modifica en la base de datos la tabla [LISTES] asignando a las listas los valores correspondientes a sus campos [voix, sieges, elimine];
  • líneas 37-51: se vuelve a leer la tabla [LISTES] y se comprueba que el resultado obtenido coincide con lo que se ha introducido;
  • líneas 52-55: comprobación visual;

Los resultados de la consola obtenidos con una capa [DAO] implementada son los siguientes:

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}
  • líneas 1-23: registros de Spring Test;
  • líneas 25-26: el contenido de la tabla [CONF];
  • líneas 27-34: el contenido inicial de la tabla [LISTES];
  • líneas 35-42: el contenido de la tabla [LISTES] tras asignar valores a los campos de [voix, sieges, elimine];

Por otra parte, la prueba JUnit se ha superado:

 

7.11. Creación del archivo [with-dependencies] a partir de la capa [dao]

El proyecto final tiene la siguiente arquitectura:

Recordemos la configuración de Maven del proyecto:


<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>
        <!-- biblioteca 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>
        <!-- Prueba de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Registro de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- complementos -->
    <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>
            <!-- para instalar el artefacto del proyecto en el repositorio local de Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  

Vamos a generar un único archivo JAR que contendrá las clases de todos los archivos JAR anteriores, además de las del proyecto de la capa [DAO]. Para ello, hay que realizar una modificación en el archivo [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>
  • líneas 25-37: configuran un plugin de Maven para generar el archivo JAR;
  • línea 15: indica la versión de Java que se debe utilizar para la compilación;

Una vez realizada esta modificación, se puede generar el archivo jar de la siguiente manera [1-10]:

  • en [5], seleccionar la carpeta del proyecto utilizando el botón [6];
  • en [7], asigna un nombre a la configuración de ejecución;
  • en [8], introduce la lista de objetivos de Maven que se van a ejecutar:
    • [clean]: vacía la carpeta [target] del proyecto en la que se colocará el archivo JAR generado;
    • [compile]: compila el proyecto;
    • [assembly:single]: genera un único archivo JAR que incluye todas las clases del proyecto y sus dependencias;
  • en [9-10], comprueba que tienes un JDK (Java Development Kit) y no un JRE (Java Runtime Environment). La diferencia es que el JDK incluye un compilador, mientras que el JRE no lo incluye. Si no dispone de un JDK, deberá añadir uno en [10] junto con [11]. Para ello, siga el procedimiento descrito en el apartado 3.1;
  • en [17], ejecute la configuración de ejecución;
  • en [13], el archivo comprimido generado;

Este archivo comprimido se puede abrir con un programa de descompresión:

 

7.12. Prueba del archivo de la capa [DAO]

Creemos un proyecto estándar de Eclipse (no Maven) [1]:

Copiamos y pegamos el paquete [dao.console] del proyecto [elections-dao-jdbc-01] en el proyecto [elections-dao-jdbc-02] [2]. Aparecen numerosos errores porque la clase [Main] hace referencia a clases que no se encuentran en su [Classpath]. Vamos a modificar este último.

En primer lugar, creamos una carpeta [lib] en el nuevo proyecto:

En [9], colocamos el archivo creado en el paso anterior en la carpeta [lib] y, a continuación, modificamos la ruta de compilación del proyecto:

  • por [18]; hemos importado el archivo de la capa [DAO] creado anteriormente;
  • en [19], el proyecto ya no presenta errores;

A continuación, se puede ejecutar la clase [Main]. Se obtienen los mismos resultados que anteriormente.