Skip to content

7. [TD]: Implementação da camada [DAO] do TD com o API JDBC

Palavras-chave: bases de dados relacionais, API, JDBC, SQLException.

Voltemos à arquitetura em camadas da nossa aplicação:

Os dados necessários para a eleição são armazenados numa base de dados MySQL [dbelections]

7.1. Support

A pasta [support / chap-07] [1] contém:

  • os projetos Eclipse deste capítulo [2];
  • o script SQL para a criação da base de dados MySQL, [dbelections] e [3];

7.2. A base de dados [dbelections]


Tarefa a realizar: crie a base de dados MySQL e [dblelections] seguindo o procedimento descrito no parágrafo 6.4.2.


A base de dados [dbelections] é uma base de dados MySQL com duas tabelas:

  

A tabela [conf] contém as informações de configuração da eleição:

 
  • [id]: chave primária autoincrementada;
  • [version]: número de versão do registo — pode ser ignorado aqui;
  • [sap]: número de lugares a preencher;
  • [seuilelectoral]: o limite abaixo do qual uma lista é eliminada;

O seu conteúdo é o seguinte:

 

A tabela [listes] contém as listas de candidatos à eleição:

 
  • [id]: chave primária autoincrementada;
  • [version]: número de versão do registo — pode ser ignorado aqui;
  • [nom]: nome da lista;
  • [voix]: voz da lista — só é conhecida após a introdução pelo utilizador na camada [présentation];
  • [sieges]: número de lugares obtidos — só é conhecido após o cálculo da camada [métier];
  • [elimine]: igual a 1 se a lista for eliminada, igual a 0 caso contrário — só é conhecido após o cálculo da camada [métier];

O conteúdo da tabela [listes] é o seguinte:

 

O script SQL para gerar a base de dados [dbelections] chama-se [dbelections.sql] e encontra-se no servidor. O seu código é o seguinte:


-- phpMyAdmin SQL Dump
-- versão 4.0.4
-- http://www.phpmyadmin.net
--
-- Cliente: localhost
-- Gerado em: Quarta-feira, 11 de março de 2015, às 12:20
-- Versão do servidor: 5.6.12-log
-- Versão do 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 dados: `dbelections`
--
CREATE DATABASE IF NOT EXISTS `dbelections` DEFAULT CHARACTER SET utf8 COLLATE utf8_swedish_ci;
USE `dbelections`;

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

--
-- Estrutura da tabela `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 ;

--
-- Conteúdo da tabela `conf`
--

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

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

--
-- Estrutura da tabela `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 ;

--
-- Conteúdo da tabela `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. O projeto Eclipse

O projeto Eclipse da camada [DAO] será o seguinte:

  
  • o pacote [elections.dao.entities] contém os objetos manipulados pela camada [DAO];
  • o pacote [elections.dao.service] contém a implementação da camada [DAO];
  • o pacote [elections.dao.config] contém a configuração da camada [DAO]
  • o pacote [elections.dao.junit] contém uma classe de teste JUnit do projeto;
  • o pacote [elections.dao.console] contém uma classe de teste executável;

7.4. Configuração do projeto Maven

 

O ficheiro [pom.xml] que configura o projeto Maven é o seguinte:


<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>
        <!-- Teste do Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Registo no 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 a instalação do artefacto do projeto no repositório local do Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>

Este ficheiro é semelhante ao descrito no parágrafo 6.5.1. Foram-lhe introduzidas as seguintes alterações:

  • linhas 8-12: foi definido um projeto Maven pai. O projeto [spring-boot-starter-parent] (linha 10) define um número muito elevado de dependências com as respetivas versões. Quando se utiliza uma delas (linhas 19-57), não é necessário especificar a versão, uma vez que esta está definida no projeto Maven pai;
  • linhas 40-51: dependências necessárias para a classe de teste do projeto. Estas dependências têm o atributo [<scope>test</scope>], o que significa que só são necessárias para as classes da pasta [src / test / java]. Estas dependências não serão incluídas no arquivo final do projeto;
  • linhas 53-56: a biblioteca [spring-boot-starter-logging] será utilizada pelo Spring para gerar registos no ecrã;
  • linhas 14-17: propriedades de configuração do Maven:
    • linha 15: indica que os ficheiros de código-fonte estão escritos em UTF-8;
    • linha 16: indica que o compilador a utilizar deve ser da versão 1.8;

7.5. As entidades da camada [DAO]

  
  • [ElectionsConfig] é o modelo de objeto associado a uma linha da tabela [CONF];
  • [ListeElectorale] é o modelo de objeto associado a uma linha da tabela [LISTES];
  • [AbstactEntity] é a classe pai das duas classes anteriores. Esta classe agrupa os campos [id, version] comuns às duas classes;
  • [ElectionsException] é uma classe de exceção;

7.5.1. A classe [ElectionsException]

  

A classe [ElectionsException] foi descrita no parágrafo 4.3. Reproduzimos aqui o seu código:


package ...;

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

// classe de exceção para a aplicação «Eleições»
// a exceção é não controlada

public class ElectionsException extends RuntimeException implements Serializable {

    // série ID
    private static final long serialVersionUID = 1L;

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

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

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

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

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

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

    // lista de mensagens de erro de uma exceção
    private List<String> getErreursForException(Throwable th) {
        // recupera-se a lista de mensagens de erro da exceção
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // recupera-se a mensagem apenas se esta for !=null e não estiver em branco
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa seguinte
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getters e setters
...
}

Um objeto [ElectionsException] é caracterizado por duas informações:

  • linha 16: um código de erro;
  • linha 17: uma lista de mensagens de erro associadas à pilha de exceções que ocorreram;
  • a classe tem 5 construtores (linhas 20, 24, 32, 40, 50);
  • linhas 59-76: o método [getErreursForException] permite recuperar as mensagens de erro da pilha de exceções;

7.5.2. A classe [AbstractEntity]

  

A classe [AbstractEntity] é a seguinte:


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;

    // construtores
    public AbstractEntity() {

    }

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

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

Esta classe armazena os campos [ID, NOM] das linhas das tabelas [CONF] e [LISTES] (linhas 8-9).

  • linha 8: a classe tem o atributo [Abstract], indicando que não pode ser instanciada. Só pode ser derivada;
  • linhas 26-33: assinatura jSON do objeto;
  • linha 28: é devolvida a cadeia jSON de [this]. Se, no momento da execução, [this] representar um objeto derivado de [AbstactEntity], é a cadeia jSON do objeto derivado que é obtida. Assim, as classes derivadas não precisarão de definir um método [toString]. O da classe pai é suficiente;

7.5.3. A classe [ElectionsConfig]

  

A classe [ElectionsConfig] é a seguinte:


package elections.dao.entities;


public class ElectionsConfig extends AbstractEntity {

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

    // construtores
    public ElectionsConfig() {
    }

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

    // getters e setters
...
}
  • linha 4: a classe estende a classe [AbstractEntity];
  • linhas 8-9: armazenam as informações das colunas [SAP, SEUILELECTORAL] da tabela [CONF];

7.5.4. A classe [ListeElectorale]

  

A classe [ListeElectorale] é a seguinte:


package elections.dao.entities;

public class ListeElectorale extends AbstractEntity {

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

    // construtores
    public ListeElectorale() {
    }

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

    public ListeElectorale(Long id, Long version, String nom, int voix, int sieges, boolean elimine) {
        // pai
        super(id, version);
        // campos locais
        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 e 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;
    }

}
  • linha 3: a classe estende a classe [AbstractEntity];
  • linhas 6-9: a classe armazena as colunas [NOM, VOIX, SIEGES, ELIMINE] da tabela [LISTES];

7.6. Configuração Spring da camada [DAO]

 

A classe [AppConfig] é uma classe de configuração Spring que configura o acesso à base de dados da seguinte forma:


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() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração de acesso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // uma ligação inicialmente aberta
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("electionsConfig");
    }
}
  • linhas 25-38: o acesso à BD será feito através de uma fonte de dados [tomcat-jdbc]. Este tipo de fonte foi utilizado e explicado no parágrafo 6.5;
  • linhas 17-23: um conjunto de constantes estáticas acessíveis por todas as classes do projeto;
  • linha 13: a anotação [@Configuration] transforma a classe [AppConfig] numa classe de configuração Spring;
  • linha 11: a anotação [@ComponentScan] indica os pacotes onde se podem encontrar objetos Spring. Aqui, vamos definir um objeto Spring no pacote [dao]. A anotação [@ComponentScan] transforma a classe numa classe de configuração, o que nos poupa de ter de colocar a anotação [@Configuration];
  • a linha 12 ativa a gestão de um cache. O princípio é o seguinte:
    • colocamos a anotação [@Cacheable('nom_du_cache')] num método M. O «nome_do-cache» é o nome utilizado na linha 41;
    • quando o método M é chamado pela primeira vez, os seus resultados são devolvidos e também colocados no cache indicado pela anotação;
    • quando o método M é chamado nas vezes seguintes com os mesmos parâmetros da primeira vez, não é executado e o Spring limita-se a devolver os valores armazenados na cache;

7.7. Configuração dos registos

As bibliotecas de registos são definidas pela seguinte dependência em [pom.xml]:


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

Esta dependência inclui as seguintes bibliotecas:

  

É a biblioteca [logback] que irá gerir os registos. Está configurada por dois ficheiros:

  • [logback.xml] para o ramo principal do código;
  • [logback-test.xml] para o ramo de teste do código. Na ausência deste ficheiro, é utilizado o ficheiro anterior;

Estes dois ficheiros devem estar localizados no diretório [Classpath] do projeto. Por esse motivo, estão colocados na pasta:

  • [src / main / ressources] para o ramo principal do código;
  • [src / test / ressources] para o ramo de teste do código;

O conteúdo dos ficheiros é aqui o mesmo:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- por predefinição, é atribuído o tipo aos codificadores
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- controlo do nível dos registos -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Tudo se passa na linha 12, onde se define o nível de informação pretendido:

  • [debug]: o nível mais detalhado;
  • [off]: sem registos;
  • [info]: o nível normal de registos;
  • [warn]: igual ao nível [info], mas com as mensagens de aviso (warning). Estas mensagens indicam a possibilidade de um erro;

Mude para o modo [debug] assim que surgirem erros durante a execução.

7.8. Implementação da camada [DAO]

  

A interface [IDao] da camada [DAO] é a seguinte:


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 {
    // configuração das eleições
    public ElectionsConfig getElectionsConfig();

    // as listas concorrentes
    public ListeElectorale[] getListesElectorales();

    // gravação dos resultados da eleição
    public void setListesElectorales(ListeElectorale[] listesElectorales);

}

O esqueleto da classe [ElectionsDaoJdbc] que implementa a camada [dao] com uma base de dados será o seguinte:


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")
    // obtenção da configuração da eleição
    public ElectionsConfig getElectionsConfig() {
        throw new RuntimeException("[getElectionsConfig] not yet implemented");
    }

    // obtenção das listas
    public ListeElectorale[] getListesElectorales() {
        throw new RuntimeException("[getListesElectorales] not yet implemented");
    }

    // alteração das listas [voix, sieges, elimine]
    public void setListesElectorales(ListeElectorale[] listesElectorales) {
        throw new RuntimeException("[setListesElectorales] not yet implemented");
    }

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

    // gestão do finally
    private ElectionsException doFinally(int code, ResultSet rs, PreparedStatement ps, Connection connexion) {
        // encerramento ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

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

            }
        }
        // encerrar a ligação
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // é devolvido um [ElectionsException]
                return new ElectionsException(code, e3);
            }
        }
        // sem exceção
        return null;
    }

    // gestão do catch
    private ElectionsException doCatchException(int code1, int code2, Connection connexion, Throwable th) {
        // é gerado um [ElectionsException]
        ElectionsException ex1 = new ElectionsException(code1, th);
        // anulação da transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
        }
        // retorna a exceção
        return ex1;
    }
}
  • linha 24: a anotação [@Cacheable] é uma anotação Spring que solicita que os resultados do método [getElectionsConfig] sejam armazenados em memória («armazenados em cache»). É possível fazer isso aqui porque o conteúdo da tabela [CONF] nunca muda. Não seria possível colocar esta anotação no método [getListesElectorales], uma vez que o conteúdo da tabela [LISTES] muda ao longo do tempo;
  • os métodos [doCatchException] e [doFinally] devolvem um tipo [ElectionsException]. O método [doFinally] devolve um ponteiro null se a libertação dos recursos tiver ocorrido sem erros;

Tarefa: escreva a classe [ElectionDaoJdbc] inspirando-se na classe [IntroJdbc02] estudada no parágrafo 6.5.2.


7.9. A classe de teste [Main]

  

A classe [Main] é a seguinte:


package elections.dao.console;

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

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class Main {

    // fonte de dados
    private static IElectionsDao dao;

    public static void main(String[] args) {
        // recupera-se a referência da camada [DAO] após instanciar o contexto Spring
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // recuperação da fonte de dados
            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);
            }
            // fim
            return;
        }
        // leitura da BD
        ElectionsConfig electionsConfig = null;
        ListeElectorale[] listes;
        try {
            // conteúdo das duas tabelas
            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);
            }
            // fim
            return;
        }
        // tudo correu bem - exibição
        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) {
        // recuperamos a lista de mensagens de erro da exceção
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // recuperamos a mensagem apenas se ela for !=null e não estiver vazia
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa seguinte
            cause = cause.getCause();
        }
        return erreurs;
    }

}
  • linhas 21-31: recuperação de uma referência na camada [DAO];
  • linha 21: utiliza-se uma sintaxe denominada try_with_resources. A sua sintaxe é a seguinte:

try (T ressource=expression) {
            // exploração de recursos
...
}
  • (continuação)
    • o tipo T da linha 1 deve implementar a interface [java.lang.AutoCloseable];
    • ao sair do bloco das linhas 1-4, o recurso do tipo [java.lang.AutoCloseable] é automaticamente libertado, quer tenha ocorrido uma exceção ou não. Quem conhece a linguagem C# reconhecerá aqui um equivalente sintático e funcional da cláusula using (T recurso=expressão);
  • linhas 40-49: utilização da camada [DAO] para obter o conteúdo das tabelas [CONF] e [LISTES];
  • linhas 41-47: testa-se a cache [electionsconfig]. Esta foi definida em dois locais:
    • na classe [ElectionsDaoJdbc]:

  @Cacheable("electionsConfig")
  public ElectionsConfig getElectionsConfig() {
  • (continuação)
    • na classe de configuração [AppConfig]:

@EnableCaching
public class AppConfig {
...
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("electionsConfig");
  }
}
  • linha 59: encerramento do contexto Spring;
  • linhas 62-67: exibição das informações obtidas;

Os resultados obtidos com uma camada [DAO] implementada são os seguintes:

...
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}
  • linhas 2-4: mostram a influência da cache:
    • a consulta 1 demora 80 milissegundos;
    • a consulta 2 demora 1 milissegundo;

Quando a base de dados está desligada, obtêm-se os seguintes 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. Testes JUnit da classe [ElectionsDaoJdbc]

  

A classe [Test01] é a classe de teste seguinte à [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 {

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

    @Before
    public void init() {
        // limpa-se a tabela [LISTES]
        // listas concorrentes
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // zera-se o número de votos e de lugares e define-se como «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);
        }
        // tornamos estes dados persistentes através da camada [dao]
        electionsDao.setListesElectorales(listes);
    }

    @Test
    public void testElections01() {
        ...
    }
}
  • linha 17: a anotação [RunWith], que é uma anotação [JUnit] (linha 6), assegura a integração com o Spring através da classe [SpringJUnit4ClassRunner];
  • linha 16: a anotação [SpringApplicationConfiguration] é uma anotação [Spring] (linha 8) que permite designar as classes de configuração do teste JUnit. Aqui, designa-se a classe [AppConfig] utilizada para configurar o projeto. Dispõe-se, assim, de todos os objetos Spring definidos por esta classe de configuração. É assim que se pode injetar, nas linhas 21-22, uma referência à camada [DAO] que vai ser testada;
  • linha 25: a anotação [Before] indica um método que deve ser executado antes de cada teste;
  • linhas 26-41: o método [init] define a zero os votos e os lugares das listas da tabela [LISTES] e o valor booleano [elimine] como [false];

O único método de teste é o seguinte:


@Test
    public void testElections01() {
        System.out.println("testElections01-------------------------------------");
        // recuperação da configuração das eleições
        ElectionsConfig electionsConfig = electionsDao.getElectionsConfig();
        int nbSiegesAPourvoir = electionsConfig.getNbSiegesAPourvoir();
        double seuilElectoral = electionsConfig.getSeuilElectoral();
        Assert.assertEquals(6, nbSiegesAPourvoir);
        Assert.assertEquals(0.05, seuilElectoral, 1E-6);

        // listas concorrentes
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // exibição dos valores lidos
        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]);
        }

        // atribuição de votos e lugares às 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;
        }

        // permanência destes dados através da camada [dao]
        electionsDao.setListesElectorales(listes);

        // releitura dos dados
        ListeElectorale[] listesElectorales2 = electionsDao.getListesElectorales();
        // verifica-se os dados lidos
        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]);
        }
    }
  • linhas 5-9: verifica-se se é possível recuperar o conteúdo da tabela [CONF];
  • linhas 11-19: exibe-se o conteúdo da tabela [LISTES]. Aqui não há testes, apenas uma verificação visual;
  • linhas 21-35: altera-se, na base de dados, a tabela [LISTES], atribuindo às listas os valores para os seus campos [voix, sieges, elimine];
  • linhas 37-51: volta-se a ler a tabela [LISTES] e verifica-se se o resultado obtido corresponde efetivamente ao que foi introduzido;
  • linhas 52-55: verificação visual;

Os resultados na consola obtidos com uma camada [DAO] implementada são os seguintes:

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}
  • linhas 1-23: registos do Spring Test;
  • linhas 25-26: o conteúdo da tabela [CONF];
  • linhas 27-34: o conteúdo inicial da tabela [LISTES];
  • linhas 35-42: o conteúdo da tabela [LISTES] após a atribuição de valores aos campos [voix, sieges, elimine];

Além disso, o teste JUnit é bem-sucedido:

 

7.11. Criação do arquivo [with-dependencies] a partir da camada [dao]

O projeto final tem a seguinte arquitetura:

Recorde-se a configuração Maven do projeto:


<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>
        <!-- Teste do Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Registo no 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 a instalação do artefacto do projeto no repositório local do Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  

Vamos gerar um único ficheiro .jar que conterá as classes de todos os ficheiros .jar acima referidos, além das do projeto da camada [DAO]. Isto é feito através de uma alteração no ficheiro [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>
  • linhas 25-37: configuram um plugin do Maven para gerar o ficheiro JAR;
  • linha 15: indica a versão do Java a utilizar para a compilação;

Depois de efetuada esta alteração, é possível gerar o ficheiro jar da seguinte forma [1-10]:

  • em [5], selecione a pasta do projeto utilizando o botão [6];
  • em [7], atribuir um nome à configuração de execução;
  • em [8], introduza a lista de tarefas Maven a executar:
    • [clean]: esvazia a pasta [target] do projeto, na qual será colocado o ficheiro JAR gerado;
    • [compile]: compila o projeto;
    • [assembly:single]: gera um único ficheiro JAR que inclui todas as classes do projeto e das suas dependências;
  • no [9-10], verifique se tem um JDK (Java Development Kit) e não um JRE (Java Runtime Environment). A diferença é que o JDK inclui um compilador, ao passo que o JRE não o inclui. Se não tiver o JDK, terá de adicionar um ao [10] com o [11]. Para tal, siga o procedimento descrito no parágrafo 3.1;
  • no [17], execute a configuração de execução;
  • em [13], o arquivo gerado;

É possível abrir este arquivo com um programa de descompressão:

 

7.12. Teste do arquivo da camada [DAO]

Vamos criar um projeto Eclipse padrão (não Maven) [1]:

Vamos copiar e colar o pacote [dao.console] do projeto [elections-dao-jdbc-01] para o projeto [elections-dao-jdbc-02] [2]. Surgem vários erros porque a classe [Main] faz referência a classes que não se encontram no seu [Classpath]. Vamos modificar este último.

Primeiro, criamos a pasta [lib] no novo projeto:

No [9], colocamos o arquivo criado na etapa anterior na pasta [lib] e, em seguida, alteramos o Build Path do projeto:

  • para [18]; importámos o arquivo da camada [DAO] criado anteriormente;
  • em [19], o projeto já não apresenta erros;

É então possível executar a classe [Main]. Obtêm-se os mesmos resultados que anteriormente.