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 en capas de nuestra aplicación:

Los datos necesarios para la elección 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 párrafo 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 autoincremental;
  • [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 elimina una lista;

Su contenido es el siguiente:

 

La tabla [listes] contiene las listas de candidatos de la elección:

 
  • [id]: clave primaria autoincremental;
  • [version]: número de versión del registro; aquí se puede omitir;
  • [nom]: nombre de la lista;
  • [voix]: opción de la lista —solo se conoce después de que el usuario la haya ingresado en la capa [présentation];
  • [sieges]: número de escaños obtenidos —solo se conoce después de calcular 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 `listas`
--

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 `listas`
--

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>

    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- 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>

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

  • líneas 8-12: se ha definido un proyecto Maven padre. El proyecto [spring-boot-starter-parent] (línea 10) define un gran número de dependencias con sus versiones. Cuando se utiliza una de ellas (líneas 19-57), no es necesario especificar la versión, ya que esta está definida en el proyecto Maven padre;
  • 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 se debe utilizar el compilador 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 objeto asociado a una fila de la tabla [LISTES];
  • [AbstactEntity] es la clase padre de las dos clases anteriores. Agrupa los campos [id, version] que son comunes a ambas clases;
  • [ElectionsException] es una clase de excepción;

7.5.1. La clase [ElectionsException]

  

La clase [ElectionsException] se describió en el párrafo 4.3. A continuación, volvemos a presentar su código:


package ...;

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

// clase de excepción para la aplicación Elecciones
// 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 no es nulo y no está en blanco
            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 filas de las tablas [CONF] y [LISTES] (filas 8-9).

  • línea 8: la clase tiene el atributo [Abstract], lo 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 esta manera, las clases derivadas no necesitarán definir un método [toString]. El de la clase padre es suficiente;

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 extiende 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 extiende 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 para 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 utilizó y explicó en el párrafo 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 a la clase [AppConfig] en una clase de configuración de Spring;
  • línea 11: la anotación [@ComponentScan] indica los paquetes donde se pueden encontrar objetos de Spring. Aquí, vamos a definir un objeto de Spring en el paquete [dao]. La anotación [@ComponentScan] convierte a 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 el método M se invoca las siguientes veces con los mismos parámetros que la primera vez, no se ejecuta y Spring simplemente devuelve 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 prueba del código. Si no existe este archivo, se utiliza el anterior;

Estos dos archivos deben encontrarse en el directorio [Classpath] del proyecto. Por esta razón, se colocan en la carpeta:

  • [src / main / ressources] para la rama principal del código;
  • [src / test / ressources] para la rama de prueba 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 ocurre 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 mensajes de advertencia (warning). Estos mensajes indican la posibilidad de 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 en contienda
    public ListeElectorale[] getListesElectorales();

    // almacenamiento de los resultados de la elección
    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 excepción
        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 hacer esto 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 realizó sin errores;

Tarea: escriba la clase [ElectionDaoJdbc] inspirándose en la clase [IntroJdbc02] estudiada en el párrafo 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] después de 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 salió 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 llamada 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 a 4, el recurso de tipo [java.lang.AutoCloseable] se libera automáticamente, haya habido una excepción o no. 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 de 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 dura 1 milisegundo;

Cuando la base de datos está desactivada, 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 competencia
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // se ponen a 0 los votos y los escaños y se elimina 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 esta manera, se dispone de todos los objetos de 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 probar;
  • 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 contienda
        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 verifican 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 verifica que se pueda 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 verificació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 verifica que lo obtenido sea igual a lo que se ingresó;
  • líneas 52-55: verificació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] después de asignar valores a los campos de la tabla [voix, sieges, elimine];

Por otra parte, la prueba JUnit se realiza con éxito:

 

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>

    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- 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]. Esto se logra mediante 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 complemento de Maven para generar el archivo JAR;
  • línea 15: indica la versión de Java que se debe usar para la compilación;

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

  • en [5], selecciona la carpeta del proyecto utilizando el botón [6];
  • en [7], asigna un nombre a la configuración de ejecución;
  • en [8], ingrese la lista de objetivos de Maven que se deben 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], asegúrate de que tengas 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 tiene. Si no tienes un JDK, debes agregar uno en [10] junto con [11]. Para ello, sigue el procedimiento descrito en el párrafo 3.1;
  • en [17], ejecute la configuración de ejecución;
  • en [13], el archivo comprimido generado;

Este archivo 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]:

Copiemos y peguemos 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.

Primero creamos una carpeta [lib] en el nuevo proyecto:

En [9], colocamos el archivo creado en el paso anterior en la carpeta [lib] y luego 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;

Ahora se puede ejecutar la clase [Main]. Se obtienen los mismos resultados que anteriormente.