Skip to content

3. Introdução à API JDBC

3.1. Configurar o ambiente de trabalho

Iremos trabalhar com uma base de dados MySQL5.

É necessário ter:

Partimos do princípio de que o administrador do MySQL5 é o utilizador root com a palavra-passe root. Inicie o SGBD MySQL5 e o seu cliente [MyManager]. Utilizando o [MyManager], criamos a base de dados [dbproduits] [1-34]:

  • em [3], a base de dados deve ser denominada [dbproduits];
  • em [8-9], inicie sessão como root com a palavra-passe de root (que não aparece na captura de ecrã acima);
  • em [14a], a palavra-passe é novamente root (o que a captura de ecrã não mostra);
  • em [15], a base de dados [dbproduits] foi criada;
  • em [20], preste atenção à base de dados selecionada. Deve ser a base de dados [dbproduits];
  • em [22], a pasta é <examples>/spring-database-config/mysql/databases, onde <examples> é a pasta que contém os exemplos descarregados;
  • em [23], selecione o script SQL [dbproduits.sql]. Isto irá gerar a tabela [PRODUITS] na base de dados [dbproduits];
  • em [30], a tabela [produtos] foi criada;
  • em [33], as colunas da tabela [produtos];
  • em [34], está inicialmente vazia;

Agora, utilizando o STS, importe os seguintes projetos (siga o mesmo procedimento utilizado para os projetos na pasta <examples>/spring-core):

  • em [2], o projeto [mysql-config-jdbc] encontra-se na pasta [<examples>/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:

  • Em [2], os projetos estarão na pasta [<examples>/spring-database-config/spring-jdbc] [1];

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


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

O mesmo artefacto será gerado pelos projetos [oracle-config-jdbc, db2-config-jdbc, ...]. Para garantir que os projetos [spring-generic-jdbc-*] atualmente carregados no STS estão de facto a utilizar o projeto [mysql-config-jdbc]:

  • Certifique-se de que nenhum outro projeto [sgbd-config-jdbc] está carregado ao mesmo tempo. Isso pode causar erros difíceis de compreender;
  • Atualize a configuração do Maven dos projetos carregados da seguinte forma:

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

Deve ver o seguinte resultado na consola:

------------------------------ 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 pode:

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

3.2. Passos para operar uma base de dados

Na arquitetura acima, a operação de uma base de dados através do programa de consola envolve os seguintes passos:

  1. carregar o controlador JDBC da base de dados;
  2. abrir uma ligação à base de dados;
  3. executar uma consulta SQL no banco de dados e processar os resultados da consulta SQL;
  4. fechar a ligação;

O Passo 1 é executado apenas uma vez. Os Passos 2 a 4 são executados repetidamente. Note-se que as ligações não ficam abertas; são encerradas assim que deixam de ser necessárias.

3.2.1. Passo 1 - Carregar o controlador JDBC na memória

O código


        // driver loading JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
            // handle the exception
}

O objetivo da operação na linha 3 é carregar o controlador JDBC da base de dados na memória. Esta operação só precisa de ser realizada uma vez. No entanto, repeti-la não causa um erro. A classe do controlador JDBC é procurada no caminho de classes do projeto. Portanto, no projeto Eclipse, o ficheiro [jar] que contém a classe do controlador JDBC deve ter sido incluído no caminho de classes do projeto.

3.2.2. Passo 2 - Abrir uma ligação

Assim que o controlador JDBC estiver instalado, solicitamos que ele abra uma ligação à base de dados:

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 {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // we handle the exception
            ...
        } finally {
         // close connection
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // handle the exception
                ...
            }
         }
}
  • Linhas 3–7: As classes que implementam a interface JDBC estão todas no pacote [java.sql]. Além disso, em caso de erro, todas lançam uma [SQLException] (linhas 19, 27). Esta exceção deriva da classe [Exception] e é uma chamada exceção verificada: deve utilizar um bloco try/catch para a tratar ou, em alternativa, optar por não a tratar e indicar que o método permite que a exceção se propague, adicionando [throws SQLException] à assinatura do método;
  • na linha 17, [DriverManager.getConnection] é um método estático que espera três parâmetros:
    • [url]: o URL da base de dados. Trata-se de uma cadeia de caracteres que depende da base de dados utilizada. Para o MySQL, tem o formato [jdbc:mysql://localhost:3306/db_name];
    • [user]: o proprietário da ligação;
    • [passwd]: a palavra-passe do utilizador;
  • linhas 24–30: a ligação deve ser encerrada na cláusula [finally] para que seja encerrada independentemente de ocorrer ou não uma exceção.

3.2.3. Passo 3 - Executar instruções SQL [SELECT]

Assim que a ligação for estabelecida, os comandos SQL podem ser executados. A forma como os comandos de leitura [SELECT] são tratados difere daquela utilizada para operações de atualização [UPDATE, INSERT, DELETE]. Começaremos pelos comandos SQL [SELECT]:

O código


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
             doCatchException(connexion,e1);
        } finally {
            // we treat the 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 de leitura apenas (linha 10). Uma transação é uma sequência de instruções SQL que ou todas são bem-sucedidas ou todas falham. Assim, numa transação que contenha N instruções SQL, se a (I+1)ª instrução falhar, então as I instruções anteriores serão revertidas. Para uma operação de leitura, não é necessária uma transação. No entanto, a criação de uma transação apenas de leitura pode permitir que certos DBMS realizem determinadas otimizações;
  • Linha 12: utilização de um [PreparedStatement]. Um [PreparedStatement] tem normalmente parâmetros indicados pelo caractere ?. Aqui, não tem nenhum. Um [PreparedStatement] é uma instrução preparada pelo SGBD. Esta preparação tem um custo e é realizada apenas uma vez. Esta instrução preparada é então executada pelo SGBD com parâmetros reais que substituem os parâmetros de espaço reservado ?. Note-se que é preferível especificar as colunas desejadas em vez de utilizar a notação * para recuperar todas as colunas. Ao especificar os nomes das colunas, os seus valores podem então ser recuperados com base na sua posição na instrução SELECT;
  • Linha 13: execução do [PreparedStatement]. É recuperado um objeto [ResultSet];

Um objeto [ResultSet] representa uma tabela, ou seja, um conjunto de linhas e colunas. Em qualquer momento, temos acesso a apenas uma linha da tabela, chamada de linha atual. Quando o [ResultSet] é criado inicialmente, não há linha atual. Temos de realizar uma operação [ResultSet.next()] para a obter. A assinatura do método next é a seguinte:

    boolean next()

Este método tenta avançar para a linha seguinte do [ResultSet] e devolve true se for bem-sucedido, false caso contrário. Se for bem-sucedido, a linha seguinte torna-se a nova linha atual. A linha anterior é perdida e não pode ser recuperada.

A tabela [ResultSet] possui colunas denominadas labelCol1, labelCol2, ... conforme especificado 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] irá para uma coluna no [ResultSet] denominada [myId];
  • a coluna [NAME] irá para uma coluna no [ResultSet] chamada [myName];
  • ...

No exemplo acima, os identificadores [myCol] são chamados de rótulos de coluna. Sem esses rótulos, os nomes das colunas do [ResultSet] dependem do SGBD. Quando o [SELECT] opera numa única tabela, os rótulos de coluna serão, por predefinição, os nomes das colunas solicitadas pelo SELECT. O problema surge quando o [SELECT] opera em várias tabelas e essas tabelas contêm nomes de colunas idênticos, como no exemplo seguinte:

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

assumindo que a tabela [PRODUCTS] tem uma chave estrangeira para a tabela [CATEGORIES] representada pela relação [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID, e que ambas as tabelas [PRODUCTS] e [CATEGORIES] têm um campo [NAME]. Neste caso, os nomes atribuídos no [ResultSet] às colunas [PRODUITS.NOM] e [CATEGORIES.NOM] dependem do SGBD. Para garantir a portabilidade entre SGBDs, devem ser utilizados aqui os rótulos das colunas, pelo que escreveríamos:


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

Para aceder aos vários campos da linha atual no [ResultSet], estão disponíveis os seguintes métodos:

Type getType("labelColi") 

para recuperar a coluna denominada "labelColi" da linha atual, ou seja, a coluna na instrução [SELECT] com essa etiqueta. Type refere-se ao tipo de dados do campo "labelColi". Podem ser utilizados os seguintes métodos [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Em vez de utilizar o nome da coluna, pode utilizar a sua posição na consulta [SELECT] executada:

Type getType(i) 

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

  • linhas 15–17: recuperação dos valores lidos da base de dados;
  • linha 19: a transação é validada (também conhecida como confirmada). Isto encerra-a e liberta os recursos que o SGBD tinha alocado para ela;
  • linha 25: os recursos são libertados no bloco [finally]. Isto chama o seguinte método [doFinally]:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                // handle the exception
            }
        }
    }
  • linhas 3-9: fechar o [ResultSet];
  • linhas 11–17: fechar o [PreparedStatement];
  • linhas 18–27: fechar a ligação;

Os encerramentos nas linhas 3–17 parecem redundantes, uma vez que a ligação é encerrada nas linhas 18–25. Na verdade, em alguns casos, não são redundantes, e recomenda-se mantê-los [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • Linha 22: A exceção é tratada pelo seguinte método [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // handle the exception
        }
}
  • linhas 4–6: a transação é revertida. Isto encerra-a, e o SGBD pode libertar os recursos que lhe foram atribuídos;

3.2.4. Passo 3 - Emissão de instruções SQL [INSERT, UPDATE, DELETE]

As instruções SQL [INSERT, UPDATE, DELETE] são operações de atualização: modificam a base de dados, mas não devolvem quaisquer linhas. A única informação devolvida é o número de linhas afetadas pela operação de atualização.

O código


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture / écriture
            connexion.setReadOnly(false);
            // on met à jour la table
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // catégorie 1
            ps.setInt(1, 10);
            // exécution
            int nbLignes=ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException(connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, ps, connexion);
        }
    }
  • linha 9: a conexão é usada para leitura e gravação;
  • linha 11: um [PreparedStatement] com 1 parâmetro (representado por ?). Pode haver vários parâmetros. São numerados a partir de 1;
  • linha 13: o seu valor é atribuído 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. Pode utilizar os métodos [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • linha 15: é utilizado o método [executeUpdate], e não [executeQuery], que está reservado para instruções SELECT. O método devolve o número de linhas afetadas pela operação. Pode ser 0.
  • linha 17: a transação é confirmada;

3.2.5. Passo 4 - Fechar a ligação

Uma ligação deve ser encerrada o mais rapidamente possível num ambiente multiutilizador, porque um SGBD aceita um número limitado de ligações abertas. Nos exemplos anteriores, foi encerrada na cláusula [finally] das operações SQL, para que fosse encerrada independentemente de ter ocorrido ou não uma exceção.

3.3. Configurar a camada JDBC para o sistema de gestão de bases de dados MySQL5

Iremos examinar o projeto [mysql-config-jdbc], que configura a camada JDBC da seguinte forma:

3.3.1. O projeto Eclipse

 

3.3.2. Configuração do Maven

O ficheiro [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>
        <!-- dépendances variables ********************************************** -->
        <!-- driver JDBC from SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- dépendances constantes ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- library 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>
        <!-- Spring Boot Test -->
        <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>

Esta configuração do Maven inclui vários arquivos necessários para o projeto [mysql-config-jdbc] ou para projetos que dependem dele:

  • linhas 4–6: o artefacto Maven gerado pelo projeto. Como mencionado anteriormente, todos os projetos [*-config-jdbc] geram este mesmo artefacto. Portanto, dois projetos [*-config-jdbc] não devem ser carregados ao mesmo tempo;
  • linhas 9–13: o projeto Maven pai deste. Define as versões de um grande número de arquivos utilizados pelo ecossistema Spring. Isto evita ter de as especificar nos projetos que dele derivam;
  • linhas 18–21: o arquivo do driver JDBC para o SGBD MySQL5. Este é o único arquivo exigido pelo projeto [spring-jdbc-01];
  • linhas 24–27: o artefacto [tomcat-jdbc] fornece um arquivo exigido pelos projetos JDBC [spring-jdbc-02 a 04];
  • linhas 29–36: fornecem as bibliotecas necessárias para o tratamento de JSON. Utilizadas em quase todos os projetos deste documento;
  • linhas 38–42: o Google Guava é uma biblioteca de gestão de coleções. Utilizada em quase todos os projetos deste documento;
  • linhas 43–52: bibliotecas para escrever testes que integram Spring e JUnit. Utilizadas em quase todos os projetos deste documento;
  • linhas 54–57: bibliotecas de registo. Utilizadas em quase todos os projetos deste documento;
  • linhas 67–71: o plugin utilizado para instalar o artefacto do projeto [mysql-config-jdbc] no repositório Maven local;

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 {
 
    // paramètres de connexion
    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";
...
    // ordres 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')";
 
    // ordres 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] é utilizada para configurar a camada JDBC para os quatro projetos [spring-jdbc-01 a 04]. A maior parte da configuração diz respeito ao projeto [spring-jdbc-04]. Abordaremos esta secção quando analisarmos esse projeto. Apenas a configuração para os projetos [spring-jdbc-01 a 03] é apresentada acima.

  • linhas 14–17: parâmetros de ligação para a base de dados MySQL5 [dbproduits];
  • linhas 20–25: as instruções SQL utilizadas nos projetos [spring-jdbc-01 e 02];
  • linhas 28–34: as instruções SQL utilizadas no projeto [spring-jdbc-03];

Estas instruções SQL utilizam a tabela [PRODUCTS] na base de dados MySQL5 [dbproducts], que tem a seguinte estrutura:

 
  • [ID]: chave primária no modo AUTO_INCREMENT (se não for especificada nenhuma chave primária, o SGBD gera-a);
  • [NAME]: nome de um produto — único;
  • [CATEGORY]: número da categoria;
  • [PRICE]: o seu preço;
  • [DESCRIPTION]: uma descrição do produto;

3.3.4. A classe [Product]

  

A classe [Product] representa uma linha na tabela [PRODUCTS]:


package generic.jdbc.entities.dbproduits;
 
public class Produit {
 
    // fields
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;
 
    // manufacturers
    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 and setters
...
}

Mais tarde, precisaremos de comparar dois produtos para determinar se são iguais ou não. Diremos que dois produtos são iguais se todos os seus campos forem iguais. Para tal, vamos substituir o método [equals] da classe [Object], da qual a classe [Product] deriva:


    // méthode d'égalité
    @Override
    public boolean equals(Object o) {
        // cas 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 podemos determinar imediatamente que os dois objetos não são iguais. [Object].getClass() retorna uma instância do tipo [Class], um tipo que representa a classe real do objeto;
  • linha 8: o objeto o é convertido num produto p;
  • linha 9: se as duas referências o e p a um produto forem iguais, então elas referem-se fisicamente ao mesmo produto;
  • linha 9: se o e p são 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 existe uma representação exata de números reais na ciência da computação, consideraremos dois preços idênticos se estiverem dentro de 10⁻⁶ um do outro;

Além disso, vamos redefinir o método [hashCode] da classe [Object]:


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

Os valores hashCode de dois produtos devem ser iguais se o método [equals] tiver declarado que esses dois produtos são iguais. Este valor hashCode é utilizado para ordenar objetos em coleções, tais como dicionários. No exemplo acima, se dois produtos forem idênticos, terão, de facto, o mesmo hashCode.

3.3.5. A [UncheckedException]

  

Considere a seguinte arquitetura:

  • a camada [JDBC] lança exceções [SQLException]. Esta exceção deve propagar-se pelas camadas até chegar à camada mais elevada, neste caso a camada de testes;

A camada [DAO] poderia simplesmente deixar a [SQLException] propagar-se até à camada de testes. Mas, uma vez que esta exceção não é verificada (deriva diretamente de [Exception]), isso implicaria que a interface [IDao] da camada [DAO] seria a seguinte:


public interface IDao {
 
    // ajouter des produits
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;
 
    // liste de tous les produits
    public List<Produit> getAllProduits() throws SQLException;
 
    // un produit particulier
    public Produit getProduitById(int id) throws SQLException;
 
    public Produit getProduitByName(String name) throws SQLException;
 
    // mise à jour de plusieurs produits
    public int updateProduits(List<Produit> produits) throws SQLException;
 
    // suppression de tous les produits
    public int deleteAllProduits() throws SQLException;
 
    // suppression de plusieurs produits
    public int deleteProduits(int[] ids) throws SQLException;
}

E isso é muito incómodo porque nos impede de implementar a interface [IDao] com uma classe que lançaria uma exceção diferente. Para contornar este problema, a camada [DAO] lançará uma [DaoException] não tratada (derivada de [RuntimeException]), o que nos permite omitir a cláusula [throws] nas assinaturas dos métodos da interface. Como resultado, a interface pode ser implementada por qualquer classe que também lance uma exceção não verificada, que pode ser diferente da [DaoException]. A nossa arquitetura fica agora assim:

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

  

package generic.jdbc.infrastructure;
 
import java.util.ArrayList;
import java.util.List;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
// generic exception class
// the exception is uncontrolled
 
public class UncheckedException extends RuntimeException {
 
    // serial ID generated
    private static final long serialVersionUID = -2924871763340170310L;
 
    // properties
    private int code;
    private String trace;
    private List<ShortException> exceptions;
 
    // manufacturers
    public UncheckedException() {
        super();
    }
 
    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        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);
    }
 
    // list of exception error messages
    private List<ShortException> getErreursForException(Throwable th) {
        // retrieve the elements of the exception stack
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // retrieve the current exception
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // following exception
            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 and setters
...
}
  • linha 12: a classe estende [RuntimeException] e é, portanto, um tipo de exceção não verificada. Será utilizada para encapsular uma exceção verificada (SQLException) num tipo de exceção não verificada (UncheckedException);
  • para diferenciar entre exceções [UncheckedException], podemos atribuir-lhes um código que será armazenado no campo privado na linha 18. O código Java que intercepta uma [UncheckedException] terá acesso a este código de erro através 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 formas 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 de um tipo derivado, especificamente o tipo [Exception];
  • linhas 69–78: o método [toString] devolve uma cadeia de caracteres que representa a exceção. Para apresentar a lista de erros da linha 20, utiliza uma biblioteca JSON. Esta biblioteca está incluída nas dependências Maven do projeto:

        <!-- library 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 será exibida;

A classe [UncheckedException] armazena, no campo da linha 20, uma lista de exceções descritas pelo seguinte tipo [ShortException]:


package pam.dao.exceptions;
 
public class ShortException {
 
    // properties
    private String className;
    private String errorMessage;
 
    // manufacturers
    public ShortException() {
 
    }
 
    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }
 
    // getters and 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);
        // trace
        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;
            }
        }
}
  • na linha 1, os parâmetros são os seguintes:
    • [code]: um código de erro;
    • [e]: a exceção que está a ser 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 se pode escrever [throw c;], onde c é uma instância de C;
    • [simpleClassName]: o nome simples da classe de código do utilizador onde a exceção e foi detetada;
  • linha 4: o código de erro é registado;
  • linha 5: a lista de [ShortException] é construída a partir do [Throwable e] passado como parâmetro;
  • linhas 7–16: os chamados traços de exceção são examinados. Uma exceção inicial ocorre num ponto específico do código e depois propaga-se de volta para o método que chamou aquele onde a exceção ocorreu, e assim por diante até que um bloco try/catch a capture. Durante esta propagação, a exceção inicial deixa rastros armazenados na matriz [e.stackTrace] da exceção e. Estes são recuperados aqui na linha 8 a partir do [Throwable e] passado como parâmetro. Cada elemento do tipo [StackTraceElement] é um objeto com os seguintes campos:
    • [fileName]: o nome do ficheiro Java onde a exceção ocorreu;
    • [lineNumber]: o número da linha neste ficheiro onde a exceção ocorreu;
    • [methodName]: o nome do método neste ficheiro onde a exceção ocorreu;
  • As linhas 10–16 pesquisam a matriz de rastreios para a exceção passada como parâmetro, procurando a primeira ocorrência da condição [trace.fileName == simpleClassName.java], onde [simpleClassName] é o terceiro parâmetro do construtor. A ideia é registar onde a exceção ocorreu no código do utilizador. O código do utilizador irá encapsular uma exceção da seguinte forma:
1
2
3
4
5
6
7
try{
// code qui peut lancer une exception contrôlée
...
}catch(UnTypeDexception e){
// on encapsule l'exception contrôlée e dans une exception non contrôlée
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • linha 13: criamos uma cadeia de caracteres do tipo [nome do ficheiro, nome do método, número da linha] que descreve a localização no código do utilizador onde a exceção e foi detetada;

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


    // liste des messages d'erreur d'une exception
    private List<ShortException> getErreursForException(Throwable th) {
        // on récupère les éléments de la pile de l'exception
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // on récupère l'exception courante
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // exception suivante
            cause = cause.getCause();
        }
        return exceptions;
}

À medida que se propaga de volta para o método que a capturou usando um bloco try/catch, a exceção inicial e pode ter sido encapsulada dentro de outra exceção. É então esta última exceção que se propaga de volta para o método que a irá finalmente capturar. Por conseguinte, também esta pode ser encapsulada. Em última análise, quando um método decide capturar uma exceção th e tratá-la, irá 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. O seu atributo [th.cause] revela a exceção que ele próprio encapsula. E assim por diante. Quando uma exceção e satisfaz [e.getCause()==null], significa que e é a exceção inicial.

  • Linha 8: Para cada exceção na 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. Exemplo-01

3.4.1. Arquitetura do projeto

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

3.4.2. O projeto Eclipse

Criamos um projeto Spring/Maven [spring-jdbc-01] seguindo o procedimento descrito na Secção 2.5.2.1.

  

O projeto é um projeto Maven definido pelo seguinte ficheiro [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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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 artefacto [generic-config-jdbc] do projeto [mysql-config-jdbc] que acabámos de analisar. O projeto [spring-jdbc-01] tem, portanto, acesso a todos os elementos do projeto [mysql-config-jdbc];

Podemos verificar este último ponto de duas formas, analisando as dependências Maven do projeto:

  • Em [2], vemos que o projeto [mysql-config-jdbc] está listado nas dependências Maven do projeto. Uma vez que estas dependências se encontram no Classpath do projeto, isto significa que o projeto [mysql-config-jdbc] também se encontra neste Classpath e, por conseguinte, as suas classes e interfaces são visíveis no projeto [spring-jdbc-01];

O projeto Maven [mysql-config-jdbc] não precisa de estar presente no separador [Package Explorer] para poder ser utilizado por outros projetos Maven. Basta que esteja presente no repositório Maven local. Ao contrário de um IDE como o NetBeans, isto não é automático no Eclipse. Tem de ser forçado:

Analisámos as condições que permitem esta geração na Secção 2.3.5. Depois de concluída, pode remover o projeto [mysql-config-jdbc] da guia [Package Explorer]:

  • Não marque [3], pois isso elimina fisicamente o projeto do disco, tornando-o irrecuperável;

Esta operação recalcula as dependências Maven dos projetos que dependem do projeto removido do [Package Explorer]. Isto altera o ramo [Maven Dependencies] desses projetos. Por exemplo, para o projeto [spring-jdbc-01], o ramo [Maven Dependencies] fica da seguinte forma:

Desta vez, a dependência já não é de um projeto, mas do seu artefacto Maven, neste caso o artefacto [generic-config-jdbc] [1]. Podemos ver que temos, de facto, acesso a todas as classes e interfaces deste artefacto. Conforme mencionado, este artefacto será gerado por todos os projetos [*-config-jdbc]. Para evitar erros, nós:

  • mantemos sempre um único projeto [*-config-jdbc] no separador [Package Explorer];
  • atualizamos a configuração do Maven de todos os projetos na guia [Package Explorer] (Alt-F5) para que incluam o projeto [*-config-jdbc] nas suas dependências do Maven;

3.4.3. O esqueleto da classe principal

  

O esqueleto 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 {
 
    // constants
    final static ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) {
        // loading the JDBC driver from SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // fill it
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // we read it
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // update
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // display
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // empty table [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // we display it
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion of two identical elements
        // the INSERTion must fail and neither element is inserted because of the transaction
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // we check
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // finish
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }
 
    // product list
    private static void select() {
    ...
    }
 
    // display jSON of an object
    private static void affiche(Object object) {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }

    // add 2 products with the same primary keys
    public static void insert2() {
...
    }
 
    // product updates
    public static void update() {
...
    }
 
    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                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) {
        // display error msg
        show(title, getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }
 
    private static List<String> getErreursFromThrowable(Throwable th) {
        // retrieve the list of exception error msgs
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // throwable error message
            erreurs.add(th.getMessage());
            // we move on to the cause of throwable
            th = th.getCause();
        }
        // result
        return erreurs;
    }
 
    private static void show(String title, List<String> messages) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • linhas 23–29: carregamento do controlador JDBC para o SGBD. Na linha 25, é utilizada a constante [ConfigJdbc.DRIVER_CLASSNAME] definida no projeto [mysql-config-jdbc];
  • linhas 136–147: o método [getErrorsFromThrowable] devolve a lista de mensagens de erro encapsuladas num objeto do tipo [Throwable], que é a classe pai da classe [Exception]. Uma exceção pode conter outra exceção, que pode ser recuperada utilizando o método [Throwable].getCause(). Isto permite-nos percorrer 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 connection, Throwable th)] trata as exceções encontradas pelos métodos da classe. A exceção tratada é representada pelo parâmetro [Throwable th]. O objetivo do método é:
    • reverter a transação atual do objeto [Connection connection] (linhas 127–129);
    • escrever 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 connection)] trata do bloco [finally] dos métodos de acesso ao SGBD. O seu objetivo é libertar os recursos alocados pela ligação;

3.4.4. Eliminar o conteúdo da tabela de produtos

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


    // product deletion
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // we treat the 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 instrução 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 instruções SQL que devem ser todas bem-sucedidas ou todas revertidas. Há quatro operações a ter em conta:

  • início de uma transação: [connection.setAutoCommit(false)];
  • fim de uma transação bem-sucedida: [connection.commit()]. Neste caso, todas as operações realizadas na base de dados durante a transação são confirmadas;
  • fim de uma transação com falha: [connection.rollback()]. Neste caso, todas as operações realizadas na base de dados durante a transação são revertidas;

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


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // display error msg
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            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 {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // fill the table
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // preparation
                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));
                // execution
                ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, ps, connexion);
        }
    }

Na linha 12, a instrução 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:


// product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
    }

Na linha 14, a instrução SQL preparada é a seguinte:


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

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


    // display jSON of an object
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Exibe a representação JSON do objeto passado como parâmetro (ver JSON na Secção 23.12).

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

O método [update] atualiza determinados produtos:


    // product updates
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // table is updated
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, ps, connexion);
        }
}

Na linha 13, a instrução SQL preparada é a seguinte:


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

3.4.8. Papel 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 numa transação, a primeira inserção será revertida.


    // add 2 products with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // add 1 line
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the INSERTion must fail and neither element must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, ps, connexion);
        }
}

Na linha 13, a instrução 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]:

 

O resultado na consola é o seguinte:


------------------------------ 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 de inserir os dois produtos com a mesma chave primária, a tabela está vazia;
  • linha 35: após inserir os dois produtos com a mesma chave primária, a tabela está vazia. Isto demonstra o papel da transação:
    • a primeira inserção é bem-sucedida. Não há motivo para que falhe;
    • a segunda inserção falha (linha 32). Consequentemente, como estas duas inserções estão dentro da mesma transação, todas as instruções SQL nessa transação são revertidas, incluindo a primeira inserção.

3.4.10. Conclusão

O que chama a atenção nos trechos de código anteriores é a quantidade significativa de espaço dedicada ao tratamento da [SQLException]. Uma vez que qualquer operação JDBC pode potencialmente lançar esta exceção, existem inúmeros blocos try/catch no código.

3.5. Exemplo-02

Vamos revisitar a aplicação anterior utilizando uma fonte de dados [javax.sql.DataSource]:

Image

Iremos utilizar uma fonte de dados implementada pela classe [org.apache.tomcat.jdbc.pool.DataSource]. Esta classe utiliza um conjunto de ligações, ou seja, um conjunto de ligações abertas:

  • Quando o pool é instanciado, é aberto um determinado número de ligações à base de dados. Este número é configurável;
  • quando o código Java abre uma ligação, esta é fornecida pelo conjunto;
  • quando o código Java fecha uma ligação, esta é devolvida ao conjunto;

Em última análise, as ligações são abertas apenas uma vez, o que melhora o desempenho do acesso à base de dados. A fonte de dados será definida numa classe de configuração Spring

3.5.1. Arquitetura do projeto

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

3.5.2. O projeto Eclipse

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

Em seguida, desenvolvemos o projeto de [6] para [7]:

3.5.3. Configuração do Maven

O projeto [7] é um projeto Maven definido pelo seguinte ficheiro [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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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 no projeto [mysql-config-jdbc];

É o projeto [mysql-config-jdbc] que inclui nas suas dependências do Maven a biblioteca que fornece uma implementação de uma fonte de dados [javax.sql.DataSource] (ver secção 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 {
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
}
  • linha 10: [AppConfig] é uma classe de configuração Spring;
  • linha 11: importação da classe de configuração [generic.jdbc.config.ConfigJdbc.class] definida no projeto [mysql-config-jdbc]. Isto significa que todos os beans definidos por este ficheiro de configuração estão disponíveis;
  • linhas 14–27: o bean Spring que define a fonte de dados;
  • linha 17: criação da fonte de dados, que ainda não está configurada;
  • linhas 19–22: as informações que permitem que a fonte de dados se ligue à base de dados;
  • linha 24: cria um conjunto de 5 ligações. Aqui, precisamos apenas de uma. Nunca há várias ligaçõ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 {
 
    // mapper jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // spring context retrieval
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // data source recovery
            dataSource = ctx.getBean(DataSource.class);
            // empty table [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // finish
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }
 
    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • linha 25: a fonte de dados. Note-se que é do tipo [javax.sql.DataSource] (linha 13), que é uma interface;
  • linha 31: instanciação de objetos Spring;
  • linha 32: obtenção de uma referência à fonte de dados. Note-se que a classe efetivamente utilizada nunca é mencionada. Assim, aqui, nada sugere que esteja a ser utilizada uma implementação [TomcatJdbc];
  • linha 49: obtenção de uma ligação aberta. É assim que os vários métodos em [IntroJdbc02] obtêm uma ligação à base de dados. O resto do código é idêntico ao da classe [IntroJdbc01];

3.5.6. Os testes

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

 

Obtemos os mesmos resultados que anteriormente (secção 3.4.9).

3.6. Exemplo-03

3.6.1. Arquitetura do projeto

Neste exemplo, os métodos de acesso aos dados estão isolados numa camada [dao]. Serão testados utilizando um teste JUnit.

3.6.2. O projeto Eclipse

O projeto Eclipse [spring-jdbc-03] é um projeto Spring/Maven construído como o anterior e, em seguida, complementado da seguinte forma:

 

Os vários 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 tratada [DaoException];

3.6.3. Configuração do Maven

O projeto Maven é configurado pelo seguinte ficheiro [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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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>

É idêntico ao do projeto [spring-jdbc-02]. Em particular, utiliza a dependência Maven do projeto [mysql-config-jdbc] (linhas 28–32).

3.6.4. Interface da camada [DAO]

  

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


package spring.jdbc.dao;
 
import java.util.List;
 
import spring.jdbc.entities.Produit;
 
public interface IDao {
 
    // add products
    public List<Produit> addProduits(List<Produit> produits);
 
    // list of all products
    public List<Produit> getAllProduits();

    // a special product
    public Produit getProduitById(int id);
 
    public Produit getProduitByName(String name);
 
    // several product updates
    public int updateProduits(List<Produit> produits);
 
    // removal of all products
    public int deleteAllProduits();
 
    // removal of several products
    public int deleteProduits(int[] ids);
}

3.6.5. A classe [DaoException]

A classe [DaoException] simplesmente estende a classe [UncheckedException] apresentada na secção 3.3.5:

  

package spring.jdbc.infrastructure;
 
public class DaoException extends UncheckedException {
 
    private static final long serialVersionUID = 1L;
 
    // manufacturers
    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 ficheiro de configuração Spring no exemplo [spring-jdbc-02], exceto na 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 {
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
}
  • linha 11: o pacote [spring.jdbc.dao] será analisado para encontrar outros componentes Spring além dos definidos neste ficheiro de configuração;

3.6.7. Implementação da camada [DAO]

  

Recorde-se (Secção 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 {
 
    // add products
    public List<Produit> addProduits(List<Produit> produits);
 
    // list of all products
    public List<Produit> getAllProduits();
 
    // a special product
    public Produit getProduitById(int id);
 
    public Produit getProduitByName(String name);
 
    // several product updates
    public int updateProduits(List<Produit> produits);
 
    // removal of all products
    public int deleteAllProduits();
 
    // removal of several products
    public int deleteProduits(int[] ids);
}

As classes [Dao1] e [Dao2] implementam ambas esta interface. A classe [Dao2] é uma variante da classe [Dao1] que introduz uma nova funcionalidade de sintaxe. Vamos concentrar-nos na classe [Dao1]. A 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 {
 
    // class name
    private String simpleClassName = getClass().getSimpleName();
    // data source
    @Autowired
    protected DataSource dataSource;
 
    // manufacturer
    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) {
...
    }
 
    // ---------------------------------------- local methods
    // management finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }
 
    // catch management
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • linha 20: a classe [Dao] é um componente Spring denominado [dao1]. Este nome é opcional. Quando não está presente, o nome utilizado é o nome da classe com a primeira letra em minúscula;
  • linha 24: o nome da classe. Evitamos codificar [Dao] de forma rígida para permitir que a classe seja renomeada sem ter de redefinir este campo, que assim permanece 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: tratamento centralizado dos blocos catch para os vários métodos;
  • linhas 72–75: tratamento centralizado dos blocos `finally` para os vários métodos;

Os blocos catch dos vários métodos são tratados da seguinte forma:


    // gestion catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // annulation transaction
        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 derivadas o utilizem sem que seja público. Aceita os seguintes parâmetros:
    • [Connection connection]: a ligação ao SGBD — pode ser nula;
    • [Throwable th]: a exceção que ocorreu e que será encapsulada num tipo [DaoException];
    • [int code]: um código de erro a ser utilizado se o método criar uma nova [DaoException];
  • linhas 4–7: a principal função deste método é reverter a transação associada à ligação passada como parâmetro 1;
  • linhas 8–10: se o rollback da transação falhar, o rastreio da exceção é gravado na consola. Não há muito mais que possamos fazer, uma vez que vamos lançar uma exceção na linha 12;

Os blocos *finally* dos vários métodos são tratados da seguinte forma:


// management finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        // close connection
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // record the error if possible
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // result
        return daoException;
    }
  • linha 2: este método também é declarado como [protected]. Aceita os seguintes parâmetros:
    • [ResultSet rs]: o [ResultSet] caso tenha sido executada uma operação [SELECT] — pode ser nulo;
    • [PreparedStatement ps]: o [PreparedStatement] que foi executado — pode ser nulo;
    • [Connection connection]: a ligação ao SGBD — pode ser nulo;
    • [int code]: um código de erro a utilizar se o método lançar uma nova [DaoException];
    • [DaoException daoException]: a [DaoException] caso tenha ocorrido uma antes do bloco finally — pode ser nulo;
  • linhas 21–30: o objetivo principal deste método é fechar a ligação (linha 23);
  • linhas 24–29: se ocorrer uma exceção durante este encerramento, verificamos o estado do parâmetro [DaoException daoException] que nos foi passado: se [daoException == null], criamos uma nova [DaoException] com o código passado como parâmetro;
  • linha 32: a [DaoException] antiga ou nova é devolvida como resultado;

Não apresentaremos todos os métodos da classe [Dao], mas apenas alguns. São todos semelhantes.

3.6.7.1. O método [getProductById]

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


@Override
    public Produit getProduitById(int id) {
        // connection resources
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // initially no exceptions
        DaoException daoException = null;
        // the product you are looking for
        Produit produit = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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));
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // we treat the finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produit;
    }
  • linha 10: o produto a ser devolvido é definido como nulo;
  • linha 19: a instrução SQL [ConfigJdbc.V2_SELECT_PRODUCT_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, esta é utilizada para criar o produto a ser devolvido; caso contrário, o produto a ser devolvido permanece nulo;
  • linha 41: o produto é devolvido;
  • linha 8: a [DaoException] do método é inicializada como nula;
  • linha 31: o método [doCatchException] cria uma [DaoException];
  • linha 34: o parâmetro [daoException] do método [doFinally] é nulo ou a exceção criada pelo método [doCatchException]. O método [doFinally]:
    • mantém este parâmetro inalterado se fechar a ligação com sucesso;
    • mantém este parâmetro inalterado se não conseguir fechar a conexão e uma [DaoException] já tiver ocorrido anteriormente;
    • cria uma nova [DaoException] se não conseguir fechar a conexão e nenhuma [DaoException] tiver ocorrido anteriormente;
  • linhas 37–39: se a [daoException] local não for nula, então é lançada; caso contrário, o resultado solicitado é devolvido (linha 41);

3.6.7.2. O método [deleteProducts]

O método [deleteProduits] elimina os produtos cujas chaves primárias lhe são passadas como parâmetros. Retorna o número de produtos eliminados.


@Override
    public int deleteProduits(int[] ids) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        // number of products updated
        int nbProduits = 0;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // we do away with products
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // settings
                ps.setInt(1, id);
                // execution
                nbProduits += ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return nbProduits;
    }
  • Na linha 18, a instrução 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 para eliminar produtos. Podemos ver que a instrução SQL é preparada uma vez (linha 18) e executada n vezes (linhas 19–24). Esta é a vantagem do objeto [PreparedStatement];
  • linha 23: o método [PreparedStatement].executeUpdate() devolve o número de linhas afetadas pela operação de atualização;
  • linha 41: retorna o número de produtos atualizados;

3.6.7.3. O método [updateProducts]

O método [updateProduits] atualiza os produtos que lhe são passados como parâmetros na base de dados. Retorna o número de produtos atualizados.


@Override
    public int updateProduits(List<Produit> produits) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        // number of products updated
        int nbProduits = 0;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // table [PRODUITS] is updated
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // settings
                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());
                // execution
                nbProduits += ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return nbProduits;
    }
  • linha 18: a instrução 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 do produto;

3.6.7.4. O método [addProducts]

O método [addProducts] adiciona os produtos que lhe são passados como parâmetros à base de dados. Devolve esses mesmos produtos com as suas chaves primárias (antes de serem adicionados à base de dados, os produtos não têm uma chave primária).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // in read/write mode
            connexion.setReadOnly(false);
            // start of transaction
            connexion.setAutoCommit(false);
            // add elements to table [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // settings
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // order execution
                ps.executeUpdate();
                // generated primary key
                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()));
                }
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produits;
}
  • Na linha 16, a instrução SQL [ConfigJdbc.V2_INSERT_PRODUITS] é a seguinte:

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

No código acima, o comando de inserção de produtos não inclui a chave primária [ID]. Uma vez que a chave primária na base de dados MySQL possui o atributo [AUTOINCREMENT], o SGBD irá gerar uma chave primária para cada inserção. Surge então o problema de recuperar esta chave. Este é um ponto importante, pois as operações sobre os produtos são realizadas através das suas chaves primárias. Por isso, precisamos de conhecer estas chaves;

  • linhas 17–33: o ciclo de inserção de produtos;
  • linha 16: uma forma específica do método [prepareStatement]. O segundo parâmetro [generatedColumns] é uma matriz de nomes de colunas cujos valores queremos recuperar após a inserção. Na linha 16, especificámos que queríamos recuperar o valor da coluna [id]. Note-se aqui que, embora os nomes das colunas de uma tabela não sejam sensíveis a maiúsculas e minúsculas, o SGBD PostgreSQL exige que este nome esteja em minúsculas. Este é tipicamente o tipo de problema encontrado ao portar código de um SGBD para outro;
  • linha 24: inserção de uma linha na base de dados;
  • Linha 26: recupera a lista de valores das colunas especificadas na linha 16 para um [ResultSet]. Aqui, para uma única inserção, o [ResultSet] conterá uma linha, e essa linha terá uma única coluna contendo a chave primária;
  • Linha 28: Recupera a chave primária gerada pelo SGBD;
  • Linhas 29–32: Se a chave primária gerada não for obtida, é lançada uma [RuntimeException], que será encapsulada numa [DaoException] (linhas 38–40);

3.6.8. A classe [Dao2]

  

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

1
2
3
4
try(resource){
...
}
...
  • [resource] é um recurso que implementa a interface [java.lang.AutoCloseable]. Todos os recursos libertados através do método [close] enquadram-se nesta categoria. Esta sintaxe garante que, na linha 4, o [resource] será fechado. Isto evita a necessidade de escrever uma cláusula [finally] para realizar esta operação de fecho;

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


    @Override
    public List<Produit> getAllProduits() {
        // possible exception
        DaoException daoException = null;
        // product list
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
                    }
                }
                // end transaction
                connexion.commit();
                // return to default mode
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // cancel the transaction
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // we handle the exception
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produits;
}
  • Linha 7: bloco try com o recurso [Connection]. A linha 27 garante que este seja fechado;
  • linha 13: bloco try com o recurso [PreparedStatement]. A linha 23 garante que este seja fechado;
  • linha 14: bloco try com o recurso [ResultSet]. A linha 19 garante que este seja fechado;
  • Linha 25: A transação é revertida da seguinte forma:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // génération de l'exception
        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 teste

3.6.9.1. As classes de teste

  
  • o teste [JUnitTestDao1] é um teste JUnit para a classe [Dao1];
  • O teste [JUnitTestDao2] é um teste JUnit para a classe [Dao2];
  • [AbstractJUnitTestDao] é a classe pai das duas classes de teste anteriores;
  • [MainTestDao1] é uma classe de teste de consola para a classe [Dao1];
  • [MainTestDao2] é uma classe de teste de consola para a classe [Dao2];
  • [AbstractMainTestDao] é a classe pai das duas classes anteriores. Reutiliza o código das classes de consola [IntroJdbc01, IntroJdbc02] já apresentadas, pelo que não iremos examinar estas classes de consola;

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 {
 
    // layer [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;
 
    @Override
    IDao getDao() {
        return dao;
    }
 
}
  • As anotações nas linhas 12–13 foram discutidas na Secção 2.5.5. Elas permitem que um teste JUnit aceda facilmente ao contexto Spring e aos seus beans. Este contexto é configurado pela classe [AppConfig] (linha 12), discutida na Secção 2.4.3;
  • linha 14: a classe estende a classe [AbstractJUnitTestDao], que discutiremos em breve. Os métodos de teste JUnit estão localizados dentro desta classe;
  • linhas 17–19: o bean denominado [dao1] (linha 18) é injetado (linha 17). Assim, uma instância da classe [Dao1] é injetada aqui;
  • linhas 21–24: o método [getDao] substitui o método com o mesmo nome na classe pai;

Em última análise, o objetivo desta classe é fornecer à classe pai uma referência à camada [DAO] que precisa de ser testada, neste caso uma instância de [Dao1]. Da mesma forma, a classe [JUnitTestDao2] fornece à classe pai [AbstractJUnitTestDao] uma instância da classe [Dao2].

A classe [AbstractJUnitTestDao] é uma classe de teste 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 {
 
    // layer [DAO]
    abstract IDao getDao();
 
    // mapper jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        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() {
    ....
    }

    // -------------- private methods
...
}
  • linha 19: a classe [AbstractJUnitTestDao] é abstrata;
  • linha 22: o método abstrato [getDao], que fornece uma referência à camada [DAO] a ser testada. Este método é implementado pelas classes filhas;
  • linha 25: um mapeador JSON que nos permite exibir o valor JSON dos produtos na consola;
  • linhas 27–32: antes de cada teste (linha 27), a tabela [PRODUCTS] é limpa;

3.6.9.2. O método privado [fill]

O método privado [fill] é utilizado para adicionar produtos à tabela [PRODUCTS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // on crée une liste de produits
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nom, int categorie, double prix, String description
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // on la persiste en base - on récupère des produits avec leur clé primaire
        produits = getDao().addProduits(produits);
        // on crée un dictionnaire des produits pour pouvoir les retrouver + facilement
        // la clé du dictionnaire est la clé primaire du produit en base
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // on rend les produits
        return produits;
    }
  • linha 1: o método [fill] insere [nbProducts] na tabela [PRODUCTS], que se presume estar vazia;
  • linhas 3–10: criação de uma lista de produtos na forma:

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 Product(int id, String name, int category, double price, String description). O valor do primeiro parâmetro [id] (chave primária da tabela [PRODUCTS]) é irrelevante, uma vez que o método [addProducts] na linha 10 não o insere na base de dados e permite que o SGBD gere o seu valor;

  • linha 12: a lista de produtos é persistida na base de dados. A cada produto desta lista é atribuída uma nova chave primária [id]. O método [addProduits] devolve o seu parâmetro [produits]. Poderíamos, portanto, ter omitido a recuperação do resultado;
  • linhas 15–17: os produtos são colocados num dicionário:

    // dictionnaire des produits
    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: devolvemos a lista de produtos;

3.6.9.3. O teste [getProducts]

É o seguinte:


    @Test
    public void getProduits() throws JsonProcessingException {
        // remplissage
        fill(10);
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // on vérifie que la liste récupérée et celle persistée sont les mêmes
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // tous les produits initiaux doivent avoir disparu du dictionnaire
        Assert.assertEquals(0, mapProduits.size());
}
}
  • linha 4: são adicionados 10 produtos à base de dados;
  • linha 7: assim que isto estiver feito, solicitamos a visualização de todos os produtos na base de dados;
  • linha 8: exibimo-los. O objetivo é verificar se os produtos foram guardados com sucesso e se possuem uma chave primária;
  • linhas 10–13: verificamos se os produtos recuperados são idênticos aos que guardámos e se podem ser encontrados no dicionário [mapProduits];
  • linha 11: recuperamos do dicionário o produto com a mesma chave primária que a devolvida pela base de dados. Isto demonstra que aos produtos guardados foi, de facto, atribuída uma chave primária;
  • linha 12: garantimos que os dois produtos são idênticos. Recorde-se que a classe [Product] definiu um método [equals] (ver secção 3.3.4);
  • linha 13: o elemento encontrado é removido do dicionário;
  • linha 16: verificamos se o dicionário de produtos iniciais está efetivamente vazio, o que significa que esses produtos iniciais estavam todos presentes na lista de produtos recuperados da base de dados;

O método [display] na linha 8 é o seguinte método privado:


    // product list display
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • linha 2: O método [display] é um método genérico. É parametrizado por um tipo T, denotado sintaticamente como <T>. Se fosse parametrizado por dois tipos T1 e T2, escreveríamos <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 da classe C pode então ser chamado da seguinte forma:

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

onde T1 é o tipo real que substituirá o tipo formal T do método m. Na maioria das vezes, o compilador consegue inferir o tipo T1 a partir dos argumentos do método m. Por conseguinte, a instrução anterior será, na maioria das vezes, simplificada para:

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

Voltemos ao método [display]. Este apresenta uma lista de elementos do tipo T. Isto é possível porque o mapeador JSON utilizado na linha 4 é capaz de renderizar a representação JSON de qualquer tipo de objeto. Neste exemplo específico, o único tipo T utilizado será o tipo [Product].

O método [display] também poderia ter sido escrito da seguinte forma:


    // product list display
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Uma vez que o parâmetro real é uma lista de produtos, a linha 3 teria impresso a representação JSON dessa lista. Isto não é o mesmo que imprimir a representação de cada um dos seus elementos, um por um.

A saída produzida pelo teste [getProducts] é 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 [getProductBy]

É o seguinte:


    @Test
    public void getProduitBy() {
        // remplissage
        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 [getProductByName] da interface [IDao] é utilizado para recuperar o produto denominado [NAME3];
  • linha 7: o método [getProductById] da interface [IDao] é então utilizado para recuperar o mesmo produto, desta vez identificado pela sua chave primária;
  • linhas 8–10: verificamos se [product2] e [product] têm 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);
        // on fait l'insertion
        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;
        }
        // vérifications
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • linhas 5-7: criamos uma lista de dois produtos com o mesmo nome [x];
  • linha 10: estes dois produtos são inseridos na tabela [PRODUCTS], que está vazia (o método [clean] anotado com [@Before]). A primeira inserção será bem-sucedida, mas a segunda não, porque a tabela [PRODUCTS] tem uma restrição de unicidade nos nomes dos produtos. Por conseguinte, deve ocorrer uma exceção. Isto é testado na linha 15;
  • como todos os métodos da interface [IDao] são executados dentro de uma transação, o facto de a segunda inserção falhar irá reverter toda a transação, incluindo a primeira inserção. Em última análise, não devem ser feitas inserções na tabela [PRODUCTS];
  • Linhas 16–17: Verificamos isto recuperando a lista de produtos na tabela [PRODUCTS] e verificando se esta lista está vazia;

3.6.9.6. O teste [updateProducts]

Este é o seguinte:


    @Test
    public void updateProduits() {
        // remplissage
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // on récupère les produits
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nom, int categorie, double prix, String description
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // vérifications
        // Assert.assertEquals(nbUpdated, nbProduits); -- does not work with DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • Linha 4: Inserimos 10 produtos na base de dados;
  • linha 7: recuperamo-los;
  • linhas 9–18: aumentamos os preços dos produtos da categoria n.º 1 em 10%;
  • linha 19: estas alterações são guardadas na base de dados;
  • linhas 22–25: percorremos a lista de produtos utilizados para a atualização na memória. Para cada um, procuramos o produto com a mesma chave primária na base de dados e verificamos se a atualização do preço foi bem-sucedida;
  • linha 19: recuperamos o número de produtos atualizados pela operação [updateProducts];
  • linha 21: verificamos se este número é, de facto, o esperado. Este teste é aprovado em todos os SGBDs, exceto no DB2. Por isso, comentámo-lo;

3.6.9.7. O teste [deleteProducts]

Este teste é o seguinte:


    @Test
    public void deleteProduits() {
        // filling
        fill(10);
        log("deleteProduits", 1);
        // product list
        List<Produit> produits = getDao().getAllProduits();
        // discontinuation of two products
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // checks
        // Assert.assertEquals(2, nbDeleted); -- does not pass with 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 na base de dados;
  • linhas 7–11: recuperamos todos os produtos da base de dados e removemos os produtos nas posições 0 e 5;
  • linhas 14–16: verificamos se os dois produtos já não se encontram na base de dados e se a base de dados tem agora menos dois produtos;
  • O teste na linha 13 falha com o SGBD DB2. É aprovado com os outros SGBDs;

3.6.9.8. Testes de desempenho

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


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

    @Test
    public void perf2() {
        // remplissage
        fill(10000);
        // modification
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nom, int categorie, double prix, String description
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }
 
    @Test
    public void perf3() {
        // remplissage
        fill(10000);
        // suppression
        List<Produit> produits = getDao().getAllProduits();
        // clés primaires
        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 utilizando as suas chaves primárias;
  • linhas 23-34: inserção de 10 000 produtos e, em seguida, eliminação dos mesmos utilizando as suas chaves primárias;

Para executar os testes [JUnitTestDao1] e [JUnitTestDao2], podem ser utilizadas as seguintes configurações de teste:

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

Em [1] os resultados do [JUnitTestDao1] e em [2] os do [JUnitTestDao2]. Não há diferenças significativas entre eles. Em [1]:

  • o teste é aprovado;
  • a inserção de 10 000 produtos demora 3,15 segundos;
  • a inserção de 10 000 produtos seguida da sua modificação demora 4,80 segundos;
  • a inserção de 10 000 produtos seguida da sua eliminação demora 4,40 segundos;
  • portanto, a operação mais dispendiosa é a inserção;