3. Introdução ao API JDBC
3.1. Configuração do ambiente de trabalho
Vamos trabalhar com uma base de dados MySQL5.
É necessário ter:
- ter instalado o JDK (Java Development Kit) (parágrafo 23.1);
- ter instalado o gestor de dependências Maven (parágrafo 23.2);
- ter instalado o IDE Spring Tool Suite (STS) (parágrafo 23.3);
- instalei o SGBD MySQL5 (parágrafo 23.4) e o seu cliente EMS MyManager (parágrafo 23.5);
- descarregou os códigos do documento [http://tahe.developpez.com/java/spring-database];
Presume-se, a seguir, que o administrador do MySQL5 é o utilizador root, com a palavra-passe «root». Inicie o SGBD, o MySQL5 e o seu cliente [MyManager]. Com a ajuda do [MyManager], criamos a base de dados [dbproduits] [1-34]:
![]() |
- em [3], a base de dados deve chamar-se [dbproduits];
![]() |
- em [8-9], como «root» com a palavra-passe «root» (o que não aparece na captura de ecrã acima);
![]() |
- para [14a], a palavra-passe é novamente root (o que não aparece na captura de ecrã);
- em [15], foi criada a base de dados [dbproduits];
![]() |
- em [20], tenha cuidado com a base selecionada. Deve ser a base [dbproduits];
![]() |
- em [22], a pasta é <exemplos>/spring-database-config/mysql/databases, em que <exemplos> é a pasta dos exemplos descarregados;
- em [23], selecione o script SQL [dbproduits.sql]. Este irá gerar a tabela [PRODUITS] na base de dados [dbproduits];
![]() |
![]() |
- em [30], foi criada a tabela [produits];
![]() |
- em [33], as colunas da tabela [produits];
![]() |
- para [34], que está inicialmente vazia;
Agora, com STS, importe os seguintes projetos (siga o procedimento utilizado para os projetos da pasta <exemplos>/spring-core):
![]() |
- em [2], o projeto [mysql-config-jdbc] será encontrado na pasta [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];
Este projeto configura a camada JDBC da arquitetura abaixo:
![]() |
Em seguida, volte a importar os três projetos seguintes:
![]() |
- para o [2]; os projetos estarão localizados na pasta [<exemples>/spring-database-config/spring-jdbc] [1];
Estes três projetos são projetos Maven que utilizam o projeto Maven [mysql-config-jdbc]. Este último 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 pelo projeto [oracle-config-jdbc, db2-config-jdbc, ...]. Para se certificar de que os projetos [spring-generic-jdbc-*] atualmente carregados no STS utilizam efetivamente o projeto [mysql-config-jdbc]:
- certifique-se de que não está carregado, ao mesmo tempo, outro projeto [sgbd-config-jdbc]. Isso poderia provocar 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 execução [spring-jdbc-generic-01.IntroJdbc01] [1-3]:
![]() |
Deve obter os seguintes resultados na consola:
Nos exemplos que se seguem, o leitor poderá:
- trabalhar diretamente com os projetos carregados anteriormente;
- ou criar ele próprio os projetos;
3.2. Etapas de exploração de uma base de dados
![]() |
Na arquitetura acima, a exploração de uma base de dados pelo programa de consola envolve as seguintes etapas:
- carregamento do controlador JDBC da base de dados;
- abertura de uma ligação com a base de dados;
- execução de um comando SQL na base de dados e processamento dos resultados do comando SQL;
- encerramento da ligação;
A etapa 1 é executada apenas uma vez. As etapas 2 a 4 são repetidas. Note-se que não se deixa uma ligação aberta. Fecha-se assim que deixar de ser necessária.
3.2.1. etapa 1 — carregamento na memória do controlador JDBC
O código
// carregamento do controlador JDBC
try {
Class.forName(nom de la classe du pilote JDBC);
} catch (ClassNotFoundException e1) {
// tratar a exceção
}
A operação da linha 3 tem como objetivo carregar na memória o controlador JDBC da base de dados. Esta operação só precisa de ser realizada uma vez. No entanto, repeti-la não causa qualquer erro. A classe do controlador JDBC é procurada no Classpath do projeto. Por conseguinte, no projeto Eclipse, o ficheiro [jar], que contém a classe do controlador JDBC, deve ter sido incluído no Classpath do projeto.
3.2.2. Passo 2 - abertura de uma ligação
Assim que o controlador JDBC estiver instalado, solicita-se que este abra uma ligação com o BD:
O código
package spring.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class IntroJdbc01 {
...
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// abertura da ligação
connexion = DriverManager.getConnection(url, user, passwd);
...
} catch (SQLException e1) {
// a exceção está a ser tratada
...
} finally {
// fechar a ligação
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e2) {
// tratar a exceção
...
}
}
}
- linhas 3-7: as classes de implementação da interface JDBC encontram-se todas no pacote [java.sql]. Além disso, em caso de erro, todas elas lançam uma exceção do tipo [SQLException] (linhas 19 e 27). Esta exceção deriva da classe [Exception] e é uma exceção dita «controlada»: é obrigatório utilizar um try/catch para a gerir ou, em alternativa, não a gerir e indicar que o método permite que a exceção seja lançada, completando a assinatura do método com [throws SQLException];
- linha 17, [DriverManager.getConnection] é um método estático que espera três parâmetros:
- [url]: o URL da base de dados. Trata-se de uma cadeia de caracteres dependente do BD utilizado. Para MySQL, tem o formato [jdbc:mysql://localhost:3306/nom_de_la_bd];
- [user]: o proprietário da ligação;
- [passwd]: a sua palavra-passe;
- 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. Etapa 3 — emissão de ordens SQL e [SELECT]
Assim que for estabelecida uma ligação, é possível emitir ordens SQL. A forma de gerir as ordens de leitura [SELECT] difere da utilizada para as operações de atualização [UPDATE, INSERT, DELETE]. Começamos pelas ordens SQL e [SELECT]:
O código
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// abertura da ligação
connexion = DriverManager.getConnection(url, user, passwd);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura
connexion.setReadOnly(true);
// a tabela [PRODUITS] está a ser lida
ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratando a exceção
doCatchException(connexion,e1);
} finally {
// processa o finally
doFinally(rs, ps, connexion);
}
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
- linhas 8, 10: abertura de uma transação (linha 8) em modo de leitura apenas (linha 10). Uma transação é uma sequência de ordens SQL que ou são todas bem-sucedidas ou todas falham. Assim, numa transação que inclua N ordens SQL, se a ordem I+1 falhar, as I anteriores serão anuladas. Para uma operação de leitura, não é necessária uma transação. No entanto, criar uma transação em modo de leitura apenas pode permitir que certas ordens SGBD realizem algumas otimizações;
- linha 12: utilização de um [PreparedStatement]. Um [PreparedStatement] tem normalmente parâmetros indicados pelo caractere ?. Aqui, não os tem. Um [PreparedStatement] é uma ordem preparada pelo SGBD. Esta preparação tem um custo e é realizada apenas uma vez. Posteriormente, esta ordem preparada é executada pelo SGBD com diferentes parâmetros efetivos que irão substituir os parâmetros formais «?». Note-se que é preferível nomear as colunas pretendidas em vez de utilizar a notação * para obter todas as colunas. Ao especificar o nome das colunas, é possível obter os seus valores a partir da sua posição na consulta SELECT;
- linha 13: execução do [PreparedStatement]. Recupera-se um objeto do tipo [ResultSet];
Um objeto do tipo [ResultSet] representa uma tabela, ou seja, um conjunto de linhas e colunas. Num determinado momento, só se tem acesso a uma linha da tabela, denominada linha atual. Aquando da criação inicial do [ResultSet], não existe nenhuma linha atual. É necessário executar uma operação [ResultSet.next()] para a obter. A assinatura do método next é a seguinte:
Este método tenta passar para a linha seguinte do [ResultSet] e devolve true se for bem-sucedido, false caso contrário. Em caso de sucesso, a linha seguinte torna-se a nova linha atual. A linha anterior é perdida e não será possível voltar atrás para a recuperar.
A tabela [ResultSet] possui colunas denominadas labelCol1, labelCol2, ... especificadas na consulta [SELECT] executada. Com a consulta:
SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
- a coluna [ID] será transferida para uma coluna do [ResultSet] denominada [myId];
- a coluna [NOM] será transferida para uma coluna do [ResultSet] denominada [myNom];
- ...
No exemplo acima, os identificadores [myCol] são designados por «rótulos de coluna». Na ausência destes rótulos, os nomes das colunas do [ResultSet] dependem do SGBD. Quando o [SELECT] opera numa única tabela, os rótulos das colunas serão, por predefinição, os nomes das colunas solicitados pelo SELECT. O problema surge quando o [SELECT] opera sobre várias tabelas e nestas existem nomes de colunas idênticos, como no exemplo seguinte:
SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
supondo que a tabela [PRODUITS] tenha uma chave estrangeira para a tabela [CATEGORIES], simbolizada pela relação [Produits].CATEGORIE_ID --> [CATEGORIES].ID, e que as tabelas [PRODUITS] e [CATEGORIES] tenham ambas um campo [NOM]. Neste caso, os nomes atribuídos na tabela [ResultSet] às colunas [PRODUITS.NOM] e [CATEGORIES.NOM] dependem da tabela SGBD. Para garantir a portabilidade entre o SGBD, é necessário utilizar aqui os rótulos das colunas, pelo que se escreverá:
SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
Para explorar os diferentes campos da linha atual do [ResultSet], dispõe-se dos seguintes métodos:
para obter a coluna denominada «labelColi» da linha atual e, consequentemente, a coluna do [SELECT] com esse rótulo. Type designa o tipo do campo coli. Podem ser utilizados os seguintes métodos [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Em vez de utilizar o nome da coluna, pode utilizar-se a sua posição na consulta [SELECT] executada:
onde i é o índice da coluna pretendida (i>=1).
- linhas 15-17: recuperação dos valores lidos na consulta BD;
- linha 19: a transação é validada (também se diz «confirmada»). Isto encerra-a e liberta os recursos que a SGBD tinha mobilizado para ela;
- linha 25: os recursos são libertados na transação [finally]. Esta transação chama o método [doFinally] seguinte:
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// encerramento de ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// encerramento [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// encerrar a ligação
connexion.close();
} catch (SQLException e3) {
// tratar a exceção
}
}
}
- linhas 3-9: encerramento do [ResultSet];
- linhas 11-17: encerramento do [PreparedStatement];
- linhas 18-27: encerramento da ligação;
Os encerramentos nas linhas 3-17 parecem redundantes, na medida em que a ligação é encerrada nas linhas 18-25. Na verdade, em alguns casos, não o são e é aconselhável 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 método [doCatchException] seguinte:
private static void doCatchException(Connection connexion, Throwable th) {
// anular transação
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// tratar a exceção
}
}
- linhas 4-6: a transação é anulada. Isto encerra-a e o SGBD poderá libertar os recursos mobilizados para a mesma;
3.2.4. Etapa 3 — emissão das ordens SQL e [INSERT, UPDATE, DELETE]
As ordens SQL e [INSERT, UPDATE, DELETE] são operações de atualização: alteram a base de dados, mas não devolvem nenhuma linha. A única informação fornecida é o número de linhas afetadas pela operação de atualização.
O código
Connection connexion = null;
PreparedStatement ps = null;
try {
// abrir ligação
connexion = DriverManager.getConnection(url, user, passwd);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// atualização da tabela
ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
// categoria 1
ps.setInt(1, 10);
// execução
int nbLignes=ps.executeUpdate();
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratamos a exceção
doCatchException(connexion, e1);
} finally {
// processa-se o finally
doFinally(null, ps, connexion);
}
}
- linha 9: a ligação é utilizada para leitura e escrita;
- linha 11: um [PreparedStatement] com 1 parâmetro (simbolizado por ?). Podem existir vários parâmetros. Estes são numerados a partir de 1;
- linha 13: atribui-se o seu valor ao único parâmetro. O primeiro parâmetro de [setType] é a posição do parâmetro no [PreparedStatement] (1, 2, ...) e o segundo é o valor que lhe é atribuído. É possível utilizar os métodos [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
- linha 15: utiliza-se o método [executeUpdate] e não o [executeQuery], reservado para as ordens SELECT. O método devolve o número de linhas afetadas pela operação. Pode ser 0.
- linha 17: a transação é validada;
3.2.5. etapa 4 — encerramento da ligação
Uma ligação deve ser encerrada o mais rapidamente possível num contexto multiutilizador, uma vez que um SGBD aceita um número limitado de ligações abertas. Nos exemplos anteriores, a ligação era encerrada na cláusula [finally] das operações SQL, de modo a garantir o seu encerramento independentemente de ter ocorrido ou não uma exceção.
3.3. Configuração da camada JDBC do SGBD MySQL5
Vamos analisar o projeto [mysql-config-jdbc], que configura a camada JDBC abaixo:
![]() |
3.3.1. O projeto Eclipse
![]() |
3.3.2. Configuração do Maven
O 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>
<!-- dependências variáveis ********************************************** -->
<!-- controlador JDBC do SGBD -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- dependências constantes ********************************************** -->
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- biblioteca jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<!-- Teste do Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- registos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
Nesta configuração do Maven, foram agrupados vários arquivos necessários tanto para o projeto [mysql-config-jdbc] como para os projetos que se basearão nele:
- linhas 4-6: o artefacto Maven gerado pelo projeto. Como já foi referido, todos os projetos do tipo [*-config-jdbc] geram este mesmo artefacto. Por isso, não devem ser carregados simultaneamente dois projetos do tipo [*-config-jdbc];
- 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 especificar essas versões nos projetos que dele derivam;
- linhas 18-21: o arquivo do driver JDBC do SGBD MySQL5. Este é o único arquivo necessário para o projeto [spring-jdbc-01];
- linhas 24-27: o artefacto [tomcat-jdbc] fornece um arquivo necessário para os projetos JDBC e [spring-jdbc-02 à 04];
- linhas 29-36: fornecem as bibliotecas necessárias para a gestão do jSON. Utilizadas em praticamente todos os projetos do documento;
- linhas 38-42: o Google Guava é uma biblioteca de gestão de coleções. Utilizada em praticamente todos os projetos do documento;
- linhas 43-52: as bibliotecas que permitem a criação de testes que integram o Spring e o JUnit. Utilizadas em praticamente todos os projetos do documento;
- linhas 54-57: as bibliotecas de registos. Utilizadas em praticamente todos os projetos do documento;
- linhas 67-71: o plugin que permite 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 {
// parâmetros de ligação
public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "root";
...
// comandos SQL [jdbc-01, jdbc-02]
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
//public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
// ordens SQL [jdbc-03]
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
...
}
A classe [ConfigJdbc] serve para configurar a camada JDBC dos quatro projetos [spring-jdbc-01 à 04]. A maior parte da configuração diz respeito ao projeto [spring-jdbc-04]. Apresentaremos esta parte quando analisarmos esse projeto. Acima, apenas foi mantida a configuração dos projetos [spring-jdbc-01 à 03].
- linhas 14-17: os parâmetros de ligação à base de dados MySQL5 [dbproduits];
- linhas 20-25: os comandos SQL utilizados nos projetos [spring-jdbc-01 et 02];
- linhas 28-34: as ordens SQL utilizadas no projeto [spring-jdbc-03];
Estas ordens SQL utilizam a tabela [PRODUITS] da base de dados MySQL5 [dbproduits], cuja estrutura é a seguinte:
![]() |
- [ID]: chave primária no modo AUTO_INCREMENT (se não for indicada uma chave primária, o SGBD gera-a);
- [NOM]: nome de um produto — único;
- [CATEGORIE]: n.º da sua categoria;
- [PRIX]: o seu preço;
- [DESCRIPTION]: uma descrição do produto;
3.3.4. A classe [Produit]
![]() |
A classe [Produit] corresponde à imagem de uma linha da tabela [PRODUITS]:
package generic.jdbc.entities.dbproduits;
public class Produit {
// campos
private int id;
private String nom;
private int categorie;
private double prix;
private String description;
// construtores
public Produit() {
}
public Produit(int id, String nom, int categorie, double prix, String description) {
this.id = id;
this.nom = nom;
this.categorie = categorie;
this.prix = prix;
this.description = description;
}
// getters e setters
...
}
Mais tarde, precisaremos de comparar dois produtos para saber se são iguais ou não. Consideraremos que dois produtos são iguais se todos os seus campos forem iguais. Para tal, vamos redefinir o método [equals] da classe [Object], da qual deriva a classe [Produit]:
// método de igualdade
@Override
public boolean equals(Object o) {
// casos simples
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Produit p = (Produit) o;
return this == o
|| (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
&& Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
- linha 3: o método [equals] recebe um objeto o que deve comparar com o objeto this;
- linhas 5-7: os casos simples em que se pode determinar imediatamente que os dois objetos não são iguais. [Object].getClass() fornece uma instância do tipo [Class], um tipo que representa a classe real do objeto;
- linha 8: o objeto o é convertido num produto p;
- linha 9: se as duas referências o e p a um produto forem iguais, então trata-se fisicamente do mesmo produto;
- linha 9: se o e p forem duas referências diferentes a dois produtos com os mesmos campos, dir-se-á que são iguais. Como o preço é do tipo [double] e não existe uma representação exata dos números reais na informática, consideraremos que dois preços são idênticos se forem iguais com uma precisão de 10⁻⁶;
Além disso, redefiniremos o método [hasCode] da classe [Object]:
// hashcode
@Override
public int hashCode() {
return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}
Os valores do hashCode de dois produtos devem ser os mesmos se o método [equals] tiver declarado esses dois produtos como iguais. Este valor do hashCode é utilizado para distribuir objetos em conjuntos, tais como dicionários. No exemplo acima, se dois produtos forem idênticos, terão, de facto, o mesmo hashCode.
3.3.5. A exceção [UncheckedException]
![]() |
Consideremos a seguinte arquitetura:
![]() |
- a camada [JDBC] lança exceções do tipo [SQLException]. Esta exceção deve subir pelas camadas até atingir a camada mais elevada, neste caso a camada de testes;
A camada [DAO] poderia limitar-se a deixar que a [SQLException] subisse até à camada de testes. Mas, como esta exceção não é controlada (decorre diretamente de [Exception]), isso implicaria que a interface [IDao] da camada [DAO] fosse a seguinte:
public interface IDao {
// adicionar produtos
public List<Produit> addProduits(List<Produit> produits) throws SQLException;
// lista de todos os produtos
public List<Produit> getAllProduits() throws SQLException;
// um produto específico
public Produit getProduitById(int id) throws SQLException;
public Produit getProduitByName(String name) throws SQLException;
// atualização de vários produtos
public int updateProduits(List<Produit> produits) throws SQLException;
// eliminação de todos os produtos
public int deleteAllProduits() throws SQLException;
// eliminação de vários produtos
public int deleteProduits(int[] ids) throws SQLException;
}
E isso é muito incómodo, pois impede-nos de implementar a interface [IDao] através de uma classe que lançaria uma exceção diferente. Para contornar esta dificuldade, a camada [DAO] irá lançar uma exceção [DaoException] não controlada (derivada de [RuntimeException]), o que nos poupa da cláusula [throws] na assinatura dos métodos da interface. Assim, esta poderá ser implementada por qualquer classe que também lance uma exceção não controlada, que poderá ser diferente da exceção [DaoException]. A nossa arquitetura passa agora a ser a seguinte:
![]() |
Para facilitar a criação de exceções não controladas para diferentes camadas de uma aplicação, criamos uma classe pai para elas, a [UncheckedException]:
![]() |
package generic.jdbc.infrastructure;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
// classe de exceção genérica
// a exceção é não controlada
public class UncheckedException extends RuntimeException {
// número de série ID gerado
private static final long serialVersionUID = -2924871763340170310L;
// propriedades
private int code;
private String trace;
private List<ShortException> exceptions;
// construtores
public UncheckedException() {
super();
}
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// local
this.code = code;
this.exceptions = getErreursForException(e);
// rastreio
String fileName = String.format("%s.java", simpleClassName);
StackTraceElement[] traces = e.getStackTrace();
boolean trouve = false;
for (int i = 0; !trouve && i < traces.length; i++) {
StackTraceElement trace = traces[i];
if (fileName.equals(trace.getFileName())) {
this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
trouve = true;
}
}
}
@Override
public String getMessage() {
return this.toString();
}
@Override
public void printStackTrace() {
System.out.println(this);
}
// lista de mensagens de erro de uma exceção
private List<ShortException> getErreursForException(Throwable th) {
// recuperam-se os elementos da pilha da exceção
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
// recuperar a exceção atual
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// exceção seguinte
cause = cause.getCause();
}
return exceptions;
}
@Override
public String toString() {
ObjectMapper jsonMapper = new ObjectMapper();
try {
return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getters e setters
...
}
- linha 12: a classe deriva de [RuntimeException] e é, portanto, um tipo de exceção não controlada. Servirá para encapsular uma exceção controlada (SQLException) num tipo de exceção não controlada (UncheckedException);
- para diferenciar as exceções do tipo [UncheckedException] entre si, poderá atribuir-lhes um código que será armazenado no campo privado da linha 18. Um código Java que intercepte uma exceção do tipo [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 derivado, em particular 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 encontra-se nas dependências Maven do projeto:
<!-- biblioteca jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
- linhas 45-48: redefinem o método [getMessage] da classe pai [RuntimeException]. Este método devolve aqui a assinatura [toString] da classe;
- linhas 50-53: redefinem o método [printStackTrace] da classe pai [RuntimeException]. É a assinatura [toString] da classe que será apresentada;
A classe [UncheckedException] regista no campo da linha 20 uma lista de exceções descritas pelo tipo [ShortException] seguinte:
package pam.dao.exceptions;
public class ShortException {
// propriedades
private String className;
private String errorMessage;
// construtores
public ShortException() {
}
public ShortException(String className, String errorMessage) {
this.className = className;
this.errorMessage = errorMessage;
}
// getters e setters
...
}
- linha 6: o nome da classe da exceção que ocorreu;
- linha 7: a mensagem de erro associada;
Analisemos o seguinte construtor da classe [UncheckedException]:
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// local
this.code = code;
this.exceptions = getErreursForException(e);
// rastreio
String fileName = String.format("%s.java", simpleClassName);
StackTraceElement[] traces = e.getStackTrace();
boolean trouve = false;
for (int i = 0; !trouve && i < traces.length; i++) {
StackTraceElement trace = traces[i];
if (fileName.equals(trace.getFileName())) {
this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
trouve = true;
}
}
}
- linha 1, os parâmetros são os seguintes:
- [code]: um código de erro;
- [e]: a exceção que está 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;], sendo c uma instância de C;
- [simpleClassName]: o nome simples da classe do 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: analisam-se os chamados rastros da exceção. Uma exceção inicial ocorre num ponto específico do código e, em seguida, remonta ao método que chamou aquele onde a exceção ocorreu, e assim sucessivamente até que um try/catch a interrompa. Nesta remontada, a exceção inicial deixa rastros armazenados na matriz [e.stackTrace] da exceção e. Estes são aqui obtidos na linha 8, a partir do [Throwable e] passado como parâmetro. Cada elemento do tipo [StackTraceElement] é um objeto que possui, entre os seus campos, os seguintes:
- [fileName]: o nome do ficheiro Java onde ocorreu a exceção;
- [lineNumber]: o número da linha nesse ficheiro onde ocorreu a exceção;
- [methodName]: o nome do método nesse ficheiro onde ocorreu a exceção;
- as linhas 10-16 procuram, na tabela de rastreios da exceção passada como parâmetro, a primeira ocorrência da condição [trace.fileName==simpleClassName.java], em que [simpleClassName] é o terceiro parâmetro do construtor. A ideia é registar onde ocorreu a exceção no código do utilizador. Este irá encapsular uma exceção da seguinte forma:
- linha 13: cria-se uma cadeia de caracteres do tipo [fileName, methodName, lineNumber] que identifica o local do código do utilizador onde a exceção foi detida;
Agora, vamos analisar o código que regista a lista de exceções da pilha de exceções da exceção [Throwable th] encapsulada pelo construtor anterior:
// lista de mensagens de erro de uma exceção
private List<ShortException> getErreursForException(Throwable th) {
// recuperam-se os elementos da pilha da exceção
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
// recupera-se a exceção atual
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// exceção seguinte
cause = cause.getCause();
}
return exceptions;
}
Durante a sua subida até ao método que a interceptou através de um try/catch, a exceção inicial e pode ter sido encapsulada numa exceção. É então esta última que prossegue a sua subida até ao método que a irá interceptar definitivamente. Por conseguinte, também ela pode ser sujeita a encapsulamento. No final, quando um método decide interromper uma exceção th e explorá-la, 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] permite identificar a exceção que ela própria encapsula. E assim sucessivamente. Quando uma exceção e tem o valor [e.getCause()==null], isso significa que e é a exceção inicial.
- linha 8: para cada exceção da pilha de exceções de [Throwable th], são memorizadas duas informações:
- [getClass().getName()]: o nome completo da exceção;
- [getMessage()]: a mensagem de erro associada;
3.4. Exemple-01
3.4.1. A arquitetura do projeto
![]() |
Neste exemplo, um programa de consola utiliza a interface da camada [JDBC].
3.4.2. O projeto Eclipse
Criamos um projeto Spring/Maven com o nome [spring-jdbc-01], seguindo os passos descritos no parágrafo 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 /> <!-- pesquisa do pai no repositório -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuração JDBC do SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- linhas 28-32: o projeto utiliza o 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];
É possível verificar este último ponto de duas formas, ao inspecionar as dependências Maven do projeto:
![]() |
- no [2], verifica-se que o projeto [mysql-config-jdbc] consta das dependências Maven do projeto. Como estas últimas se encontram no Classpath do projeto, isso significa que o projeto [mysql-config-jdbc] também se encontra nesse Classpath e que, 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 local do Maven. Ao contrário do que acontece com um IDE como o NetBeans, esta presença não é automática no Eclipse. É necessário forçá-la:
![]() |
Vimos as condições que tornam possível esta geração no parágrafo 2.3.5. Depois de concluída, é possível remover o projeto [mysql-config-jdbc] do separador [Package Explorer]:
![]() |
- não se deve marcar a opção [3], que elimina fisicamente o projeto do disco, tornando-o irrecuperável;
Esta operação reinicia o cálculo das 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] passa a ser o seguinte:
![]() |
Desta vez, a dependência já não é de um projeto, mas sim do artefacto Maven do mesmo, neste caso o artefacto [generic-config-jdbc] [1]. Vemos que temos, de facto, acesso a todas as classes e interfaces deste artefacto. Como já foi referido, este artefacto será gerado por todos os projetos [*-config-jdbc]. Para evitar erros, iremos:
- manteremos sempre um único projeto [*-config-jdbc] no separador [Package Explorer];
- atualizaremos a configuração Maven de todos os projetos no separador [Package Explorer] (Alt-F5) para que estes incluam, nas suas dependências Maven, o projeto [*-config-jdbc] utilizado;
3.4.3. O esboço da classe principal
![]() |
O esboço da classe principal [IntroJdbc01] é o seguinte:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IntroJdbc01 {
// constantes
final static ObjectMapper jsonMapper = new ObjectMapper();
public static void main(String[] args) {
// carregamento do controlador JDBC do SGBD
try {
Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
} catch (ClassNotFoundException e1) {
doCatchException("Pilote JDBC introuvable", null, e1);
return;
}
// esvaziar a tabela [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// preenche-se a tabela
System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
insert();
// a tabela é lida
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// atualização
System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
update();
// exibição
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// esvazia-se a tabela [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// exibir
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// INSERTion de dois elementos idênticos
// o INSERTion deve falhar e nenhum dos dois elementos é inserido devido à transação
System.out.println(String.format("------------------------------ %s",
"Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
insert2();
// verifica-se
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// concluído
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// lista de produtos
private static void select() {
...
}
// exibição de um objeto jSON
private static void affiche(Object object) {
...
}
// eliminação de produtos
public static void delete() {
...
}
// adição de produtos
public static void insert() {
...
}
// adição de 2 produtos com as mesmas chaves primárias
public static void insert2() {
...
}
// atualização de determinados produtos
public static void update() {
...
}
private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// encerramento ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// encerramento [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// encerrar a sessão
connexion.close();
} catch (SQLException e3) {
// são apresentadas as mensagens de erro
show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
getErreursFromThrowable(e3));
}
}
}
private static void doCatchException(String title, Connection connexion, Throwable th) {
// são apresentadas as mensagens de erro
show(title, getErreursFromThrowable(th));
// anulação da transação
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// exibir as mensagens de erro
show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
}
}
private static List<String> getErreursFromThrowable(Throwable th) {
// recupera-se a lista de mensagens de erro da exceção
List<String> erreurs = new ArrayList<String>();
while (th != null) {
// mensagem de erro do throwable
erreurs.add(th.getMessage());
// avança-se para a causa do throwable
th = th.getCause();
}
// resultado
return erreurs;
}
private static void show(String title, List<String> messages) {
// título
System.out.println(String.format("%s : ", title));
// mensagens
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
- linhas 23-29: carregamento do controlador JDBC do SGBD. Na linha 25, utiliza-se a constante [ConfigJdbc.DRIVER_CLASSNAME] definida no projeto [mysql-config-jdbc];
- linhas 136-147: o método [getErreursFromThrowable] 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, que é obtida através do método [Throwable].getCause(). Desta forma, percorrem-se todas as exceções encapsuladas no objeto [Throwable];
- linhas 149-156: o método [show(String title, List<String> messages)] apresenta as mensagens precedidas do texto [title];
- linhas 122-134: o método [doCatchException(String title, Connection connexion, Throwable th))] gere as exceções encontradas pelos métodos da classe. A exceção gerida é representada pelo parâmetro [Throwable th]. O objetivo do método é:
- anular a transação em curso do objeto [Connection connexion] (linhas 127-129);
- gravar as mensagens de erro encapsuladas na exceção [Throwable th] (linhas 124, 132);
- linhas 93-120: o método [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] gere o ramo [finally] dos métodos de acesso ao SGBD. Tem como objetivo libertar os recursos mobilizados pela ligação;
3.4.4. Eliminação do conteúdo da tabela de produtos
O método [delete] elimina o conteúdo da tabela:
// eliminação de produtos
public static void delete() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// início da sessão
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// esvaziar a tabela [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
ps.executeUpdate();
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratamento da exceção
doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
} finally {
// processar o finally
doFinally(null, ps, connexion);
}
}
A linha 7 utiliza as seguintes constantes da classe [ConfigJdbc]:
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";
Na linha 13, a ordem SQL preparada é a seguinte:
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
O método [delete] utiliza transações. Uma transação permite agrupar ordens SQL que devem ser todas bem-sucedidas ou todas canceladas. Há quatro operações a ter em conta:
- início de uma transação: [connexion.setAutoCommit(false)];
- fim de uma transação com sucesso: [connexion.commit()]. Neste caso, todas as operações realizadas na BD durante a transação são validadas;
- fim de uma transação com falha: [connexion.rollback()]. Neste caso, todas as operações realizadas no BD durante a transação são anuladas;
Nos nossos exemplos, sempre que ocorre uma exceção, cancelamos a transação no método [doCatchException]:
private static void doCatchException(String title, Connection connexion, Throwable th) {
// exibem-se as mensagens de erro
Static.show(title, Static.getErreursFromThrowable(th));
// anulação da transação
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// exibição das mensagens de erro
Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
}
}
3.4.5. Criação do conteúdo da tabela de produtos
O método [insert] cria o conteúdo da tabela:
public static void insert() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// abertura da ligação
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// preenchimento da tabela
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
for (int i = 0; i < 10; i++) {
// preparação
int n = i + 1;
ps.setInt(1, n);
ps.setString(2, String.format("NOM%s", n));
ps.setInt(3, n / 5 + 1);
ps.setDouble(4, 100 * (1 + (double) i / 100));
ps.setString(5, String.format("DESC%s", n));
// execução
ps.executeUpdate();
}
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratamento da exceção
doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
} finally {
// tratando o finally
doFinally(null, ps, connexion);
}
}
Na linha 12, a ordem SQL preparada é a seguinte:
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
3.4.6. Exibição do conteúdo da tabela de produtos
O método [select] apresenta o conteúdo da tabela:
// lista de produtos
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// abertura de ligação
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura
connexion.setReadOnly(true);
// leitura da tabela [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratando a exceção
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// processa-se o finally
doFinally(rs, ps, connexion);
}
}
Na linha 14, a ordem SQL preparada é a seguinte:
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
O método [affiche] (linha 18) é o seguinte:
// exibição de um objeto jSON
private static void affiche(Object object) {
try {
System.out.println(jsonMapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Apresenta a representação jSON do objeto passado como parâmetro (ver jSON, parágrafo 23.12).
3.4.7. Atualização do conteúdo da tabela
O método [update] atualiza determinados produtos:
// atualização de determinados produtos
public static void update() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// abertura de sessão
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// atualização da tabela
ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
// categoria 1
ps.setInt(1, 1);
// execução
ps.executeUpdate();
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratamos a exceção
doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
} finally {
// processa-se o finally
doFinally(null, ps, connexion);
}
}
Na linha 13, a ordem SQL preparada é a seguinte:
public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
3.4.8. Função da transação
O método [insert2] insere dois produtos com a mesma chave primária na tabela, o que não é possível. Como estamos numa transação, a primeira inserção será anulada.
// adição de 2 produtos com as mesmas chaves primárias
public static void insert2() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// abertura de ligação
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// adiciona-se 1 linha
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
// execução
ps.executeUpdate();
// a mesma linha é adicionada uma segunda vez, portanto com a mesma chave primária
// o INSERTion deve falhar e nenhum dos dois elementos deve ser inserido devido à transação
ps.executeUpdate();
// confirmar transação
connexion.commit();
} catch (SQLException e1) {
// tratamos a exceção
doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
connexion, e1);
} finally {
// processa-se o finally
doFinally(null, ps, connexion);
}
}
Linha 13, a ordem SQL preparada é a seguinte:
public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
3.4.9. Resultados
Executa-se a configuração de execução denominada [spring-jdbc-generic-01.IntroJdbc01]:
![]() |
Obtêm-se os seguintes resultados 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.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire :
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Travail terminé
- linha 30: antes da inserção dos dois produtos com a mesma chave primária, a tabela está vazia;
- linha 35: após a inserção dos dois produtos com a mesma chave primária, a tabela está vazia. Isto demonstra o papel da transação:
- a primeira inserção é bem-sucedida. Não há qualquer motivo para que falhe;
- a segunda inserção falha (linha 32). Assim, como estas duas inserções fazem parte da mesma transação, todas as ordens SQL dessa transação são anuladas, incluindo a primeira inserção.
3.4.10. Conclusão
O que é notável nos códigos anteriores é o grande espaço dedicado ao tratamento da exceção [SQLException]. Como qualquer operação JDBC pode desencadeá-la, existem inúmeros blocos try/catch no código.
3.5. Exemple-02
Vamos retomar a aplicação anterior utilizando uma fonte de dados do tipo [javax.sql.DataSource]:

Vamos utilizar uma fonte de dados implementada pela classe [org.apache.tomcat.jdbc.pool.DataSource]. Esta classe utiliza um pool 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;
No final, 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 do Spring
3.5.1. A 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 através da cópia do projeto anterior [1-6]:
![]() |
![]() |
Em seguida, o projeto é atualizado 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 /> <!-- pesquisar o pai no repositório -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuração JDBC do SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- linhas 28-33: a dependência Maven do projeto [mysql-config-jdbc];
É o projeto [mysql-config-jdbc] que inclui nas suas dependências Maven a biblioteca que fornece uma implementação de uma fonte de dados do tipo [javax.sql.DataSource] (ver parágrafo 3.3.2):
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
3.5.4. Configuração do Spring
![]() |
A classe de configuração do Spring [AppConfig] é a seguinte:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
// fonte de dados
@Bean
public DataSource dataSource() {
// fonte de dados TomcatJdbc
DataSource dataSource = new DataSource();
// configuração de acesso JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// ligações abertas inicialmente
dataSource.setInitialSize(5);
// resultado
return dataSource;
}
}
- linha 10: [AppConfig] é uma classe de configuração do Spring;
- linha 11: importação da classe de configuração [generic.jdbc.config.ConfigJdbc.class] definida no projeto [mysql-config-jdbc]. Isto significa que dispomos de todos os beans definidos por este ficheiro de configuração;
- linhas 14-27: o bean Spring que define a fonte de dados;
- linha 17: criação da fonte de dados, ainda não configurada;
- linhas 19-22: as informações que permitem que a fonte de dados se ligue à base de dados;
- linha 24: cria um conjunto de 5 ligações. Aqui, só precisamos 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 {
// mapeamento jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
// fonte de dados
private static DataSource dataSource;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = null;
try {
// recuperação do contexto Spring
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// recuperação da fonte de dados
dataSource = ctx.getBean(DataSource.class);
// esvaziar a tabela [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
...
// concluído
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// lista de produtos
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// abertura de sessão
connexion = dataSource.getConnection();
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura
connexion.setReadOnly(true);
// a tabela [PRODUITS] está a ser lida
ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// confirmação da transação
connexion.commit();
} catch (SQLException e1) {
// tratando a exceção
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// processa-se o 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 dos objetos Spring;
- linha 32: obtenção de uma referência à fonte de dados. Note-se que, em nenhum momento, é mencionada a classe efetivamente utilizada. Assim, neste caso, nada sugere que se esteja a utilizar uma implementação [TomcatJdbc];
- linha 49: obtenção de uma ligação aberta. É assim que os diferentes métodos de [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
Executa-se a configuração de execução denominada [spring-jdbc-generic-02.IntroJdbc02]:
![]() |
Obtêm-se os mesmos resultados que anteriormente (ponto 3.4.9).
3.6. Exemple-03
3.6.1. A arquitetura do projeto
![]() |
Neste exemplo, os métodos de acesso aos dados estão isolados numa camada [dao]. Serão testados por um teste JUnit.
3.6.2. O projeto Eclipse
O projeto Eclipse [spring-jdbc-03] é um projeto Spring/Maven construído à semelhança do anterior e posteriormente complementado da seguinte forma:
![]() | ![]() |
Os diferentes pacotes têm as seguintes funções:
- [spring.jdbc.config]: configuração do projeto Spring;
- [spring.jdbc.dao]: implementação da camada [DAO];
- [spring.jdbc.infrastructure]: implementa a exceção não controlada [DaoException];
3.6.3. Configuração do Maven
O projeto Maven é configurado pelo seguinte 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 /> <!-- pesquisar o pai no repositório -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuração JDBC do SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
É idêntico ao do projeto [spring-jdbc-02]. Utiliza, nomeadamente, a dependência Maven do projeto [mysql-config-jdbc] (linhas 28-32).
3.6.4. Interface da camada [DAO]
![]() |
A camada [DAO] apresenta a seguinte interface [IDao]:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.Produit;
public interface IDao {
// adicionar produtos
public List<Produit> addProduits(List<Produit> produits);
// lista de todos os produtos
public List<Produit> getAllProduits();
// um produto específico
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// atualização de vários produtos
public int updateProduits(List<Produit> produits);
// eliminação de todos os produtos
public int deleteAllProduits();
// eliminação de vários produtos
public int deleteProduits(int[] ids);
}
3.6.5. A classe [DaoException]
A classe [DaoException] limita-se a estender a classe [UncheckedException] apresentada no parágrafo 3.3.5:
![]() |
package spring.jdbc.infrastructure;
public class DaoException extends UncheckedException {
private static final long serialVersionUID = 1L;
// fabricantes
public DaoException() {
super();
}
public DaoException(int code, Throwable e, String className) {
super(code, e, className);
}
}
3.6.6. Configuração do projeto Spring
![]() |
A classe [AppConfig], que configura o projeto Spring, é idêntica ao ficheiro de configuração Spring do exemplo [spring-jdbc-02], com exceção da linha 11:
package spring.jdbc.config;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
// fonte de dados
@Bean
public DataSource dataSource() {
// fonte de dados TomcatJdbc
DataSource dataSource = new DataSource();
// configuração de acesso JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// ligações inicialmente abertas
dataSource.setInitialSize(5);
// resultado
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 (parágrafo 3.6.4) que a camada [DAO] implementa a seguinte interface [IDao]:
package spring.jdbc.dao;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.List;
public interface IDao {
// adicionar produtos
public List<Produit> addProduits(List<Produit> produits);
// lista de todos os produtos
public List<Produit> getAllProduits();
// um produto específico
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// atualização de vários produtos
public int updateProduits(List<Produit> produits);
// eliminação de todos os produtos
public int deleteAllProduits();
// eliminação de vários produtos
public int deleteProduits(int[] ids);
}
Ambas as classes [Dao1, Dao2] implementam esta interface. A classe [Dao2] é uma variante da classe [Dao1] que introduz uma novidade sintática. Vamos concentrar-nos na classe [Dao1]. A estrutura desta classe é a seguinte:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.jdbc.infrastructure.DaoException;
@Component("dao1")
public class Dao1 implements IDao {
// nome da classe
private String simpleClassName = getClass().getSimpleName();
// fonte de dados
@Autowired
protected DataSource dataSource;
// fabricante
public Dao1() {
System.out.println("building Dao1...");
}
// ------------------------------- interface
@Override
public List<Produit> getAllProduits() {
...
}
@Override
public Produit getProduitById(int id) {
...
}
@Override
public Produit getProduitByName(String name) {
...
}
@Override
public List<Produit> addProduits(List<Produit> produits) {
....
}
@Override
public int updateProduits(List<Produit> produits) {
...
}
@Override
public int deleteAllProduits() {
...
}
@Override
public int deleteProduits(int[] ids) {
...
}
// ---------------------------------------- métodos locais
// gestão finally
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
...
}
// gestão do «catch»
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 maiúscula convertida em minúscula;
- linha 24: o nome da classe. Evita-se escrever «[Dao]» de forma rígida para permitir renomear a classe sem ter de redefinir este campo, que assim permanece sempre válido;
- linhas 26-27: injeção da fonte de dados [tomcat-jdbc] definida na classe de configuração [AppConfig];
- linhas 36-68: implementação da interface [IDao];
- linhas 78-80: gestão centralizada do catch dos diferentes métodos;
- linhas 72-75: gestão centralizada do finally dos diferentes métodos;
O catch dos diferentes métodos é gerido da seguinte forma:
// gestão de «catch»
protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
// anulação da transação
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
e2.printStackTrace();
}
// daoException
return new DaoException(code, th, simpleClassName);
}
- linha 2: o método é declarado como [protected], o que permite que as classes filhas o utilizem sem que seja, por isso, público. Recebe os seguintes parâmetros:
- [Connection connexion]: a ligação com o SGBD — talvez null;
- [Throwable th]: a exceção que ocorreu e que será encapsulada num tipo [DaoException];
- [int code]: um código de erro a utilizar se o método criar um novo [DaoException];
- linhas 4-7: a principal função deste método é anular a transação associada à ligação passada como parâmetro 1;
- linhas 8-10: se a anulação da transação falhar, o registo da exceção é gravado na consola. Não é possível fazer muito mais, uma vez que será lançada uma exceção na linha 12;
O finally dos diferentes métodos é gerido da seguinte forma:
// gestão «finally»
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
// encerramento ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// encerramento [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
// encerramento da sessão
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e3) {
// regista-se o erro, se possível
if (daoException == null) {
daoException = new DaoException(code, e3, simpleClassName);
}
}
}
// resultado
return daoException;
}
- linha 2: este método também é declarado como [protected]. Recebe os seguintes parâmetros:
- [ResultSet rs]: o eventual [ResultSet], caso tenha sido executada uma operação [SELECT] — talvez null;
- [PreparedStatement ps]: o [PreparedStatement] que foi executado — talvez null;
- [Connection connexion]: a ligação com o SGBD — talvez null;
- [int code]: um código de erro a utilizar se o método criar um novo [DaoException];
- [DaoException daoException]: o eventual [DaoException], caso tenha ocorrido uma exceção antes do finally — talvez null;
- linhas 21-30: o objetivo principal deste método é encerrar a ligação (linha 23);
- linhas 24-29: se, durante este encerramento, ocorrer uma exceção, então verifica-se o estado do parâmetro [DaoException daoException] que nos foi passado: se for [daoException == null], então cria-se um novo [DaoException] com o código passado como parâmetro;
- linha 32: o antigo ou o novo [DaoException] é devolvido como resultado;
Não vamos apresentar todos os métodos da classe [Dao], mas apenas alguns. São todos semelhantes.
3.6.7.1. O método [getProduitById]
O método [getProduitById] devolve o produto cuja chave primária é igual ao parâmetro [id] ou, caso contrário, ao null;
@Override
public Produit getProduitById(int id) {
// recursos da ligação
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
// Inicialmente, nenhuma exceção
DaoException daoException = null;
// o produto procurado
Produit produit = null;
try {
// abertura da ligação
connexion = dataSource.getConnection();
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura
connexion.setReadOnly(true);
// a tabela [PRODUITS] está a ser lida
ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
}
// confirmação da transação
connexion.commit();
// regresso ao modo predefinido
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// tratamos a exceção
daoException = doCatchException(connexion, e1, 112);
} finally {
// processar o finally
daoException = doFinally(rs, ps, connexion, 113, daoException);
}
// exceção?
if (daoException != null) {
throw daoException;
}
// resultado
return produit;
}
- linha 10: o produto a devolver é definido como null;
- linha 19: a ordem SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] é a seguinte:
public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
- linhas 22-24: se o [ResultSet] tiver uma linha, esta é utilizada para criar o produto a devolver; caso contrário, o produto a devolver permanece em null;
- linha 41: devolve-se o produto;
- linha 8: a exceção [DaoException] do método é inicializada com null;
- linha 31: o método [doCatchException] cria uma exceção [DaoException];
- linha 34: o parâmetro [daoException] do método [doFinally] é ou null, ou a exceção criada pelo método [doCatchException]. O método [doFinally]:
- mantém este parâmetro inalterado se conseguir encerrar a ligação;
- mantém este parâmetro inalterado se não conseguir encerrar a ligação e já tiver ocorrido um [DaoException] anteriormente;
- cria um novo [DaoException] se não conseguir encerrar a ligação e não tiver havido um [DaoException] anteriormente;
- linhas 37-39: se a exceção local [daoException] não for igual a null, então é lançada; caso contrário, devolve-se o resultado solicitado (linha 41);
3.6.7.2. O método [deleteProduits]
O método [deleteProduits] elimina os produtos cujas chaves primárias lhe são passadas como parâmetros. Devolve o número de produtos eliminados.
@Override
public int deleteProduits(int[] ids) {
// recursos da ligação
PreparedStatement ps = null;
Connection connexion = null;
// inicialmente, nenhuma exceção
DaoException daoException = null;
// número de produtos atualizados
int nbProduits = 0;
try {
// abertura da ligação
connexion = dataSource.getConnection();
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// eliminam-se os produtos
ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
for (int id : ids) {
// parâmetros
ps.setInt(1, id);
// execução
nbProduits += ps.executeUpdate();
}
// confirmar transação
connexion.commit();
// regresso ao modo predefinido
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// tratamento da exceção
daoException = doCatchException(connexion, e1, 171);
} finally {
// tratando o finally
daoException = doFinally(null, ps, connexion, 172, daoException);
}
// exceção?
if (daoException != null) {
throw daoException;
}
// resultado
return nbProduits;
}
- na linha 18, a ordem SQL [ConfigJdbc.V2_DELETE_PRODUITS] é a seguinte:
public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
- linhas 18-24: o código de eliminação de produtos. Vê-se que a ordem SQL é preparada 1 vez (linha 18) e executada n vezes (linhas 19-24). É esta a utilidade 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: é devolvido o número de produtos atualizados;
3.6.7.3. O método [updateProduits]
O método [updateProduits] atualiza na base de dados os produtos que lhe são passados como parâmetros. Devolve o número de produtos atualizados.
@Override
public int updateProduits(List<Produit> produits) {
// recursos da ligação
PreparedStatement ps = null;
Connection connexion = null;
// inicialmente, nenhuma exceção
DaoException daoException = null;
// número de produtos atualizados
int nbProduits = 0;
try {
// abertura da ligação
connexion = dataSource.getConnection();
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura/gravação
connexion.setReadOnly(false);
// a tabela [PRODUITS] está a ser atualizada
ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
for (Produit produit : produits) {
// parâmetros
ps.setString(1, produit.getNom());
ps.setDouble(2, produit.getPrix());
ps.setInt(3, produit.getCategorie());
ps.setString(4, produit.getDescription());
ps.setInt(5, produit.getId());
// execução
nbProduits += ps.executeUpdate();
}
// confirmar transação
connexion.commit();
// regresso ao modo predefinido
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// tratamento da exceção
daoException = doCatchException(connexion, e1, 131);
} finally {
// tratando o finally
daoException = doFinally(null, ps, connexion, 132, daoException);
}
// exceção?
if (daoException != null) {
throw daoException;
}
// resultado
return nbProduits;
}
- linha 18: a ordem SQL [ConfigJdbc.V2_UPDATE_PRODUITS] é a seguinte:
public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
- linhas 19-28: o código de atualização dos produtos;
3.6.7.4. O método [addProduits]
O método [addProduits] insere na base de dados os produtos que lhe são passados como parâmetros. Este método devolve esses mesmos produtos com as suas chaves primárias (antes da inserção na base de dados, os produtos não têm chave primária).
@Override
public List<Produit> addProduits(List<Produit> produits) {
// recursos da ligação
PreparedStatement ps = null;
Connection connexion = null;
// inicialmente, nenhuma exceção
DaoException daoException = null;
try {
// abertura da ligação
connexion = dataSource.getConnection();
// em modo de leitura/gravação
connexion.setReadOnly(false);
// início da transação
connexion.setAutoCommit(false);
// são adicionados elementos à tabela [PRODUITS]
String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
for (Produit produit : produits) {
// parâmetros
ps.setString(1, produit.getNom());
ps.setLong(2, produit.getCategorie());
ps.setDouble(3, produit.getPrix());
ps.setString(4, produit.getDescription());
// execução do comando
ps.executeUpdate();
// chave primária gerada
ResultSet generatedKeys = ps.getGeneratedKeys();
if (generatedKeys.next()) {
produit.setId(generatedKeys.getInt(1));
} else {
throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
produit.getNom()));
}
}
// confirmação da transação
connexion.commit();
// regresso ao modo predefinido
connexion.setAutoCommit(true);
} catch (SQLException | RuntimeException e1) {
// a exceção está a ser tratada
daoException = doCatchException(connexion, e1, 151);
} finally {
// processando o finally
daoException = doFinally(null, ps, connexion, 152, daoException);
}
// exceção?
if (daoException != null) {
throw daoException;
}
// resultado
return produits;
}
- na linha 16, a ordem SQL [ConfigJdbc.V2_INSERT_PRODUITS] é a seguinte:
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
No exemplo acima, o comando de inserção de um produto não inclui a chave primária [ID]. Como a chave primária da base de dados MySQL tem o atributo [AUTOINCREMENT], o SGBD irá, então, gerar uma chave primária para cada inserção. Surge então o problema de recuperar essa chave. Trata-se de um ponto importante, uma vez que as operações nos produtos são realizadas através das suas chaves primárias. É, portanto, necessário conhecê-las;
- linhas 17-33: o ciclo de inserção dos produtos;
- linha 16: uma variante específica do método [prepareStatement]. O segundo parâmetro, [generatedColumns], é uma matriz de nomes de colunas cujos valores se pretende recuperar após a inserção. Na linha 16, indicámos que pretendíamos recuperar o valor da coluna [id]. Note-se aqui que, embora os nomes das colunas de uma tabela não distingam maiúsculas de minúsculas, o SGBD PostgreSQL exigiu que esse nome fosse escrito em minúsculas. Este é tipicamente o tipo de problema que se encontra ao portar código de um SGBD para outro;
- linha 24: inserção de uma linha na base de dados;
- linha 26: recupera-se a lista de valores das colunas especificadas na linha 16 num [ResultSet]. Aqui, para 1 inserção, o [ResultSet] terá 1 linha e essa linha terá uma única coluna que contém a chave primária;
- linha 28: recupera-se a chave primária gerada pelo SGBD;
- linhas 29-32: se não se obtiver a chave primária gerada, lança-se um [RuntimeException] que será encapsulado num [DaoException] nas 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)»:
- [resource] é um recurso que implementa a interface [java.lang.AutoCloseable]. Todos os recursos que são libertados com o método [close] fazem parte desta classe. Esta sintaxe garante que, na linha 4, o recurso [resource] será encerrado. Isto evita ter de escrever uma cláusula [finally] para realizar esta tarefa de encerramento;
Tomemos como exemplo o método [getAllProduits] da classe [Dao2]:
@Override
public List<Produit> getAllProduits() {
// eventual exceção
DaoException daoException = null;
// lista de produtos
List<Produit> produits = new ArrayList<Produit>();
try (Connection connexion = dataSource.getConnection()) {
// início da transação
connexion.setAutoCommit(false);
// em modo de leitura
connexion.setReadOnly(true);
// a tabela [PRODUITS] está a ser lida
try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
}
// fim da transação
connexion.commit();
// regresso ao modo predefinido
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// anula-se a transação
daoException = doRollback(connexion, e1, 111);
}
} catch (SQLException e2) {
// processa-se a exceção
if (daoException == null) {
daoException = new DaoException(112, e2, simpleClassName);
}
}
// exceção?
if (daoException != null) {
throw daoException;
}
// resultado
return produits;
}
- linha 7: try com a recurso [Connection]. Na linha 27, temos a certeza de que esta está fechada;
- linha 13: bloco «try» com a recurso [PreparedStatement]. Na linha 23, tem-se a certeza de que esta está fechada;
- linha 14: instrução «try» com a recurso [ResultSet]. Na linha 19, tem-se a garantia de que esta está encerrada;
- linha 25: a transação é cancelada da seguinte forma:
private DaoException doRollback(Connection connexion, Throwable e1, int code) {
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
}
// geração da exceção
return new DaoException(code, e1, simpleClassName);
}
No final, ficamos com um código mais fácil de ler.
3.6.9. Implementação da camada de testes
3.6.9.1. As classes de teste
![]() |
![]() |
- o teste [JUnitTestDao1] é um teste JUnit da classe [Dao1];
- o teste [JUnitTestDao2] é um teste JUnit da classe [Dao2];
- [AbstractJUnitTestDao] é a classe pai das duas classes de teste anteriores;
- [MainTestDao1] é uma classe de consola de teste da classe [Dao1];
- [MainTestDao2] é uma classe de consola de teste da classe [Dao2];
- [AbstractMainTestDao] é a classe pai das duas classes anteriores. Ela retoma o código das classes de consola [IntroJdbc01, IntroJdbc02] já apresentadas, pelo que não iremos analisar essas 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 {
// camada [DAO]
@Autowired
@Qualifier("dao1")
private IDao dao;
@Override
IDao getDao() {
return dao;
}
}
- As anotações das linhas 12-13 foram apresentadas no parágrafo 2.5.5. Permitem que um teste Junit tenha um acesso simples ao contexto Spring e aos seus beans. Este contexto é configurado pela classe [AppConfig] (linha 12), analisada no parágrafo 2.4.3;
- linha 14: a classe estende a classe [AbstractJUnitTestDao], que iremos apresentar. É nesta classe que se encontram os métodos de teste JUnit;
- linhas 17-19: o bean denominado [dao1] (linha 18) é injetado (linha 17). Trata-se, portanto, de uma instância da classe [Dao1] que é aqui injetada;
- linhas 21-24: o método [getDao] redefine o método com o mesmo nome na classe pai;
Em suma, o objetivo desta classe é devolver à classe pai uma referência à camada [DAO] que deve ser testada, neste caso uma instância de [Dao1]. Da mesma forma, a classe [JUnitTestDao2] devolve à classe pai [AbstractJUnitTestDao] uma instância da classe [Dao2].
A classe [AbstractJUnitTestDao] é uma classe de testes da JUnit:
package spring.jdbc.tests;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;
import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractJUnitTestDao {
// camada [DAO]
abstract IDao getDao();
// mapeamento jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
@Before
public void clean() {
// limpa-se a base antes de cada teste
log("Vidage de la base de données", 1);
getDao().deleteAllProduits();
}
@Test
public void getProduits() throws JsonProcessingException {
...
}
@Test
public void getProduitBy() {
...
}
@Test
public void doInsertsInTransaction() {
...
}
@Test
public void updateProduits() {
...
}
@Test
public void deleteProduits() {
....
}
@Test
public void perf1() {
...
}
@Test
public void perf2() {
...
}
@Test
public void perf3() {
....
}
// -------------- métodos privados
...
}
- na linha 19, a classe [AbstractJUnitTestDao] é abstrata;
- linha 22: o método abstrato [getDao], que permite obter a referência à camada [DAO] a ser testada. Este método é implementado pelas classes filhas;
- linha 25: um mapeador jSON que nos permitirá apresentar na consola o valor jSON dos produtos;
- linhas 27-32: antes de cada teste (linha 27), a tabela [PRODUITS] é esvaziada;
3.6.9.2. O método privado [fill]
O método privado [fill] é utilizado para inserir produtos na tabela [PRODUITS].
private List<Produit> fill(int nbProduits) {
log("Remplissage de la base de données", 1);
// cria-se uma lista de produtos
List<Produit> produits = new ArrayList<Produit>();
for (int i = 0; i < nbProduits; i++) {
int n = i + 1;
// int id, String nome, int categoria, double preço, String descrição
produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
"DESC%s", n)));
}
// armazenamos a lista na base de dados - recuperamos os produtos com a sua chave primária
produits = getDao().addProduits(produits);
// cria-se um dicionário de produtos para poder encontrá-los mais facilmente
// a chave do dicionário é a chave primária do produto na base de dados
for (Produit produit : produits) {
mapProduits.put(produit.getId(), produit);
}
// retornamos os produtos
return produits;
}
- linha 1: o método [fill] insere [nbProduits] na tabela [PRODUITS], que se supõe estar vazia;
- linhas 3-10: criação de uma lista de produtos com o seguinte formato:
new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));
que utiliza o construtor Produto(int id, String nome, int categoria, double preço, String descrição). O valor do primeiro parâmetro [id] (chave primária da tabela [PRODUITS]) não tem importância, uma vez que o método [addProduits] da linha 10 não o insere na base de dados e deixa que o SGBD gere o seu valor;
- linha 12: a lista de produtos é guardada na base de dados. Cada um dos produtos desta lista é enriquecido com uma nova chave primária [id]. O método [addProduits] devolve como resultado o seu parâmetro [produits]. Por isso, poderíamos não ter recuperado o resultado;
- linhas 15-17: colocamos os produtos num dicionário:
// dicionário de produtos
private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();
A chave do dicionário é a chave primária do produto e o valor associado é o próprio produto;
- linha 19: devolve-se a lista de produtos;
3.6.9.3. O teste [getProduits]
Este é o seguinte:
@Test
public void getProduits() throws JsonProcessingException {
// preenchimento
fill(10);
// lista de produtos
log("Liste des produits", 2);
List<Produit> produits = getDao().getAllProduits();
affiche(produits);
// verifica-se se a lista recuperada e a lista guardada são idênticas
for (Produit produit : produits) {
Produit found = mapProduits.get(produit.getId());
Assert.assertEquals(found, produit);
mapProduits.remove(found.getId());
}
// todos os produtos iniciais devem ter desaparecido do dicionário
Assert.assertEquals(0, mapProduits.size());
}
}
- linha 4: são introduzidos 10 produtos na base de dados;
- linha 7: feito isto, solicita-se a visualização de todos os produtos da base de dados;
- linha 8: exibem-se os produtos. O objetivo é verificar se os produtos foram efetivamente registados e se possuem uma chave primária;
- linhas 10-13: verifica-se se os produtos encontrados são idênticos aos que foram guardados e que podem ser encontrados no dicionário [mapProduits];
- linha 11: recupera-se do dicionário o produto com a mesma chave primária que o obtido da base de dados. Isto demonstra que os produtos guardados receberam efetivamente uma chave primária;
- linha 12: verifica-se se os dois produtos são idênticos. Recorde-se que a classe [Produit] definiu um método [equals] (ver parágrafo 3.3.4);
- linha 13: elimina-se do dicionário o elemento encontrado;
- linha 16: verifica-se se o dicionário dos 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 [affiche] da linha 8 é o seguinte método privado:
// exibição da lista de produtos
private <T> void affiche(List<T> elements) throws JsonProcessingException {
for (T element : elements) {
System.out.println(jsonMapper.writeValueAsString(element));
}
}
- linha 2: o método [affiche] é um método genérico. É parametrizado por um tipo T, sintaxicamente denotado por <T>. Se fosse parametrizado por dois tipos, T1 e T2, escrever-se-ia <T1,T2>. A sintaxe de um método m parametrizado por um tipo T é a seguinte:
No código do método m, encontraremos dados do tipo T. O método m de uma instância c de uma classe C pode, então, ser chamado da seguinte forma:
onde T1 é o tipo efetivo que substituirá o tipo formal T do método m. Na maioria das vezes, o compilador consegue deduzir o tipo T1 com base nos argumentos do método m. Assim, a instrução anterior será, na maioria das vezes, simplificada para:
Voltemos ao método [affiche]. Este apresenta uma lista de elementos do tipo T. Isto é possível porque o mapeador jSON utilizado na linha 4 é capaz de gerar a representação jSON de qualquer tipo de objeto. Neste exemplo específico, o único tipo T utilizado será o tipo [Produit].
O método [affiche] também poderia ter sido escrito da seguinte forma:
// exibição da lista de produtos
private void affiche(Object o) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(o));
}
Sendo o parâmetro efetivo uma lista de produtos, a linha 3 teria gerado a representação jSON dessa lista. Isto não é o mesmo que gerar, uma a uma, a representação de cada um dos seus elementos.
A exibição produzida pelo teste [getProduits] é a seguinte:
3.6.9.4. O teste [getProduitBy]
Este é o seguinte:
@Test
public void getProduitBy() {
// preenchimento
fill(10);
log("getProduitBy", 1);
Produit produit = getDao().getProduitByName("NOM3");
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
- linha 6: o método [getProduitByName] da interface [IDao] é utilizado para recuperar o produto com o nome [NOM3];
- linha 7: o método [getProduitById] da interface [IDao] é, em seguida, utilizado para recuperar o mesmo produto, identificado desta vez pela sua chave primária;
- linhas 8-10: verifica-se se [produit2] e [produit] 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);
// é efetuada a inserção
List<Produit> inserts = new ArrayList<Produit>();
inserts.add(new Produit(0, "x", 1, 1.0, ""));
inserts.add(new Produit(0, "x", 1, 1.0, ""));
boolean erreur = false;
try {
getDao().addProduits(inserts);
} catch (DaoException daoException) {
erreur = true;
}
// verificações
Assert.assertTrue(erreur);
List<Produit> produits = getDao().getAllProduits();
Assert.assertEquals(0, produits.size());
}
- linhas 5-7: cria-se uma lista com dois produtos com o mesmo nome, [x];
- linha 10: estes dois produtos são inseridos na tabela [PRODUITS], que está vazia (método [clean] anotado com [@Before]). A primeira inserção será efetuada, mas não a segunda, uma vez que a tabela [PRODUITS] tem uma restrição de unicidade no nome dos produtos. Por conseguinte, deve ocorrer uma exceção. Esta é testada na linha 15;
- uma vez que todos os métodos da interface [IDao] são executados no interior de uma transação, o facto de a segunda inserção falhar irá anular toda a transação e, consequentemente, a primeira inserção. No final, não deve ocorrer nenhuma inserção na tabela [PRODUITS];
- linhas 16-17: verifica-se este ponto solicitando a lista de produtos contidos na tabela [PRODUITS] e verificando se essa lista está vazia;
3.6.9.6. O teste [updateProduits]
Este é o seguinte:
@Test
public void updateProduits() {
// preenchimento
fill(10);
log("Mise à jour du prix des produits de catégorie 1", 1);
// recuperação dos produtos
List<Produit> produits = getDao().getAllProduits();
// atualização dos da categoria 1
List<Produit> updated = new ArrayList<Produit>();
int nbUpdated = 0;
for (Produit produit : produits) {
if (produit.getCategorie() == 1) {
// int id, String nome, int categoria, double preço, String descrição
updated
.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
nbUpdated++;
}
}
int nbProduits = getDao().updateProduits(updated);
// verificações
// Assert.assertEquals(nbUpdated, nbProduits); -- não funciona com DB2
for (Produit produit : updated) {
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
}
}
- linha 4: insere-se 10 produtos na base de dados;
- linha 7: recuperam-se esses produtos;
- linhas 9-18: aumentam-se em 10% os preços dos produtos da categoria n.º 1;
- linha 19: estas alterações são registadas na base de dados;
- linhas 22-25: percorre-se na memória a lista de produtos que serviu para a atualização. Para cada um deles, procura-se na base de dados o produto com a mesma chave primária e verifica-se se a atualização do preço ocorreu efetivamente;
- linha 19: recupera-se o número de produtos atualizados pela operação [updateProduits];
- linha 21: verifica-se se esse número corresponde ao esperado. Este teste é bem-sucedido para todos os SGBD, exceto para o SGBD e o DB2. Por isso, estes foram colocados em comentários;
3.6.9.7. O teste [deleteProduits]
Este é o seguinte:
@Test
public void deleteProduits() {
// preenchimento
fill(10);
log("deleteProduits", 1);
// lista de produtos
List<Produit> produits = getDao().getAllProduits();
// eliminação de dois produtos
Produit produit0 = produits.get(0);
Produit produit5 = produits.get(5);
int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
// verificações
// Assert.assertEquals(2, nbDeleted); -- não funciona com DB2
Assert.assertNull(getDao().getProduitById(produit0.getId()));
Assert.assertNull(getDao().getProduitById(produit5.getId()));
Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
- linha 4: inserimos 10 produtos na base de dados;
- linhas 7-11: recuperam-se todos os produtos da base de dados e eliminam-se dela os produtos recuperados nas posições 0 e 5;
- linhas 14-16: verifica-se se os dois produtos já não se encontram na base de dados e se esta tem menos dois produtos;
- o teste da linha 13 não é bem-sucedido com o SGBD DB2. É bem-sucedido com os outros SGBD;
3.6.9.8. Os testes de desempenho
Incluímos nos testes três métodos cujo único objetivo é avaliar o desempenho do SGBD:
@Test
public void perf1() {
// preenchimento
fill(10000);
}
@Test
public void perf2() {
// preenchimento
fill(10000);
// alteração
List<Produit> produits = getDao().getAllProduits();
// atualizamos os da categoria 1
List<Produit> updated = new ArrayList<Produit>();
for (Produit produit : produits) {
// int id, String nome, int categoria, double preço, String descrição
updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
}
getDao().updateProduits(updated);
}
@Test
public void perf3() {
// preenchimento
fill(10000);
// eliminação
List<Produit> produits = getDao().getAllProduits();
// chaves primárias
int[] keys = new int[produits.size()];
for (int i = 0; i < keys.length; i++) {
keys[i] = produits.get(i).getId();
}
getDao().deleteProduits(keys);
}
- linhas 1-5: inserção de 10 000 produtos;
- linhas 8-20: inserção de 10 000 produtos e, em seguida, modificação dos mesmos através das suas chaves primárias;
- linhas 23-34: inserção de 10 000 produtos e, em seguida, eliminação dos mesmos através das suas chaves primárias;
Para executar os testes [JUnitTestDao1] e [JUnitTestDao2], podem ser utilizadas as seguintes configurações de execução:
![]() | ![]() |
Os resultados do teste [JUnitTestDao1] são os seguintes:
![]() |
No [1], os resultados correspondem aos do [JUnitTestDao1] e, no [2], aos do [JUnitTestDao2]. Não existem diferenças significativas entre eles. No [1]:
- o teste foi bem-sucedido;
- 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 demorada é a inserção;



















































