Skip to content

3. Introdução ao API JDBC

3.1. Configuração do ambiente de trabalho

Vamos trabalhar com um banco de dados MySQL5.

Você deve ter:

  • ter instalado o JDK (Java Development Kit) (parágrafo 23.1);
  • ter instalado o gerenciador de dependências Maven (parágrafo 23.2);
  • ter instalado o IDE Spring Tool Suite (STS) (parágrafo 23.3);
  • instalou o SGBD MySQL5 (parágrafo 23.4) e seu cliente EMS MyManager (parágrafo 23.5);
  • baixou os códigos do documento [http://tahe.developpez.com/java/spring-database];

Supõe-se, a seguir, que o administrador do MySQL5 seja o usuário root, com a senha root. Inicie o SGBD, o MySQL5 e seu cliente [MyManager]. Com o [MyManager], criamos o banco de dados [dbproduits] [1-34]:

  • em [3], a base de dados deve se chamar [dbproduits];
  • em [8-9], como root com a senha root (o que não aparece na captura de tela acima);
  • em [14a], a senha é novamente root (o que não aparece na captura de tela);
  • em [15], foi criado o banco de dados [dbproduits];
  • em [20], preste atenção ao banco de dados selecionado. Deve ser o banco de dados [dbproduits];
  • em [22], a pasta é <exemplos>/spring-database-config/mysql/databases, onde <exemplos> é a pasta dos exemplos baixados;
  • em [23], selecione o script SQL [dbproduits.sql]. Ele irá gerar a tabela [PRODUITS] no banco de dados [dbproduits];
  • em [30], a tabela [produits] foi criada;
  • em [33], as colunas da tabela [produits];
  • em [34], que está inicialmente vazia;

Agora, com STS, importe os seguintes projetos (siga o procedimento utilizado para os projetos da pasta <exemplos>/spring-core):

  • em [2], o projeto [mysql-config-jdbc] será encontrado na pasta [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Este projeto configura a camada JDBC da arquitetura abaixo:

Em seguida, importe novamente os três projetos a seguir:

  • no [2]; os projetos estarão localizados na pasta [<exemples>/spring-database-config/spring-jdbc] [1];

Esses três projetos são projetos Maven que utilizam o projeto Maven [mysql-config-jdbc]. Este último projeto gera o seguinte artefato Maven (ver pom.xml):


    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>

O mesmo artefato será gerado pelo projeto [oracle-config-jdbc, db2-config-jdbc, ...]. Para garantir que os projetos [spring-generic-jdbc-*] atualmente carregados no STS estejam de fato utilizando o projeto [mysql-config-jdbc]:

  • verifique se outro projeto [sgbd-config-jdbc] não está carregado ao mesmo tempo. Isso poderia causar erros difíceis de entender;
  • atualize a configuração do Maven dos projetos carregados da seguinte maneira:

Para verificar sua configuração, execute a configuração de execução [spring-jdbc-generic-01.IntroJdbc01] [1-3]:

Você deve obter os seguintes resultados no console:

------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.00000000000001,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.10000000000001,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.2,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.30000000000001,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé

Nos exemplos a seguir, o leitor poderá:

  • trabalhar diretamente com os projetos carregados anteriormente;
  • ou criar os próprios projetos;

3.2. Etapas de operação de um banco de dados

Na arquitetura acima, a exploração de um banco de dados pelo programa de console envolve as seguintes etapas:

  1. carregamento do driver JDBC do banco de dados;
  1. abertura de uma conexão com o banco de dados;
  2. execução de um comando SQL no banco de dados e processamento dos resultados do comando SQL;
  3. fechamento da conexão;

A etapa 1 é executada apenas uma vez. As etapas 2 a 4 são repetidas. Observe que não se deixa uma conexão aberta. Ela é fechada assim que não for mais necessária.

3.2.1. etapa 1 — carregamento na memória do driver JDBC

O código


        // carregando o driver JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // tratar a exceção
}

A operação da linha 3 tem como objetivo carregar na memória o driver JDBC do banco de dados. Essa operação precisa ser realizada apenas uma vez. Repeti-la, no entanto, não causa erros. A classe do driver JDBC é procurada no Classpath do projeto. Portanto, no projeto do Eclipse, o [jar], que contém a classe do driver JDBC, deve ter sido incluído no Classpath do projeto.

3.2.2. Etapa 2 – abertura de uma conexão

Assim que o driver JDBC estiver instalado, ele é solicitado a abrir uma conexão com o BD:

O código


package spring.jdbc;

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

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // tratando a exceção
            ...
        } finally {
         // fechando a conexão
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // tratar a exceção
                ...
            }
         }
}
  • linhas 3-7: as classes de implementação da interface JDBC estão todas no pacote [java.sql]. Além disso, em caso de erro, todas elas lançam uma exceção do tipo [SQLException] (linhas 19 e 27). Essa exceção deriva da classe [Exception] e é uma exceção chamada de controlada: é obrigatório usar um try/catch para tratá-la ou, alternativamente, não tratá-la e indicar que o método permite que a exceção seja lançada, completando a assinatura do método com [throws SQLException];
  • linha 17, [DriverManager.getConnection] é um método estático que espera três parâmetros:
    • [url]: o URL do banco de dados. Trata-se de uma sequência de caracteres que depende do BD utilizado. Para MySQL, ela tem o formato [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: o proprietário da conexão;
    • [passwd]: sua senha;
  • linhas 24-30: a conexão deve ser fechada na cláusula [finally], de modo que seja fechada independentemente de ocorrer ou não uma exceção.

3.2.3. Etapa 3 — emissão das ordens SQL e [SELECT]

Uma vez estabelecida a conexão, é possível emitir ordens SQL. A forma de gerenciar ordens de leitura [SELECT] difere daquela utilizada para as operações de atualização [UPDATE, INSERT, DELETE]. Começamos pelas ordens SQL e [SELECT]:

O código


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo somente leitura
            connexion.setReadOnly(true);
            // lendo a tabela [PRODUITS]
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
             doCatchException(connexion,e1);
        } finally {
            // tratando o finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • linhas 8, 10: abertura de uma transação (linha 8) no modo somente leitura (linha 10). Uma transação é uma sequência de ordens SQL que ou são todas bem-sucedidas ou todas falham. Assim, em uma transação com N ordens SQL, se a ordem I+1 falhar, as I anteriores serão canceladas. Para uma operação de leitura, não é necessária uma transação. No entanto, criar uma transação somente leitura pode permitir que certas ordens SGBD realizem algumas otimizações;
  • linha 12: uso de um [PreparedStatement]. Um [PreparedStatement] normalmente possui parâmetros indicados pelo caractere ?. Aqui, ele não os possui. Um [PreparedStatement] é uma ordem preparada pelo SGBD. Essa preparação tem um custo e é feita apenas uma vez. Em seguida, essa ordem preparada é executada pelo SGBD com diferentes parâmetros efetivos que substituirão os parâmetros formais ?. Vale ressaltar que é preferível nomear as colunas desejadas em vez de usar a notação * para obter todas as colunas. Ao especificar o nome das colunas, é possível obter seus valores a partir de sua posição na consulta SELECT;
  • linha 13: execução do [PreparedStatement]. Recupera-se um objeto do tipo [ResultSet];

Um objeto do tipo [ResultSet] representa uma tabela, ou seja, um conjunto de linhas e colunas. Em um determinado momento, só se tem acesso a uma linha da tabela, chamada de linha atual. Durante a criação inicial do [ResultSet], não há linha atual. É necessário executar uma operação [ResultSet.next()] para obtê-la. A assinatura do método next é a seguinte:

    boolean next()

Esse método tenta avançar para a próxima linha do [ResultSet] e retorna true se for bem-sucedido; caso contrário, retorna false. Se for bem-sucedido, a próxima linha se torna a nova linha atual. A linha anterior é perdida e não será possível voltar atrás para recuperá-la.

A tabela [ResultSet] possui colunas denominadas labelCol1, labelCol2, ... especificadas na consulta [SELECT] executada. Com a consulta:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • a coluna [ID] será transferida para uma coluna do [ResultSet] chamada [myId];
  • a coluna [NOM] será transferida para uma coluna do [ResultSet] chamada [myNom];
  • ...

No exemplo acima, os identificadores [myCol] são chamados de rótulos de coluna. Na ausência desses rótulos, os nomes das colunas do [ResultSet] dependem do SGBD. Quando o [SELECT] opera em uma única tabela, os rótulos das colunas serão, por padrão, os nomes das colunas solicitadas pelo SELECT. O problema surge quando o [SELECT] opera em várias tabelas e nessas tabelas existem nomes de colunas idênticos, como no exemplo a seguir:

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

supondo que a tabela [PRODUITS] tenha uma chave estrangeira para a tabela [CATEGORIES], representada pela relação [Produits].CATEGORIE_ID --> [CATEGORIES].ID, e que as tabelas [PRODUITS] e [CATEGORIES] tenham ambas um campo [NOM]. Nesse caso, os nomes atribuídos na tabela [ResultSet] às colunas [PRODUITS.NOM] e [CATEGORIES.NOM] dependem da tabela SGBD. Para a portabilidade entre o SGBD, é necessário, portanto, utilizar rótulos de colunas aqui, e escreveremos:


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

Para explorar os diferentes campos da linha atual do [ResultSet], dispomos dos seguintes métodos:

Type getType("labelColi") 

para obter a coluna denominada “labelColi” da linha atual e, portanto, a coluna do [SELECT] que possui esse rótulo. Type designa o tipo do campo coli. É possível utilizar os seguintes métodos [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Em vez de usar o nome da coluna, pode-se usar sua posição na consulta [SELECT] executada:

Type getType(i) 

onde i é o índice da coluna desejada (i>=1).

  • linhas 15-17: recuperação dos valores lidos na consulta BD;
  • linha 19: a transação é validada (também chamada de “confirmada”). Isso a encerra e libera os recursos que a transação SGBD havia mobilizado para ela;
  • linha 25: os recursos são liberados no [finally]. Este chama o método [doFinally] a seguir:

private void doFinally(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) {

            }
        }
        if (connexion != null) {
            try {
                // fechar a conexão
                connexion.close();
            } catch (SQLException e3) {
                 // tratar a exceção
            }
        }
    }
  • linhas 3-9: encerramento do [ResultSet];
  • linhas 11-17: encerramento do [PreparedStatement];
  • linhas 18-27: encerramento da conexão;

Os fechamentos nas linhas 3 a 17 parecem redundantes, uma vez que a conexão é fechada nas linhas 18 a 25. Na verdade, em alguns casos, eles não são redundantes, e é recomendável mantê-los no [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • linha 22: a exceção é tratada pelo método [doCatchException] a seguir:

    private static void doCatchException(Connection connexion, Throwable th) {
        // cancelar transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // tratar a exceção
        }
}
  • linhas 4-6: a transação é cancelada. Isso a encerra, e o SGBD poderá liberar os recursos mobilizados para ela;

3.2.4. Etapa 3 — emissão das ordens SQL e [INSERT, UPDATE, DELETE]

As ordens SQL e [INSERT, UPDATE, DELETE] são operações de atualização: elas modificam o banco de dados, mas não retornam nenhuma linha. A única informação fornecida é o número de linhas afetadas pela operação de atualização.

O código


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abrir conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo leitura/gravação
            connexion.setReadOnly(false);
            // atualização da tabela
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // categoria 1
            ps.setInt(1, 10);
            // execução
            int nbLignes=ps.executeUpdate();
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException(connexion, e1);
        } finally {
            // processando o finally
            doFinally(null, ps, connexion);
        }
    }
  • linha 9: a conexão é utilizada para leitura e gravação;
  • linha 11: um [PreparedStatement] com 1 parâmetro (simbolizado por ?). É possível ter vários parâmetros. Eles são numerados a partir de 1;
  • linha 13: atribui-se seu valor ao único parâmetro. O primeiro parâmetro de [setType] é a posição do parâmetro no [PreparedStatement] (1, 2, ...) e o segundo é o valor que lhe é atribuído. É possível utilizar os métodos [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • linha 15: utiliza-se o método [executeUpdate] e não o [executeQuery], reservado para as ordens SELECT. O método retorna o número de linhas afetadas pela operação. Pode ser 0.
  • linha 17: a transação é validada;

3.2.5. etapa 4 — encerramento da conexão

Uma conexão deve ser fechada o mais rápido possível em um contexto multiusuário, pois um SGBD aceita um número limitado de conexões abertas. Nos exemplos anteriores, ela era fechada na cláusula [finally] das operações SQL, de modo que fosse fechada independentemente de ter ocorrido uma exceção ou não.

3.3. Configuração da camada JDBC do SGBD MySQL5

Vamos analisar o projeto [mysql-config-jdbc], que configura a camada JDBC abaixo:

3.3.1. O projeto Eclipse

 

3.3.2. Configuração do Maven

O arquivo [pom.xml] do projeto é 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>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration generic jdbc</name>

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

    <dependencies>
        <!-- dependências variáveis ********************************************** -->
        <!-- driver JDBC do SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- dependências constantes ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- biblioteca jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Teste do Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- logs -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Nesta configuração do Maven, agrupamos alguns arquivos necessários tanto para o projeto [mysql-config-jdbc] quanto para os projetos que se basearão nele:

  • linhas 4-6: o artefato Maven gerado pelo projeto. Como já foi mencionado, todos os projetos do tipo [*-config-jdbc] geram esse mesmo artefato. Portanto, não se deve carregar dois projetos do tipo [*-config-jdbc] ao mesmo tempo;
  • linhas 9-13: o projeto Maven pai deste. Ele define as versões de um grande número de arquivos utilizados pelo ecossistema Spring. Isso evita que seja necessário especificá-las nos projetos derivados;
  • linhas 18-21: o arquivo do driver JDBC do SGBD MySQL5. Esse é o único arquivo necessário para o projeto [spring-jdbc-01];
  • linhas 24-27: o artefato [tomcat-jdbc] fornece um arquivo necessário para os projetos JDBC e [spring-jdbc-02 à 04];
  • linhas 29-36: fornecem as bibliotecas necessárias para o gerenciamento do jSON. Utilizadas em praticamente todos os projetos do documento;
  • linhas 38-42: o Google Guava é uma biblioteca de gerenciamento de coleções. Utilizada em praticamente todos os projetos do documento;
  • linhas 43-52: as bibliotecas que permitem a criação de testes integrando Spring e JUnit. Utilizadas em praticamente todos os projetos do documento;
  • linhas 54-57: as bibliotecas de logs. Utilizadas em praticamente todos os projetos do documento;
  • linhas 67-71: o plugin que permite instalar o artefato do projeto [mysql-config-jdbc] no repositório local do Maven;

3.3.3. A classe de configuração [ConfigJdbc]

  

A classe [ConfigJdbc] é a seguinte:


package generic.jdbc.config;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

public class ConfigJdbc {

    // parâmetros de conexão
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
    public final static String USER_DBPRODUITS = "root";
    public final static String PASSWD_DBPRODUITS = "root";
...
    // comandos SQL [jdbc-01, jdbc-02]
    public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
    //public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
    public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    // ordens SQL [jdbc-03]
    public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
    public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
    public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
    public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";

...

}

A classe [ConfigJdbc] serve para configurar a camada JDBC dos quatro projetos [spring-jdbc-01 à 04]. A maior parte da configuração diz respeito ao projeto [spring-jdbc-04]. Apresentaremos essa parte quando analisarmos esse projeto. Acima, mantivemos apenas a configuração dos projetos [spring-jdbc-01 à 03].

  • linhas 14-17: os parâmetros de conexão com o banco de dados MySQL5 [dbproduits];
  • linhas 20-25: os comandos SQL utilizados nos projetos [spring-jdbc-01 et 02];
  • linhas 28-34: as ordens SQL utilizadas no projeto [spring-jdbc-03];

Essas ordens SQL utilizam a tabela [PRODUITS] do banco de dados MySQL5 [dbproduits], cuja estrutura é a seguinte:

 
  • [ID]: chave primária no modo AUTO_INCREMENT (se não for fornecida uma chave primária, o SGBD a gera);
  • [NOM]: nome de um produto — único;
  • [CATEGORIE]: número da categoria;
  • [PRIX]: preço do produto;
  • [DESCRIPTION]: uma descrição do produto;

3.3.4. A classe [Produit]

  

A classe [Produit] é a imagem de uma linha da tabela [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

    // campos
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // construtores
    public Produit() {

    }

    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }

    // getters e setters
...
}

Mais adiante, precisaremos comparar dois produtos para saber se são iguais ou não. Consideraremos que dois produtos são iguais se todos os seus campos forem iguais. Para isso, vamos redefinir o método [equals] da classe [Object], da qual deriva a classe [Produit]:


    // método de igualdade
    @Override
    public boolean equals(Object o) {
        // casos simples
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Produit p = (Produit) o;
        return this == o
                || (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
                        && Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
  • linha 3: o método [equals] recebe um objeto o que deve comparar com o objeto this;
  • linhas 5-7: os casos simples em que é possível determinar imediatamente que os dois objetos não são iguais. [Object].getClass() fornece uma instância do tipo [Class], um tipo que representa a classe real do objeto;
  • linha 8: o objeto o é convertido em produto p;
  • linha 9: se as duas referências o e p a um produto forem iguais, então trata-se fisicamente do mesmo produto;
  • linha 9: se o e p forem duas referências diferentes a dois produtos com os mesmos campos, diremos que são iguais. Como o preço é do tipo [double] e não há representação exata dos números reais na informática, consideraremos que dois preços são idênticos se forem iguais com uma precisão de 10⁻⁶;

Além disso, redefiniremos o método [hasCode] da classe [Object]:


    // hashcode
    @Override
    public int hashCode() {
        return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}

Os valores de hashCode de dois produtos devem ser os mesmos se o método [equals] tiver declarado esses dois produtos como iguais. Esse valor de hashCode é utilizado para distribuir objetos em conjuntos, como dicionários. Conforme mencionado acima, se dois produtos forem idênticos, eles terão, de fato, o mesmo hashCode.

3.3.5. A exceção [UncheckedException]

  

Consideremos a seguinte arquitetura:

  • a camada [JDBC] lança exceções do tipo [SQLException]. Essa exceção deve subir pelas camadas até atingir a camada mais alta, neste caso, a camada de testes;

A camada [DAO] poderia simplesmente permitir que a [SQLException] fosse encaminhada até a camada de testes. Mas, como essa exceção é não controlada (decorre diretamente de [Exception]), isso implicaria que a interface [IDao] da camada [DAO] fosse a seguinte:


public interface IDao {

    // adicionar produtos
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // lista de todos os produtos
    public List<Produit> getAllProduits() throws SQLException;

    // um produto específico
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // atualização de vários produtos
    public int updateProduits(List<Produit> produits) throws SQLException;

    // exclusão de todos os produtos
    public int deleteAllProduits() throws SQLException;

    // exclusão de vários produtos
    public int deleteProduits(int[] ids) throws SQLException;
}

E isso é muito incômodo, pois nos impede de implementar a interface [IDao] por meio de uma classe que lançaria uma exceção diferente. Para contornar essa dificuldade, a camada [DAO] lançará uma exceção [DaoException] não controlada (derivada de [RuntimeException]), o que nos poupa da cláusula [throws] na assinatura dos métodos da interface. Assim, ela poderá ser implementada por qualquer classe que também lance uma exceção não controlada, que poderá ser diferente da exceção [DaoException]. Nossa arquitetura passa a ser a seguinte:

Para facilitar a criação de exceções não controladas para diferentes camadas de uma aplicação, criamos uma classe pai para elas, [UncheckedException]:

  

package generic.jdbc.infrastructure;

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

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

// classe de exceção genérica
// a exceção é não controlada

public class UncheckedException extends RuntimeException {

    // número de série ID gerado
    private static final long serialVersionUID = -2924871763340170310L;

    // propriedades
    private int code;
    private String trace;
    private List<ShortException> exceptions;

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

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // rastreamento
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
    }

    @Override
    public String getMessage() {
        return this.toString();
    }

    @Override
    public void printStackTrace() {
        System.out.println(this);
    }

    // lista de mensagens de erro de uma exceção
    private List<ShortException> getErreursForException(Throwable th) {
        // recupera-se os elementos da pilha da exceção
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // recuperação da exceção atual
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // próxima exceção
            cause = cause.getCause();
        }
        return exceptions;
    }

    @Override
    public String toString() {
        ObjectMapper jsonMapper = new ObjectMapper();
        try {
            return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // getters e setters
...
}
  • linha 12: a classe deriva de [RuntimeException] e, portanto, é um tipo de exceção não controlada. Ela servirá para encapsular uma exceção controlada (SQLException) em um tipo de exceção não controlada (UncheckedException);
  • para diferenciar entre as exceções do tipo [UncheckedException], é possível atribuir a elas um código que será armazenado no campo privado da linha 18. Um código Java que intercepta uma exceção do tipo [UncheckedException] terá acesso a esse código de erro por meio do método [getCode] (linhas 80 e seguintes);
  • linha 20: armazena as mensagens de erro da pilha da exceção encapsulada;
  • linhas 23-43: as diferentes maneiras de construir um objeto do tipo [UncheckedException];
  • linhas 56-67: um método privado que permite construir a lista de erros da linha 20 a partir de um objeto [Throwable] ou derivado, em particular o tipo [Exception];
  • linhas 69-78: o método [toString] retorna uma sequência de caracteres que representa a exceção. Para exibir a lista de erros da linha 20, ele utiliza uma biblioteca jSON. Essa biblioteca está presente nas dependências Maven do projeto:

        <!-- biblioteca jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
</dependency>
  • linhas 45-48: redefinem o método [getMessage] da classe pai [RuntimeException]. Aqui, ele retorna a assinatura [toString] da classe;
  • linhas 50-53: redefinem o método [printStackTrace] da classe pai [RuntimeException]. É a assinatura [toString] da classe que será exibida;

A classe [UncheckedException] registra no campo da linha 20 uma lista de exceções descritas pelo tipo [ShortException] a seguir:


package pam.dao.exceptions;

public class ShortException {

    // propriedades
    private String className;
    private String errorMessage;

    // construtores
    public ShortException() {

    }

    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }

    // getters e setters
...
}
  • linha 6: o nome da classe da exceção que ocorreu;
  • linha 7: a mensagem de erro associada;

Vamos examinar o seguinte construtor da classe [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // rastreamento
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
}
  • linha 1, os parâmetros são os seguintes:
    • [code]: um código de erro;
    • [e]: a exceção que está sendo encapsulada. [Throwable] é a classe pai da classe [Exception] e deriva diretamente da classe [Object]. É a classe pai de todas as classes C com as quais é possível escrever [throw c;], onde c é uma instância de C;
    • [simpleClassName]: o nome simples da classe do código do usuário onde a exceção e foi detectada;
  • linha 4: o código de erro é registrado;
  • linha 5: a lista de [ShortException] é construída a partir do [Throwable e] passado como parâmetro;
  • linhas 7-16: examina-se o que se denomina “rastreamento da exceção”. Uma exceção inicial ocorre em um ponto específico do código e, em seguida, remonta ao método que chamou aquele em que a exceção ocorreu, e assim por diante até que um try/catch a interrompa. Nesse rastreamento, a exceção inicial deixa rastros armazenados na matriz [e.stackTrace] da exceção e. Esses rastros são obtidos aqui na linha 8, a partir do [Throwable e] passado como parâmetro. Cada elemento do tipo [StackTraceElement] é um objeto que possui, entre seus campos, os seguintes:
    • [fileName]: o nome do arquivo Java onde ocorreu a exceção;
    • [lineNumber]: o número da linha nesse arquivo em que ocorreu a exceção;
    • [methodName]: o nome do método nesse arquivo onde ocorreu a exceção;
  • as linhas 10 a 16 procuram, na tabela de rastros da exceção passada como parâmetro, a primeira ocorrência da condição [trace.fileName==simpleClassName.java], em que [simpleClassName] é o terceiro parâmetro do construtor. A ideia é registrar onde a exceção ocorreu no código do usuário. Este encapsulará uma exceção da seguinte maneira:
1
2
3
4
5
6
7
try{
// código que pode lançar uma exceção controlada
...
}catch(UnTypeDexception e){
// encapsula-se a exceção controlada e em uma exceção não controlada
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • linha 13: cria-se uma cadeia de caracteres do tipo [fileName, methodName, lineNumber] que identifica o local no código do usuário onde a exceção foi interceptada;

Agora, vamos examinar o código que registra a lista de exceções da pilha de exceções da exceção [Throwable th] encapsulada pelo construtor anterior:


    // lista de mensagens de erro de uma exceção
    private List<ShortException> getErreursForException(Throwable th) {
        // recupera-se os elementos da pilha da exceção
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // recupera-se a exceção atual
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // próxima exceção
            cause = cause.getCause();
        }
        return exceptions;
}

Durante sua subida até o método que a interceptou por meio de um try/catch, a exceção inicial e pôde ter sido encapsulada em uma exceção. É então essa última que realiza sua subida até o método que a interceptará definitivamente. Portanto, ela também pode ser encapsulada. No final das contas, quando um método decide interceptar uma exceção th e explorá-la, ele encontrará a exceção inicial e enterrada no fundo de uma pilha de exceções. Assim, no exemplo acima, o parâmetro [Throwable th] é apenas a ponta do iceberg das exceções. Seu atributo [th.cause] permite identificar a exceção que ela própria encapsula. E assim por diante. Quando uma exceção e tem o valor [e.getCause()==null], isso significa que e é a exceção inicial.

  • linha 8: para cada exceção da pilha de exceções de [Throwable th], são armazenadas duas informações:
    • [getClass().getName()]: o nome completo da exceção;
    • [getMessage()]: a mensagem de erro associada;

3.4. Exemple-01

3.4.1. A arquitetura do projeto

Neste exemplo, um programa de console utiliza a interface da camada [JDBC].

3.4.2. O projeto Eclipse

Criamos um projeto Spring/Maven [spring-jdbc-01] seguindo os passos descritos no parágrafo 2.5.2.1.

  

O projeto é um projeto Maven definido pelo seguinte arquivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-01</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- pesquisa do pai no repositório -->
    </parent>

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

    <dependencies>
        <!-- configuração JDBC do SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • linhas 28-32: o projeto utiliza o artefato [generic-config-jdbc] do projeto [mysql-config-jdbc] que acabamos de analisar. O projeto [spring-jdbc-01] tem, portanto, acesso a todos os elementos do projeto [mysql-config-jdbc];

É possível observar esse último ponto de duas maneiras ao inspecionar as dependências do Maven do projeto:

  • no [2], percebe-se que o projeto [mysql-config-jdbc] está nas dependências Maven do projeto. Como essas dependências estão no Classpath do projeto, isso significa que o projeto [mysql-config-jdbc] também está nesse Classpath e que, portanto, suas classes e interfaces são visíveis no projeto [spring-jdbc-01];

O projeto Maven [mysql-config-jdbc] não precisa estar presente na aba [Package Explorer] para ser utilizado por outros projetos Maven. Basta que ele esteja presente no repositório local do Maven. Ao contrário de um IDE como o NetBeans, essa presença não é automática no Eclipse. É preciso forçá-la:

Vimos as condições que tornam essa geração possível no parágrafo 2.3.5. Depois que ela for concluída, é possível remover o projeto [mysql-config-jdbc] da guia [Package Explorer]:

  • não se deve marcar a opção [3], que exclui fisicamente o projeto do disco, tornando-o irrecuperável;

Essa operação reinicia o cálculo das dependências Maven dos projetos que dependem do projeto removido do [Package Explorer]. Isso altera o ramo [Maven Dependencies] desses projetos. Por exemplo, para o projeto [spring-jdbc-01], o ramo [Maven Dependencies] passa a ser o seguinte:

Desta vez, a dependência não está mais em um projeto, mas no artefato Maven desse projeto, neste caso, o artefato [generic-config-jdbc] [1]. Vemos que temos acesso a todas as classes e interfaces desse artefato. Conforme mencionado, esse artefato será gerado por todos os projetos [*-config-jdbc]. Para evitar erros, nós:

  • manteremos sempre um único projeto [*-config-jdbc] na aba [Package Explorer];
  • atualizaremos a configuração do Maven de todos os projetos na aba [Package Explorer] (Alt-F5) para que eles incluam, em suas dependências do Maven, o projeto [*-config-jdbc] utilizado;

3.4.3. O esboço da classe principal

  

O esboço da classe principal [IntroJdbc01] é o seguinte:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

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

public class IntroJdbc01 {

    // constantes
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // carregamento do driver JDBC do SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // esvaziamento da tabela [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // preenchendo-a
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // a tabela é lida
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // atualização
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // exibição
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // esvazia-se a tabela [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // exibição
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion de dois elementos idênticos
        // a transação INSERTion deve falhar e nenhum dos dois elementos é inserido devido à transação
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // verifica-se
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // concluído
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // lista de produtos
    private static void select() {
    ...
    }

    // exibição de um objeto jSON
    private static void affiche(Object object) {
...
    }

    // exclusão de produtos
    public static void delete() {
...
    }

    // adição de produtos
    public static void insert() {
...
    }

    // adição de 2 produtos com as mesmas chaves primárias
    public static void insert2() {
...
    }

    // atualização de alguns produtos
    public static void update() {
...
    }

    private static void doFinally(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) {

            }
        }
        if (connexion != null) {
            try {
                // encerrar a conexão
                connexion.close();
            } catch (SQLException e3) {
                // exibe as mensagens de erro
                show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // exibe as mensagens de erro
        show(title, getErreursFromThrowable(th));
        // cancelamento da transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // exibe as mensagens de erro
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // recupera-se a lista de mensagens de erro da exceção
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // mensagem de erro do throwable
            erreurs.add(th.getMessage());
            // avança para a causa do throwable
            th = th.getCause();
        }
        // resultado
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // título
        System.out.println(String.format("%s : ", title));
        // mensagens
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • linhas 23-29: carregamento do driver JDBC do SGBD. Na linha 25, utiliza-se a constante [ConfigJdbc.DRIVER_CLASSNAME] definida no projeto [mysql-config-jdbc];
  • linhas 136-147: o método [getErreursFromThrowable] retorna a lista de mensagens de erro encapsuladas em um objeto do tipo [Throwable], que é a classe pai da classe [Exception]. Uma exceção pode conter outra, que é obtida com o método [Throwable].getCause(). Assim, são percorridas todas as exceções encapsuladas no objeto [Throwable];
  • linhas 149-156: o método [show(String title, List<String> messages)] exibe as mensagens precedidas pelo texto [title];
  • linhas 122-134: o método [doCatchException(String title, Connection connexion, Throwable th))] lida com as exceções encontradas pelos métodos da classe. A exceção tratada é representada pelo parâmetro [Throwable th]. O objetivo do método é:
    • cancelar a transação em andamento do objeto [Connection connexion] (linhas 127-129);
    • gravar as mensagens de erro encapsuladas na exceção [Throwable th] (linhas 124, 132);
  • linhas 93-120: o método [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] gerencia o ramo [finally] dos métodos de acesso ao SGBD. Seu objetivo é liberar os recursos utilizados pela conexão;

3.4.4. Exclusão do conteúdo da tabela de produtos

O método [delete] exclui o conteúdo da tabela:


    // exclusão de produtos
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // início da sessão
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo de leitura/gravação
            connexion.setReadOnly(false);
            // esvaziando a tabela [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(null, ps, connexion);
        }
}

A linha 7 utiliza as seguintes constantes da classe [ConfigJdbc]:


public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";

Na linha 13, a ordem SQL preparada é a seguinte:


public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";

O método [delete] utiliza transações. Uma transação permite agrupar ordens SQL que devem ser todas executadas com sucesso ou todas canceladas. Há quatro operações a serem consideradas:

  • início de uma transação: [connexion.setAutoCommit(false)];
  • fim de uma transação com sucesso: [connexion.commit()]. Nesse caso, todas as operações realizadas na BD durante a transação são validadas;
  • fim de uma transação com falha: [connexion.rollback()]. Nesse caso, todas as operações realizadas no BD durante a transação são canceladas;

Em nossos exemplos, sempre que ocorre uma exceção, cancelamos a transação no método [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // exibindo as mensagens de erro
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancelamento da transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // exibindo as mensagens de erro
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

3.4.5. Criação do conteúdo da tabela de produtos

O método [insert] cria o conteúdo da tabela:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo de leitura/gravação
            connexion.setReadOnly(false);
            // preenchimento da tabela
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // preparação
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // execução
                ps.executeUpdate();
            }
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(null, ps, connexion);
        }
    }

Na linha 12, a ordem SQL preparada é a seguinte:


public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";

3.4.6. Exibição do conteúdo da tabela de produtos

O método [select] exibe o conteúdo da tabela:


// lista de produtos
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura de conexão
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo somente leitura
            connexion.setReadOnly(true);
            // lendo a tabela [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(rs, ps, connexion);
        }
    }

Na linha 14, a ordem SQL preparada é a seguinte:


public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";

O método [affiche] (linha 18) é o seguinte:


    // exibição de um objeto jSON
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Ela exibe a representação jSON do objeto passado como parâmetro (ver jSON, parágrafo 23.12).

3.4.7. Atualização do conteúdo da tabela

O método [update] atualiza determinados produtos:


    // atualização de alguns produtos
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abertura de conexão
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo de leitura/gravação
            connexion.setReadOnly(false);
            // atualização da tabela
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // categoria 1
            ps.setInt(1, 1);
            // execução
            ps.executeUpdate();
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // processando o finally
            doFinally(null, ps, connexion);
        }
}

Na linha 13, a ordem SQL preparada é a seguinte:


public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";

3.4.8. Função da transação

O método [insert2] insere dois produtos com a mesma chave primária na tabela, o que não é possível. Como estamos em uma transação, a primeira inserção será cancelada.


    // adição de 2 produtos com as mesmas chaves primárias
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abertura de conexão
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo de leitura/gravação
            connexion.setReadOnly(false);
            // adicionando 1 linha
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // execução
            ps.executeUpdate();
            // a mesma linha é adicionada uma segunda vez, portanto com a mesma chave primária
            // o INSERTion deve falhar e nenhum dos dois elementos deve ser inserido devido à transação
            ps.executeUpdate();
            // confirmar transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratamos a exceção
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // processa-se o finally
            doFinally(null, ps, connexion);
        }
}

Linha 13, a ordem SQL preparada é a seguinte:


public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

3.4.9. Resultados

Executamos a configuração de execução denominada [spring-jdbc-generic-01.IntroJdbc01]:

 

Obtêm-se os seguintes resultados no console:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé
  • linha 30: antes da inserção dos dois produtos com a mesma chave primária, a tabela está vazia;
  • linha 35: após a inserção dos dois produtos com a mesma chave primária, a tabela está vazia. Isso demonstra a função da transação:
    • a primeira inserção é bem-sucedida. Não há motivo para que ela falhe;
    • a segunda inserção falha (linha 32). Assim, como essas duas inserções estão dentro da mesma transação, todas as ordens SQL dessa transação são canceladas, incluindo a primeira inserção.

3.4.10. Conclusão

O que chama a atenção nos códigos anteriores é o grande espaço dedicado ao tratamento da exceção [SQLException]. Como qualquer operação JDBC pode acioná-la, há vários blocos try/catch no código.

3.5. Exemple-02

Vamos retomar o aplicativo anterior utilizando uma fonte de dados do tipo [javax.sql.DataSource]:

Image

Vamos utilizar uma fonte de dados implementada pela classe [org.apache.tomcat.jdbc.pool.DataSource]. Essa classe utiliza um pool de conexões, ou seja, um conjunto de conexões abertas:

  • quando o pool é instanciado, um determinado número de conexões é aberto com o banco de dados. Esse número é configurável;
  • quando o código Java abre uma conexão, ela é fornecida pelo pool;
  • quando o código Java fecha uma conexão, ela é devolvida ao pool;

No final das contas, as conexões são abertas apenas uma vez, o que melhora o desempenho do acesso ao banco de dados. A fonte de dados será definida em uma classe de configuração do Spring

3.5.1. A arquitetura do projeto

Neste exemplo, um programa de console utiliza a interface da camada [JDBC].

3.5.2. O projeto Eclipse

O novo projeto Eclipse pode ser obtido copiando-se o projeto anterior [1-6]:

Em seguida, atualiza-se o projeto de [6] para [7]:

3.5.3. Configuração do Maven

O projeto [7] é um projeto Maven definido pelo seguinte arquivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-02</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- busca o pai no repositório -->
    </parent>

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

    <dependencies>
        <!-- configuração JDBC do SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • linhas 28-33: a dependência do Maven do projeto [mysql-config-jdbc];

É o projeto [mysql-config-jdbc] que inclui em suas dependências do Maven a biblioteca que fornece uma implementação de uma fonte de dados do tipo [javax.sql.DataSource] (ver parágrafo 3.3.2):


        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
</dependency>

3.5.4. Configuração do Spring

  

A classe de configuração do Spring [AppConfig] é a seguinte:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
    // fonte de dados
    @Bean
    public DataSource dataSource() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração de acesso JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // conexões abertas inicialmente
        dataSource.setInitialSize(5);
        // resultado
        return dataSource;
    }

}
  • linha 10: [AppConfig] é uma classe de configuração do Spring;
  • linha 11: importação da classe de configuração [generic.jdbc.config.ConfigJdbc.class] definida no projeto [mysql-config-jdbc]. Isso significa que temos à disposição todos os beans definidos por esse arquivo de configuração;
  • linhas 14-27: o bean do Spring que define a fonte de dados;
  • linha 17: criação da fonte de dados, ainda não configurada;
  • linhas 19-22: as informações que permitem que a fonte de dados se conecte ao banco de dados;
  • linha 24: cria um pool de 5 conexões. Aqui, precisamos apenas de uma. Nunca há várias conexões simultâneas;

3.5.5. A classe principal

A classe principal [IntroJdbc02] é a seguinte:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class IntroJdbc02 {

    // mapeamento jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // fonte de dados
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // recuperação do contexto Spring
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // recuperação da fonte de dados
            dataSource = ctx.getBean(DataSource.class);
            // esvaziamento da tabela [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // concluído
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // lista de produtos
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura da conexão
            connexion = dataSource.getConnection();
            // início da transação
            connexion.setAutoCommit(false);
            // no modo somente leitura
            connexion.setReadOnly(true);
            // lendo a tabela [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • linha 25: a fonte de dados. Observe que ela é do tipo [javax.sql.DataSource] (linha 13), que é uma interface;
  • linha 31: instanciação dos objetos Spring;
  • linha 32: obtenção de uma referência à fonte de dados. Observe-se que, em nenhum momento, é mencionada a classe realmente utilizada. Assim, neste caso, nada sugere que se esteja utilizando uma implementação [TomcatJdbc];
  • linha 49: obtenção de uma conexão aberta. É assim que os diferentes métodos de [IntroJdbc02] obtêm uma conexão com o banco de dados. O restante do código é idêntico ao da classe [IntroJdbc01];

3.5.6. Os testes

Executa-se a configuração de execução denominada [spring-jdbc-generic-02.IntroJdbc02]:

 

Obtêm-se os mesmos resultados que anteriormente (parágrafo 3.4.9).

3.6. Exemple-03

3.6.1. A arquitetura do projeto

Neste exemplo, os métodos de acesso aos dados estão isolados em uma camada [dao]. Eles serão testados por um teste JUnit.

3.6.2. O projeto Eclipse

O projeto Eclipse [spring-jdbc-03] é um projeto Spring/Maven construído da mesma forma que o anterior e complementado da seguinte maneira:

 

Os diferentes pacotes têm as seguintes funções:

  • [spring.jdbc.config]: configuração do projeto Spring;
  • [spring.jdbc.dao]: implementação da camada [DAO];
  • [spring.jdbc.infrastructure]: implementa a exceção não controlada [DaoException];

3.6.3. Configuração do Maven

O projeto Maven é configurado pelo seguinte arquivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-03</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- pesquisa o pai no repositório -->
    </parent>

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

    <dependencies>
        <!-- configuração JDBC do SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Ele é idêntico ao do projeto [spring-jdbc-02]. Ele utiliza, notadamente, a dependência Maven do projeto [mysql-config-jdbc] (linhas 28-32).

3.6.4. Interface da camada [DAO]

  

A camada [DAO] apresenta a seguinte interface [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // adicionar produtos
    public List<Produit> addProduits(List<Produit> produits);

    // lista de todos os produtos
    public List<Produit> getAllProduits();

    // um produto específico
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // atualização de vários produtos
    public int updateProduits(List<Produit> produits);

    // exclusão de todos os produtos
    public int deleteAllProduits();

    // exclusão de vários produtos
    public int deleteProduits(int[] ids);
}

3.6.5. A classe [DaoException]

A classe [DaoException] limita-se a estender a classe [UncheckedException] apresentada no parágrafo 3.3.5:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // fabricantes
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e, String className) {
        super(code, e, className);
    }

}

3.6.6. Configuração do projeto Spring

  

A classe [AppConfig], que configura o projeto Spring, é idêntica ao arquivo de configuração do Spring do exemplo [spring-jdbc-02], com exceção da linha 11:


package spring.jdbc.config;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
    // fonte de dados
    @Bean
    public DataSource dataSource() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração de acesso JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // conexões abertas inicialmente
        dataSource.setInitialSize(5);
        // resultado
        return dataSource;
    }
}
  • linha 11: o pacote [spring.jdbc.dao] será verificado para localizar outros componentes Spring além daqueles definidos neste arquivo de configuração;

3.6.7. Implementação da camada [DAO]

  

Vale lembrar (parágrafo 3.6.4) que a camada [DAO] implementa a seguinte interface [IDao]:


package spring.jdbc.dao;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.List;

public interface IDao {

    // adicionar produtos
    public List<Produit> addProduits(List<Produit> produits);

    // lista de todos os produtos
    public List<Produit> getAllProduits();

    // um produto específico
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // atualização de vários produtos
    public int updateProduits(List<Produit> produits);

    // exclusão de todos os produtos
    public int deleteAllProduits();

    // exclusão de vários produtos
    public int deleteProduits(int[] ids);
}

Ambas as classes [Dao1, Dao2] implementam essa interface. A classe [Dao2] é uma variante da classe [Dao1] que introduz uma novidade sintática. Vamos nos concentrar na classe [Dao1]. Sua estrutura é a seguinte:


package spring.jdbc.dao;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.jdbc.infrastructure.DaoException;

@Component("dao1")
public class Dao1 implements IDao {

    // nome da classe
    private String simpleClassName = getClass().getSimpleName();
    // fonte de dados
    @Autowired
    protected DataSource dataSource;

    // fabricante
    public Dao1() {
        System.out.println("building Dao1...");
    }

    // ------------------------------- interface
    @Override
    public List<Produit> getAllProduits() {
...
    }

    @Override
    public Produit getProduitById(int id) {
...
    }

    @Override
    public Produit getProduitByName(String name) {
...
    }

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
....
    }

    @Override
    public int updateProduits(List<Produit> produits) {
...
    }

    @Override
    public int deleteAllProduits() {
...
    }

    @Override
    public int deleteProduits(int[] ids) {
...
    }

    // ---------------------------------------- métodos locais
    // gestão do finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // gestão de catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • linha 20: a classe [Dao] é um componente Spring denominado [dao1]. Esse nome é opcional. Quando não estiver presente, o nome utilizado será o nome da classe com a primeira letra maiúscula convertida em minúscula;
  • linha 24: o nome da classe. Evita-se escrever [Dao] de forma rígida para permitir renomear a classe sem precisar redefinir esse campo, que assim permanece sempre válido;
  • linhas 26-27: injeção da fonte de dados [tomcat-jdbc] definida na classe de configuração [AppConfig];
  • linhas 36-68: implementação da interface [IDao];
  • linhas 78-80: gerenciamento centralizado do catch dos diferentes métodos;
  • linhas 72-75: gerenciamento centralizado do finally dos diferentes métodos;

O catch dos diferentes métodos é gerenciado da seguinte forma:


    // gestão de catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // cancelamento de transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • linha 2: o método é declarado como [protected], o que permite que as classes filhas o utilizem sem que ele seja, por isso, público. Ele recebe os seguintes parâmetros:
    • [Connection connexion]: a conexão com o SGBD — talvez null;
    • [Throwable th]: a exceção que ocorreu e que será encapsulada em um tipo [DaoException];
    • [int code]: um código de erro a ser utilizado caso o método crie um novo [DaoException];
  • linhas 4-7: a principal função desse método é cancelar a transação associada à conexão passada como parâmetro 1;
  • linhas 8-10: se a anulação da transação não for bem-sucedida, o log da exceção é gravado no console. Não há muito mais o que fazer, já que uma exceção será lançada na linha 12;

O finally dos diferentes métodos é gerenciado da seguinte maneira:


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

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

            }
        }
        // encerramento da conexão
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // registra-se o erro, se possível
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // resultado
        return daoException;
    }
  • linha 2: este método também é declarado como [protected]. Ele recebe os seguintes parâmetros:
    • [ResultSet rs]: o eventual [ResultSet], caso tenha sido executada uma operação [SELECT] — talvez null;
    • [PreparedStatement ps]: o [PreparedStatement] que foi executado — talvez null;
    • [Connection connexion]: a conexão com o SGBD — talvez null;
    • [int code]: um código de erro a ser usado caso o método crie um novo [DaoException];
    • [DaoException daoException]: o eventual [DaoException] caso tenha ocorrido uma exceção antes do finally — talvez null;
  • linhas 21-30: o objetivo principal deste método é fechar a conexão (linha 23);
  • linhas 24-29: se, durante esse fechamento, ocorrer uma exceção, verifica-se o estado do parâmetro [DaoException daoException] que nos foi passado: se for [daoException == null], cria-se um novo [DaoException] com o código passado como parâmetro;
  • linha 32: o antigo ou o novo [DaoException] é retornado como resultado;

Não vamos apresentar todos os métodos da classe [Dao], mas apenas alguns. Todos eles são semelhantes.

3.6.7.1. O método [getProduitById]

O método [getProduitById] retorna o produto cuja chave primária é igual ao parâmetro [id] ou, caso contrário, ao null;


@Override
    public Produit getProduitById(int id) {
        // recursos da conexão
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // inicialmente, nenhuma exceção
        DaoException daoException = null;
        // o produto procurado
        Produit produit = null;
        try {
            // abertura da conexão
            connexion = dataSource.getConnection();
            // início da transação
            connexion.setAutoCommit(false);
            // no modo somente leitura
            connexion.setReadOnly(true);
            // lendo a tabela [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
            }
            // confirmação da transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratando a exceção
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // processando o finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // exceção?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return produit;
    }
  • linha 10: o produto a ser devolvido é definido como null;
  • linha 19: a ordem SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] é a seguinte:

public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";

  • linhas 22-24: se o [ResultSet] tiver uma linha, ela é usada para criar o produto a ser devolvido; caso contrário, o produto a ser devolvido permanece em null;
  • linha 41: o produto é devolvido;
  • linha 8: a exceção [DaoException] do método é inicializada como null;
  • linha 31: o método [doCatchException] gera uma exceção [DaoException];
  • linha 34: o parâmetro [daoException] do método [doFinally] é ou null, ou a exceção criada pelo método [doCatchException]. O método [doFinally]:
    • mantém esse parâmetro inalterado se conseguir fechar a conexão;
    • mantém esse parâmetro inalterado se não conseguir fechar a conexão e já tiver ocorrido um [DaoException] anteriormente;
    • cria um novo [DaoException] se não conseguir fechar a conexão e não tiver havido um [DaoException] anteriormente;
  • linhas 37-39: se a exceção local [daoException] não for igual a null, então ela é lançada; caso contrário, retorna-se o resultado solicitado (linha 41);

3.6.7.2. O método [deleteProduits]

O método [deleteProduits] exclui os produtos cujas chaves primárias são passadas como parâmetros. Ele retorna o número de produtos excluídos.


@Override
    public int deleteProduits(int[] ids) {
        // recursos da conexão
        PreparedStatement ps = null;
        Connection connexion = null;
        // inicialmente, nenhuma exceção
        DaoException daoException = null;
        // número de produtos atualizados
        int nbProduits = 0;
        try {
            // abertura da conexão
            connexion = dataSource.getConnection();
            // início da transação
            connexion.setAutoCommit(false);
            // no modo leitura/gravação
            connexion.setReadOnly(false);
            // exclusão de produtos
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // parâmetros
                ps.setInt(1, id);
                // execução
                nbProduits += ps.executeUpdate();
            }
            // confirmar transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratando a exceção
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // tratando o finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // exceção?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return nbProduits;
    }
  • linha 18, a ordem SQL [ConfigJdbc.V2_DELETE_PRODUITS] é a seguinte:

public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";

  • linhas 18-24: o código de exclusão de produtos. Observa-se que a ordem SQL é preparada 1 vez (linha 18) e executada n vezes (linhas 19-24). Esse é o objetivo do objeto [PreparedStatement];
  • linha 23: o método [PreparedStatement].executeUpdate() retorna o número de linhas afetadas pela operação de atualização;
  • linha 41: é retornado o número de produtos atualizados;

3.6.7.3. O método [updateProduits]

O método [updateProduits] atualiza no banco de dados os produtos que lhe são passados como parâmetros. Ele retorna o número de produtos atualizados.


@Override
    public int updateProduits(List<Produit> produits) {
        // recursos da conexão
        PreparedStatement ps = null;
        Connection connexion = null;
        // inicialmente, nenhuma exceção
        DaoException daoException = null;
        // número de produtos atualizados
        int nbProduits = 0;
        try {
            // abertura da conexão
            connexion = dataSource.getConnection();
            // início da transação
            connexion.setAutoCommit(false);
            // no modo de leitura/gravação
            connexion.setReadOnly(false);
            // atualizando a tabela [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // parâmetros
                ps.setString(1, produit.getNom());
                ps.setDouble(2, produit.getPrix());
                ps.setInt(3, produit.getCategorie());
                ps.setString(4, produit.getDescription());
                ps.setInt(5, produit.getId());
                // execução
                nbProduits += ps.executeUpdate();
            }
            // confirmar transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratamento da exceção
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // tratando o finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // exceção?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return nbProduits;
    }
  • linha 18: a ordem SQL [ConfigJdbc.V2_UPDATE_PRODUITS] é a seguinte:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • linhas 19-28: o código de atualização dos produtos;

3.6.7.4. O método [addProduits]

O método [addProduits] insere no banco de dados os produtos que lhe são passados como parâmetros. Ele retorna esses mesmos produtos com suas chaves primárias (antes da inserção no banco de dados, os produtos não possuem chave primária).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // recursos da conexão
        PreparedStatement ps = null;
        Connection connexion = null;
        // inicialmente, nenhuma exceção
        DaoException daoException = null;
        try {
            // abertura da conexão
            connexion = dataSource.getConnection();
            // no modo leitura/gravação
            connexion.setReadOnly(false);
            // início da transação
            connexion.setAutoCommit(false);
            // adicionando elementos à tabela [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // parâmetros
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // execução do comando
                ps.executeUpdate();
                // chave primária gerada
                ResultSet generatedKeys = ps.getGeneratedKeys();
                if (generatedKeys.next()) {
                    produit.setId(generatedKeys.getInt(1));
                } else {
                    throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
                            produit.getNom()));
                }
            }
            // confirmação da transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // tratando a exceção
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // processando o finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // exceção?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return produits;
}
  • na linha 16, a ordem SQL [ConfigJdbc.V2_INSERT_PRODUITS] é a seguinte:

public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";

No exemplo acima, o comando de inserção de um produto não inclui a chave primária [ID]. Como a chave primária do banco de dados MySQL possui o atributo [AUTOINCREMENT], o SGBD irá, então, gerar uma chave primária para cada inserção. Surge o problema de recuperar essa chave. Esse é um ponto importante, pois as operações nos produtos são realizadas por meio de suas chaves primárias. Portanto, é necessário conhecê-las;

  • linhas 17-33: o loop de inserção dos produtos;
  • linha 16: uma variante específica do método [prepareStatement]. O segundo parâmetro, [generatedColumns], é uma matriz com os nomes das colunas cujos valores queremos recuperar após a inserção. Na linha 16, indicamos que queríamos recuperar o valor da coluna [id]. Vale ressaltar aqui que, embora os nomes das colunas de uma tabela não sejam sensíveis a maiúsculas e minúsculas, o SGBD PostgreSQL exigiu que esse nome fosse escrito em minúsculas. Esse é tipicamente o tipo de problema que se encontra ao portar um código de um SGBD para outro;
  • linha 24: inserção de uma linha no banco de dados;
  • linha 26: recuperamos a lista de valores das colunas especificadas na linha 16 de um [ResultSet]. Aqui, para 1 inserção, o [ResultSet] terá 1 linha e essa linha terá uma única coluna que contém a chave primária;
  • linha 28: recupera-se a chave primária gerada pelo SGBD;
  • linhas 29-32: se não for obtida a chave primária gerada, é executado um [RuntimeException], que será encapsulado em um [DaoException] nas linhas 38-40;

3.6.8. A classe [Dao2]

  

A classe [Dao2] é uma variante da classe [Dao1] que utiliza uma sintaxe chamada try-with-resource(resource):

1
2
3
4
try(resource){
...
}
...
  • [resource] é um recurso que implementa a interface [java.lang.AutoCloseable]. Todos os recursos liberados com o método [close] fazem parte dela. Essa sintaxe garante que, na linha 4, o recurso [resource] seja fechado. Isso evita a necessidade de escrever uma cláusula [finally] para realizar essa tarefa de fechamento;

Tomemos como exemplo o método [getAllProduits] da classe [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // possível exceção
        DaoException daoException = null;
        // lista de produtos
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // início da transação
            connexion.setAutoCommit(false);
            // no modo somente leitura
            connexion.setReadOnly(true);
            // a tabela [PRODUITS] está sendo lida
            try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
                    }
                }
                // fim da transação
                connexion.commit();
                // retorno ao modo padrão
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // a transação é cancelada
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // tratando a exceção
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // exceção?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return produits;
}
  • linha 7: try com o recurso [Connection]. Na linha 27, temos a certeza de que ele está fechado;
  • linha 13: bloco `try` com o recurso [PreparedStatement]. Na linha 23, tem-se a garantia de que este está fechado;
  • linha 14: instrução try com o recurso [ResultSet]. Na linha 19, tem-se a garantia de que este está fechado;
  • linha 25: a transação é cancelada da seguinte forma:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // geração da exceção
        return new DaoException(code, e1, simpleClassName);
}

No final, temos um código mais fácil de ler.

3.6.9. Implementação da camada de testes

3.6.9.1. As classes de teste

  
  • o teste [JUnitTestDao1] é um teste JUnit da classe [Dao1];
  • o teste [JUnitTestDao2] é um teste JUnit da classe [Dao2];
  • [AbstractJUnitTestDao] é a classe pai das duas classes de teste anteriores;
  • [MainTestDao1] é uma classe de console de teste da classe [Dao1];
  • [MainTestDao2] é uma classe de console de teste da classe [Dao2];
  • [AbstractMainTestDao] é a classe pai das duas classes anteriores. Ela herda o código das classes de console [IntroJdbc01, IntroJdbc02] já apresentadas; portanto, não analisaremos essas classes de console;

A classe [JUnitTestDao1] é a seguinte:


package spring.jdbc.tests;

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

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {

    // camada [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;

    @Override
    IDao getDao() {
        return dao;
    }

}
  • as anotações das linhas 12-13 foram apresentadas no parágrafo 2.5.5. Elas permitem que um teste Junit tenha acesso simples ao contexto Spring e seus beans. Esse contexto é configurado pela classe [AppConfig] (linha 12), analisada no parágrafo 2.4.3;
  • linha 14: a classe estende a classe [AbstractJUnitTestDao], que apresentaremos a seguir. É nessa classe que se encontram os métodos de teste JUnit;
  • linhas 17-19: o bean denominado [dao1] (linha 18) é injetado (linha 17). Portanto, é uma instância da classe [Dao1] que é injetada aqui;
  • linhas 21-24: o método [getDao] redefine o método de mesmo nome na classe pai;

No final das contas, o objetivo dessa classe é devolver à classe pai uma referência à camada [DAO] que deve ser testada, neste caso, uma instância de [Dao1]. Da mesma forma, a classe [JUnitTestDao2] retorna à classe pai [AbstractJUnitTestDao] uma instância da classe [Dao2].

A classe [AbstractJUnitTestDao] é uma classe de testes da JUnit:


package spring.jdbc.tests;

import generic.jdbc.entities.dbproduits.Produit;

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

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;

import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;

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

public abstract class AbstractJUnitTestDao {

    // camada [DAO]
    abstract IDao getDao();

    // mapeamento jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();

    @Before
    public void clean() {
        // limpa-se a base antes de cada teste
        log("Vidage de la base de données", 1);
        getDao().deleteAllProduits();
    }

    @Test
    public void getProduits() throws JsonProcessingException {
    ...
    }

    @Test
    public void getProduitBy() {
    ...
    }

    @Test
    public void doInsertsInTransaction() {
...
    }

    @Test
    public void updateProduits() {
    ...
    }

    @Test
    public void deleteProduits() {
    ....
    }

    @Test
    public void perf1() {
        ...
    }

    @Test
    public void perf2() {
    ...
    }

    @Test
    public void perf3() {
    ....
    }

    // -------------- métodos privados
...
}
  • linha 19, a classe [AbstractJUnitTestDao] é abstrata;
  • linha 22: o método abstrato [getDao], que permite obter a referência à camada [DAO] a ser testada. Esse método é implementado pelas classes filhas;
  • linha 25: um mapeador jSON que nos permitirá exibir no console o valor jSON dos produtos;
  • linhas 27-32: antes de cada teste (linha 27), a tabela [PRODUITS] é esvaziada;

3.6.9.2. O método privado [fill]

O método privado [fill] é utilizado para inserir produtos na tabela [PRODUITS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // cria-se uma lista de produtos
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nome, int categoria, double preço, String descrição
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // armazenamos na base de dados — recuperamos os produtos por meio de sua chave primária
        produits = getDao().addProduits(produits);
        // cria-se um dicionário de produtos para poder localizá-los mais facilmente
        // a chave do dicionário é a chave primária do produto no banco de dados
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // retornamos os produtos
        return produits;
    }
  • linha 1: o método [fill] insere [nbProduits] na tabela [PRODUITS], que se supõe vazia;
  • linhas 3-10: criação de uma lista de produtos no formato:

new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));

que utiliza o construtor Produto(int id, String nome, int categoria, double preço, String descrição). O valor do primeiro parâmetro [id] (chave primária da tabela [PRODUITS]) não tem importância, uma vez que o método [addProduits] da linha 10 não o insere no banco de dados e permite que o SGBD gere seu valor;

  • linha 12: a lista de produtos é gravada no banco de dados. Cada um dos produtos dessa lista recebe uma nova chave primária [id]. O método [addProduits] retorna como resultado seu parâmetro [produits]. Portanto, não seria necessário recuperar o resultado;
  • linhas 15-17: colocamos os produtos em um dicionário:

    // dicionário de produtos
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

A chave do dicionário é a chave primária do produto e o valor associado é o próprio produto;

  • linha 19: retorna-se a lista de produtos;

3.6.9.3. O teste [getProduits]

Este é o seguinte:


    @Test
    public void getProduits() throws JsonProcessingException {
        // preenchimento
        fill(10);
        // lista de produtos
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // verifica-se se a lista recuperada e a lista armazenada são as mesmas
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // todos os produtos iniciais devem ter sido removidos do dicionário
        Assert.assertEquals(0, mapProduits.size());
}
}
  • linha 4: 10 produtos são inseridos no banco de dados;
  • linha 7: feito isso, solicitamos a exibição de todos os produtos do banco de dados;
  • linha 8: eles são exibidos. O objetivo é verificar se os produtos foram efetivamente registrados e se possuem uma chave primária;
  • linhas 10-13: verifica-se se os produtos encontrados são idênticos aos que foram gravados e que podem ser encontrados no dicionário [mapProduits];
  • linha 11: recupera-se do dicionário o produto com a mesma chave primária que aquele trazido do banco de dados. Isso demonstra que os produtos armazenados efetivamente receberam uma chave primária;
  • linha 12: verifica-se se os dois produtos são idênticos. Vale lembrar que a classe [Produit] definiu um método [equals] (ver parágrafo 3.3.4);
  • linha 13: remove-se do dicionário o elemento encontrado;
  • linha 16: verifica-se se o dicionário dos produtos iniciais está realmente vazio, o que significa que todos esses produtos iniciais estavam presentes na lista de produtos recuperados do banco de dados;

O método [affiche] da linha 8 é o seguinte método privado:


    // exibição da lista de produtos
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • linha 2: o método [affiche] é um método genérico. Ele é parametrizado por um tipo T, sintaxicamente denotado como <T>. Se fosse parametrizado por dois tipos, T1 e T2, escrever-se-ia <T1,T2>. A sintaxe de um método m parametrizado por um tipo T é a seguinte:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

No código do método m, encontraremos dados do tipo T. O método m de uma instância c de uma classe C pode, então, ser chamado da seguinte maneira:

type_résultat r=c.<T1>m(..., T1 value1, ..) ;

onde T1 é o tipo efetivo que substituirá o tipo formal T do método m. Na maioria das vezes, o compilador é capaz de deduzir o tipo T1 a partir dos argumentos do método m. Portanto, a instrução anterior será, na maioria das vezes, simplificada para:

type_résultat r=c.m(..., T1 value1, ..) ;

Voltemos ao método [affiche]. Ele exibe uma lista de elementos do tipo T. Isso é possível porque o mapeador jSON utilizado na linha 4 é capaz de gerar a representação jSON de qualquer tipo de objeto. Neste exemplo específico, o único tipo T utilizado será o tipo [Produit].

O método [affiche] também poderia ter sido escrito da seguinte maneira:


    // exibição da lista de produtos
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Como o parâmetro efetivo é uma lista de produtos, a linha 3 teria gerado a representação jSON dessa lista. Isso não é o mesmo que gerar, uma a uma, a representação de cada um de seus elementos.

A exibição gerada pelo teste [getProduits] é a seguinte:

-- Liste des produits
{"id":150189,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":150190,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":150191,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":150192,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":150193,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":150194,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":150195,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":150196,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":150197,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":150198,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}

3.6.9.4. O teste [getProduitBy]

Este é o seguinte:


    @Test
    public void getProduitBy() {
        // preenchimento
        fill(10);
        log("getProduitBy", 1);
        Produit produit = getDao().getProduitByName("NOM3");
        Produit produit2 = getDao().getProduitById(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
}
  • linha 6: o método [getProduitByName] da interface [IDao] é utilizado para retornar o produto com o nome [NOM3];
  • linha 7: o método [getProduitById] da interface [IDao] é então utilizado para recuperar o mesmo produto, identificado desta vez por sua chave primária;
  • linhas 8-10: verifica-se se [produit2] e [produit] possuem as mesmas características;

3.6.9.5. O teste [doInsertsInTransaction]

Este é o seguinte:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // realiza-se a inserção
        List<Produit> inserts = new ArrayList<Produit>();
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        boolean erreur = false;
        try {
            getDao().addProduits(inserts);
        } catch (DaoException daoException) {
            erreur = true;
        }
        // verificações
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • linhas 5-7: cria-se uma lista com dois produtos com o mesmo nome, [x];
  • linha 10: esses dois produtos são inseridos na tabela [PRODUITS], que está vazia (método [clean] anotado com [@Before]). A primeira inserção será realizada, mas não a segunda, pois a tabela [PRODUITS] possui uma restrição de exclusividade no nome dos produtos. Portanto, deve ocorrer uma exceção. Essa exceção é testada na linha 15;
  • como todos os métodos da interface [IDao] são executados dentro de uma transação, o fato de a segunda inserção falhar fará com que toda a transação seja cancelada, incluindo, portanto, a primeira inserção. No final, nenhuma inserção deve ocorrer na tabela [PRODUITS];
  • linhas 16-17: verifica-se esse ponto solicitando a lista de produtos contidos na tabela [PRODUITS] e confirmando que essa lista está vazia;

3.6.9.6. O teste [updateProduits]

Este é o seguinte:


    @Test
    public void updateProduits() {
        // preenchimento
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // recuperação dos produtos
        List<Produit> produits = getDao().getAllProduits();
        // atualização dos da categoria 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nome, int categoria, double preço, String descrição
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // verificações
        // Assert.assertEquals(nbUpdated, nbProduits); -- não funciona com DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • linha 4: insere-se 10 produtos no banco de dados;
  • linha 7: recuperam-se esses produtos;
  • linhas 9 a 18: aumenta-se em 10% os preços dos produtos da categoria nº 1;
  • linha 19: essas alterações são gravadas no banco de dados;
  • linhas 22-25: percorre-se na memória a lista de produtos que serviu para a atualização. Para cada um deles, busca-se no banco de dados o produto com a mesma chave primária e verifica-se se a atualização do preço ocorreu corretamente;
  • linha 19: recupera-se o número de produtos atualizados pela operação [updateProduits];
  • linha 21: verifica-se se esse número é realmente o esperado. Esse teste é aprovado para todos os SGBD, exceto para o SGBD e o DB2. Portanto, colocamos esses dois em comentários;

3.6.9.7. O teste [deleteProduits]

Este é o seguinte:


    @Test
    public void deleteProduits() {
        // preenchimento
        fill(10);
        log("deleteProduits", 1);
        // lista de produtos
        List<Produit> produits = getDao().getAllProduits();
        // exclusão de dois produtos
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // verificações
        // Assert.assertEquals(2, nbDeleted); -- não funciona com DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • linha 4: inserimos 10 produtos no banco de dados;
  • linhas 7-11: recuperam-se todos os produtos do banco de dados e excluem-se dele os produtos recuperados nas posições 0 e 5;
  • linhas 14-16: verifica-se se os dois produtos não estão mais no banco de dados e se este tem dois produtos a menos;
  • o teste da linha 13 falha com o SGBD DB2. Ele é aprovado com os outros SGBD;

3.6.9.8. Testes de desempenho

Incluímos nos testes três métodos cujo único objetivo é avaliar o desempenho do SGBD:


    @Test
    public void perf1() {
        // preenchimento
        fill(10000);
    }

    @Test
    public void perf2() {
        // preenchimento
        fill(10000);
        // alteração
        List<Produit> produits = getDao().getAllProduits();
        // atualizamos os da categoria 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nome, int categoria, double preço, String descrição
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // preenchimento
        fill(10000);
        // exclusão
        List<Produit> produits = getDao().getAllProduits();
        // chaves primárias
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • linhas 1-5: inserção de 10.000 produtos;
  • linhas 8-20: inserção de 10.000 produtos e, em seguida, modificação dos mesmos por meio de suas chaves primárias;
  • linhas 23-34: inserção de 10.000 produtos e, em seguida, exclusão dos mesmos por meio de suas chaves primárias;

Para executar os testes [JUnitTestDao1] e [JUnitTestDao2], é possível utilizar as seguintes configurações de execução:

Os resultados do teste [JUnitTestDao1] são os seguintes:

No [1], os resultados correspondem aos do [JUnitTestDao1], e no [2], aos do [JUnitTestDao2]. Não há diferenças significativas entre eles. No [1]:

  • o teste foi bem-sucedido;
  • a inserção de 10.000 produtos leva 3,15 segundos;
  • a inserção de 10.000 produtos seguida de sua modificação leva 4,80 segundos;
  • a inserção de 10.000 produtos seguida de sua exclusão leva 4,40 segundos;
  • portanto, a operação mais demorada é a inserção;