4. Introdução ao Spring JDBC
Neste capítulo, vamos estudar a seguinte arquitetura:
![]() |
Trata-se, portanto, da mesma arquitetura da anterior. Vamos introduzir duas modificações:
- o banco de dados terá duas tabelas ligadas por uma relação de chave estrangeira;
- a camada [DAO] será implementada com a biblioteca [Spring JDBC], que facilita o gerenciamento do API e do JDBC;
4.1. Configuração do ambiente de trabalho
Com o STS, importe o projeto [spring-jdbc-04], localizado na pasta [<exemples>/spring-database-generic/spring-jdbc]
![]() |
Além disso, precisamos criar um novo banco de dados MySQL com o cliente [MyManager] (ver parágrafo 3.1):
![]() |
- em [3]; os exemplos a seguir utilizam uma base de dados MySQL chamada [dbproduitscategories];
![]() |
- em [9], insira a senha do usuário root (essa senha é root neste documento);
![]() |
![]() |
- em [18], o banco de dados [dbproduitscategories] foi criado vazio. Criam-se tabelas e ele é preenchido com um script SQL [19-20];
![]() |
- no [21], selecione a pasta [<exemples>/spring-database-config/mysql/databases];
![]() |
- em [25], certifique-se de estar na base [dbproduitscategories] e não na base [dbproduits];
- em [29], o script SQL criou cinco tabelas. As tabelas [ROLES, USERS, USERS_ROLES] só serão utilizadas quando abordarmos a segurança do serviço web criado para expor o banco de dados [dbproduitscategories] na web;
4.2. O banco de dados [dbproduitscategories]
O banco de dados [dbproduitscategories] é uma extensão do banco [dbproduits] analisado anteriormente. Enquanto na tabela [PRODUITS] o produto possuía uma categoria identificada por um número sem significado específico, aqui esse número será uma chave estrangeira na tabela [CATEGORIES].
A tabela [PRODUITS] é a seguinte:
![]() |
- [ID]: a chave primária autoincrementada da tabela [2];
- [NOM]: o nome exclusivo do produto [4];
- [PRIX]: o preço do produto;
- [DESCRIPTION]: a descrição do produto;
- [VERSIONING] é o número de versão do produto. Sua versão inicial é 1 [3]. Sempre que o produto for modificado, seu número de versão será incrementado pelo código que opera a tabela;
- [CATEGORIE_ID]: a chave estrangeira na tabela [CATEGORIES] para indicar a categoria à qual o produto pertence;
![]() |
- em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Ela tem como alvo a coluna [ID] da tabela [CATEGORIES] [4-5];
- quando uma categoria é excluída, todos os produtos a ela vinculados também são excluídos ([6]). É importante observar esse ponto, pois ele é utilizado na construção da camada [DAO], que utiliza a base [dbproduitscategories];
A tabela [CATEGORIES] de categorias é a seguinte:
![]() |
- [ID]: chave primária autoincrementada;
- [VERSIONING]: número da versão da categoria;
- [NOM]: nome exclusivo da categoria;
4.3. O projeto Eclipse
![]() |
O projeto [spring-jdbc-04] implementa a seguinte arquitetura:
![]() |
O projeto [spring-jdbc-04] é um projeto Maven configurado pelo seguinte arquivo [pom.xml]:
![]() |
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jdbc-generic-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-04</name>
<description>Demo project for Spring JdbcTemplate</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>
<!-- Spring JdbcTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</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 se baseia no projeto [mysql-config-jdbc], que configura a camada JDBC;
- linhas 34-37: o artefato [spring-boot-starter-jdbc] inclui as bibliotecas do Spring JDBC;
No final, as dependências são as seguintes:
![]() |
4.4. Configuração do Spring
![]() |
A classe [AppConfig], que configura o projeto Spring, é a seguinte:
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;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
@EnableTransactionManagement
@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_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// conexões abertas inicialmente
dataSource.setInitialSize(5);
// resultado
return dataSource;
}
// Gerenciador de transações
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// JdbcTemplate
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
// inserção de produto
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
ConfigJdbc.TAB_PRODUITS_ID);
}
// adição de categoria
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
ConfigJdbc.TAB_CATEGORIES_ID);
}
}
- linha 16: a classe é uma classe de configuração do Spring;
- linha 17: o pacote [spring.jdbc.dao] será verificado em busca de outros componentes Spring além daqueles presentes na classe [AppConfig]. Nele será encontrado o componente que implementa a camada [DAO];
- linha 18: não vamos gerenciar as transações nós mesmos, mas deixá-las a cargo do Spring JDBC. A única coisa a fazer será anotar os métodos que devem ser executados em uma transação com a anotação Spring [@Transactional]. A linha 18 garante que essa anotação seja gerenciada e não ignorada. O gerenciamento das transações é garantido por uma das dependências do projeto Spring JDBC, importado pelo arquivo [pom.xml];
- linha 19: importam-se os beans já definidos na classe [generic.jdbc.config.ConfigJdbc] do projeto [mysql-config-jdbc];
- linhas 23-36: a fonte de dados [tomcat-jdbc] introduzida no exemplo [spring-jdbc-02];
- linhas 40-42: o gerenciador de transações vinculado à fonte de dados definida anteriormente. O bean deve se chamar obrigatoriamente [transactionManager], pois é esse nome que é utilizado pela anotação [@EnableTransactionManagement]. O gerenciador [DataSourceTransactionManager] é fornecido pela biblioteca Spring JDBC (linha 12);
- linhas 45-48: o bean [namedParameterJdbcTemplate], no qual se baseará a implementação da camada [DAO]. Esse bean é fornecido pela biblioteca Spring JDBC (linha 10). Esse bean também está vinculado à fonte de dados definida anteriormente (linha 47);
- linhas 51-55: o bean [simpleJdbcInsertProduit] (nome livre) será utilizado para inserir um produto na tabela [PRODUITS] e recuperar a chave primária gerada. Os diversos parâmetros utilizados são os seguintes:
- [dataSource]: a fonte de dados [tomcat-jdbc] das linhas 24-36;
- [ConfigJdbc.TAB_PRODUITS]: a tabela [PRODUITS];
- [ConfigJdbc.TAB_CATEGORIES_ID]: a coluna-chave primária da tabela [PRODUITS]. Vale lembrar que, para PostgreSQL, o nome dessa coluna deverá estar em letras minúsculas;
- linhas 58-62: o bean [simpleJdbcInsertCategorie] será utilizado para inserir uma categoria na tabela [CATEGORIES] e recuperar a chave primária gerada;
4.5. As exceções do projeto
![]() |
Já vimos as classes [UncheckedException, DaoException, ShortException] no projeto [spring-jdbc-03]. Adicionamos uma nova:
package spring.jdbc.infrastructure;
public class MyIllegalArgumentException extends UncheckedException {
private static final long serialVersionUID = 1L;
// fabricantes
public MyIllegalArgumentException() {
super();
}
public MyIllegalArgumentException(int code, Throwable e, String className) {
super(code, e, className);
}
}
- a classe [MyIllegalArgumentException] deriva da classe [UncheckedException] e, portanto, é uma classe não controlada. Ela será usada para sinalizar uma chamada com argumentos incorretos de um método da camada [DAO]. Não a chamamos de [IllegalArgumentException] porque essa exceção já existe na JDK e isso, às vezes, fazia com que o compilador gerasse uma [import] incorreta;
4.6. As entidades do projeto
![]() |
As classes do pacote [spring.jdbc.entities] correspondem às representações das linhas das tabelas do banco de dados [dbproduitscategories]. Por enquanto, ignoraremos as representações das tabelas [USERS, ROLES, USERS_ROLE].
Todas as entidades estendem a classe pai [AbstractCoreEntity]:
package spring.jdbc.entities;
public abstract class AbstractCoreEntity {
// imóveis
protected Long id;
protected Long version;
// construtoras
public AbstractCoreEntity() {
}
public AbstractCoreEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
public AbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
public void setAbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
// ------------------------------------------------------------
// redefinição de [equals] e [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
return id != null && other.id != null && id.equals(other.id);
}
// getters e setters
...
}
- linha 5: o campo [id] será associado à coluna [ID], chave primária das tabelas;
- linha 6: o campo [version] será associado à coluna [VERSIONING] das tabelas;
- linhas 8-26: diversos construtores e métodos para criar ou inicializar um objeto [AbstractCoreEntity];
- linhas 35-47: o método [equals] determina que dois objetos [AbstractCoreEntity] são iguais se tiverem o mesmo campo [id]. É importante lembrar aqui que os objetos [AbstractCoreEntity] serão representações de linhas de tabelas nas quais [id] é a chave primária e, portanto, não pode haver duas linhas com o mesmo [id];
- linhas 30-33: uma proposta de [hashCode];
A classe [Produit] será a imagem de uma linha da tabela [PRODUITS]:
package spring.jdbc.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
// propriedades
private String nom;
private Long idCategorie;
private double prix;
private String description;
private Categorie categorie;
// construtores
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
super(id, version);
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// assinatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// getters e setters
...
}
- linha 6: a classe [Produit] estende a classe [AbstractCoreEntity];
- linhas 8-12: os campos [id, version, nom, idCategorie, prix, description] são as imagens das colunas [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] da tabela [PRODUITS];
- linha 12: o objeto do tipo [Categorie] com chave primária [idCategorie]. Esse campo poderá ou não ser preenchido, dependendo do caso. Quando preenchido, trata-se do produto na versão longa [LongProduit]; caso contrário, do produto na versão curta [ShortProduit];
- linha 5: um filtro jSON. Vale lembrar que o projeto [mysql-config-jdbc] inclui uma biblioteca jSON. A necessidade do filtro decorre do fato de que o campo [categorie] pode ou não ser preenchido. Nesse caso, a representação jSON do produto difere. Para lidar com esses dois casos, configuraremos o filtro [jsonFilterProduit] na linha 5. Um filtro jSON permite especificar, de forma dinâmica, os campos a serem excluídos da representação jSON. Quando for constatado que o campo [categorie] não foi preenchido, ele será excluído da representação jSON do produto;
A classe [Categorie] é a representação de uma linha da tabela [CATEGORIES]:
package spring.jdbc.entities;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
// propriedades
private String nom;
public List<Produit> produits;
// construtores
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
super(id, version);
this.nom = nom;
this.produits = produits;
}
// assinatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// métodos
public void addProduit(Produit produit) {
// adição de um produto
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// adicionamos o produto
produits.add(produit);
// definindo a categoria
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// getters e setters
...
}
- linha 9: a classe [Categorie] estende a classe [AbstractCoreEntity];
- linha 12: os campos [id, version, nom] são as imagens das colunas [ID, VERSIONING, NOM] da tabela [CATEGORIES];
- linha 13: o campo [produits] representa a lista de produtos da categoria. Esse campo nem sempre é preenchido. Quando não estiver preenchido, falaremos de categoria na versão curta [ShortCategorie]; caso contrário, de categoria na versão longa [LongCategorie];
- linhas 32-44: o método [addProduit] permite adicionar um produto à categoria (linha 39) e definir, no produto adicionado, as características de sua categoria (idCategorie e categoria);
- linha 8: um filtro jSON. Quando a biblioteca jSON precisar serializar/deserializar um objeto [Categorie], será necessário indicar a ela como lidar com o filtro denominado [jsonFilterCategorie];
4.7. A interface Idao<T>
![]() |
![]() |
A interface [IDao] da camada [DAO] possui a seguinte assinatura:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// lista de todas as entidades T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// de entidades específicas – versão resumida
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// de entidades específicas — versão longa
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// atualização de várias entidades
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// exclusão de todas as entidades
public void deleteAllEntities();
// remoção de várias entidades
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
- linha 7: temos aqui uma interface [IDao] parametrizada por um tipo T com uma condição: esse tipo deve estender a classe [AbstractCoreEntity] ou implementar a interface [AbstractCoreEntity]. A palavra-chave [extends] é utilizada para ambos os casos. Aqui, T será instanciado pelo tipo [Produit] ou pelo tipo [Categorie]. De fato, percebe-se rapidamente que se realiza o mesmo tipo de operações (inserção, modificação, exclusão, seleção) nos tipos [Produit] e [Categorie]. Parece, então, lógico agrupar esses métodos em uma interface genérica;
- dependendo do caso, os termos [LongEntity] e [ShortEntity] se referem a situações diferentes:
- quando T é do tipo [Produit]:
- [ShortEntity] é o produto sem o campo [Categorie categorie] preenchido;
- [LongEntity] é o produto com o campo [Categorie categorie] preenchido;
- quando T é do tipo [Categorie]:
- [ShortEntity] é a categoria sem o campo [List<Produit> produits] preenchido;
- [LongEntity] é o produto com o campo [List<Produit> produits] preenchido;
- quando T é do tipo [Produit]:
Portanto, temos uma interface com 19 métodos. A maioria dos métodos existe em duplicata. Tomemos como exemplo o método [getShortEntitiesById]:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- linhas 1 e 3: o parâmetro é a lista das chaves primárias das entidades cuja versão resumida se deseja obter. Essa lista é apresentada de duas formas diferentes:
- linha 1: uma lista que implementa a interface [Iterable<Long>]. O tipo [List<Long>] implementa essa interface, mas há muitos outros. Se tivéssemos colocado [List<Long> ids], isso teria sido suficiente para nossos exemplos, mas estaríamos obrigando o usuário de nossos exemplos a fazer conversões caso seu parâmetro não fosse do tipo exato esperado;
- linha 3: infelizmente, o tipo Long[] não implementa a interface [Iterable<Long>]. Nesse caso, usaremos a versão da linha 3. O parâmetro formal [Long... ids] (3 pontos) pode receber o valor tanto de um array quanto de uma sequência de IDs: getShortEntitiesById(id1, id2, ...);
É essa mesma interface IDao<T> que será implementada pela seguinte arquitetura:
![]() |
onde uma camada [JPA] (Java Persistence API) será inserida entre a camada [DAO] e o driver JDBC do SGBD. Isso nos permitirá ter uma camada de testes comum às duas arquiteturas. Em ambos os casos, a camada [DAO] apresentará duas interfaces:
- IDao<Produto> para acessar a tabela [PRODUITS];
- IDao<Categoria> para acessar a tabela [CATEGORIES];
4.8. Implementação da interface IDao<T>
![]() |
- a interface IDao<Produto> é implementada pela classe [DaoProduit];
- a interface IDao<Categoria> é implementada pela classe [DaoCategorie];
As classes [DaoProduit] e [DaoCategorie] estendem ambas a classe abstrata [AbstractDao] e a seguir:
package spring.jdbc.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;
import spring.jdbc.entities.AbstractCoreEntity;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T extends AbstractCoreEntity> implements IDao<T> {
// injeções
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// local
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// validade do argumento
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// obtenção em parcelas
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// resultado
return entities;
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// validade do argumento
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// resultado
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(String... names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Iterable<Long> ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Long... ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(String... names) {
...
}
@Override
@Transactional
public List<T> saveEntities(Iterable<T> entities) {
...
}
@Override
@Transactional
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities) {
...
}
@Override
public void deleteEntitiesById(Iterable<Long> ids) {
...
}
@Override
public void deleteEntitiesById(Long... ids) {
...
}
@Override
public void deleteEntitiesByName(Iterable<String> names) {
...
}
@Override
public void deleteEntitiesByName(String... names) {
...
}
@Override
public void deleteEntitiesByEntity(Iterable<T> entities) {
...
}
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities) {
...
}
protected void deleteEntitiesByEntity(List<T> entities) {
...
}
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
// métodos privados ----------------------------------------------
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
}
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
// métodos protegidos ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
- linha 15: a classe [AbstractDao] é abstrata (palavra-chave abstract). Por isso, ela não pode ser instanciada. Ela só pode ser derivada. Essa classe tem várias funções:
- definir a natureza da transação na qual cada método é executado;
- fazer o máximo possível de coisas em comum às duas implementações das interfaces [IDao<Produit>] e [IDao<Categorie>]. Trata-se principalmente de verificar a validade dos argumentos. Não serão aceitos argumentos null nem listas vazias;
- uniformizar o tipo dos parâmetros T... params e Iterable<T> params em um único: List<T> params;
- delegar o trabalho às classes filhas assim que ele se tornar específico a uma das duas interfaces;
Graças à padronização dos parâmetros dos diferentes métodos realizada pela classe [AbstractDao], as classes filhas [DaoProduit] e [DaoCategorie] terão apenas 10 métodos a serem implementados, em vez de 19:
// métodos implementados pelas classes filhas ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
Vejamos alguns métodos da classe [AbstractDao].
Método [getShortEntitiesById]
Este método tem como objetivo obter a versão resumida de entidades cujas chaves primárias são fornecidas.
// injeções
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// local
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- linhas 2-4: injeta-se o bean [maxPreparedStatementParameters], definido no arquivo de configuração [ConfigJdbc], que configura a camada JDBC de um SGBD específico:
// número máximo de parâmetros de um [PreparedStatement]
public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
@Bean(name = "maxPreparedStatementParameters")
public int maxPreparedStatementParameters() {
return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
- linhas 1-7: definem o bean [maxPreparedStatementParameters], que determinará o número máximo de parâmetros que poderão ser fornecidos a um tipo [PreparedStatement]. Essa necessidade não surgiu com o SGBD e o MySQL, que aceitaram 10.000 parâmetros para um tipo [PreparedStatement]. Durante os testes com o servidor SGBD e SQL, este gerou uma exceção indicando que o número máximo de parâmetros para um tipo [PreparedStatement] era de 2.100. Portanto, esse número passou a ser um parâmetro da configuração dos diversos SGBD. Ele deve, portanto, ser inserido no projeto de configuração [sgbd-config-jdbc] de cada SGBD;
Voltemos ao código do método [getShortEntitiesById]:
// injeções
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// local
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- linha 7: o nome da classe. É utilizado como parâmetro de um dos construtores da classe de exceção [DaoException];
- linha 10: a anotação [@Transactional(readOnly = true)] indica que o método deve ser executado em uma transação somente leitura. Pode-se questionar a utilidade de tal transação, uma vez que o método realiza apenas leituras e, portanto, em caso de falha, não há nada a ser revertido. É o autor da biblioteca [Spring Data] quem recomenda isso e explica o motivo. Segui seu conselho;
O corpo do método é o seguinte:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// validade do argumento
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
...
}
- linha 5: a validade do parâmetro [ids] é verificada pelo método a seguir:
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
// elementos nulos?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
}
// elementos vazios?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T>();
}
}
// resultado padrão
return null;
}
- linha 1: o método [checkNullOrEmptyArgument] é um método genérico parametrizado pelo tipo <T2>. T2 é o tipo dos elementos passados como segundo parâmetro do método. Pode ser [Long, String, AbstractCoreEntity];
- linha 1: o método [checkNullOrEmptyArgument] aceita dois parâmetros:
- [Iterable<T2> elements]: o parâmetro a ser testado;
- [checkEmpty]: deve ser verdadeiro se for necessário verificar se o parâmetro anterior é uma lista não vazia;
- linhas 4-6: verifica-se se o parâmetro [elements] não é null. Caso contrário, é lançada uma exceção do tipo [MyIllegalArgumentException];
- linhas 8-15: se a lista estiver vazia e for necessário verificar se ela não está vazia, lança-se uma exceção do tipo [MyIllegalArgumentException];
- linha 13: se a lista estiver vazia e não for necessário verificar se ela não está vazia, retorna-se uma lista vazia de elementos do tipo T. A interface [Iterable<T2>] possui um método [iterator()] que permite iterar sobre os elementos da lista que implementam a interface. Dois métodos desse iterador são úteis:
- [itérateur].hasNext(): retorna verdadeiro se a lista ainda tiver um elemento a ser processado; falso caso contrário;
- [iterateur].next(): retorna o elemento atual da lista e avança um elemento;
- por fim,
- se o argumento [T2... elements] for null ou estiver vazio, é lançada uma exceção do tipo [MyIllegalArgumentException];
- se o argumento [T2... elements] for uma lista vazia e isso for válido, então é retornada uma lista vazia de elementos do tipo T;
Existe um método análogo quando o argumento a ser testado é do tipo [T2... elements]:
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
Voltemos ao código do método [getShortEntitiesById]:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// validade do argumento
List<T> entities = checkNullOrEmptyArgument(true, ids);
// obtenção por parcelas
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// resultado
return entities;
}
- linha 7: se chegarmos até aqui, é porque o argumento [Iterable<Long> ids] é válido;
- linhas 7-14: veremos mais adiante que o método [getShortEntitiesById] será implementado por um tipo [PreparedStatement], que terá como parâmetros a lista de chaves primárias a serem pesquisadas. Por exemplo:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
: ids é um parâmetro cujo valor efetivo será um tipo List<Long>. Cada elemento dessa lista será objeto de um parâmetro ? em um tipo [PreparedStatement]. Ora, já dissemos que esse tipo aceita um número máximo de parâmetros, número definido pelo campo [maxPreparedStatementParameters] da classe;
- linha 7: a lista de entidades T que será retornada pelo método [getShortEntitiesById]. Essa lista será construída em blocos de [maxPreparedStatementParameters] elementos;
- linha 9: a partir do argumento [Iterable<Long> ids], cria-se um tipo [List<Long> listIds]. A classe [Lists] é uma classe da biblioteca Google Guava que oferece diversos métodos estáticos para manipular coleções de objetos. A biblioteca Google Guava foi importada (pom.xml) pelo projeto Maven [mysql-config-jdbc]:
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- linha 10: o número de entidades T a serem pesquisadas no banco de dados;
- linhas 11-13: elas são pesquisadas em grupos de [taille = maxPreparedStatementParameters] elementos;
- linha 12: um cálculo para evitar ultrapassar o fim da lista [listIds];
- linha 13: as entidades T são obtidas pela chamada [getShortEntitiesById(listIds.subList(i, limit))]. Esse método é definido na classe por:
abstract protected List<T> getShortEntitiesById(List<Long> ids);
Portanto, é a classe filha que irá buscar as entidades T no banco de dados:
- [DaoProduit] se T for do tipo [Produit];
- [DaoCategorie] se T for do tipo [Categorie];
O benefício desse trabalho da classe pai é duplo:
- a assinatura do método [getShortEntitiesById] na classe filha é única: seu argumento é do tipo [List<Long> ids];
- a classe filha não precisa se preocupar com o problema dos parâmetros [maxPreparedStatementParameters] de um [PreparedStatement]. Sua classe pai já cuidou disso por ela;
- linha 13: as entidades retornadas pela classe filha são acumuladas na lista de entidades que será retornada pela classe pai (linha 16);
Agora, vamos ver a implementação do outro método [getShortEntitiesById] da classe:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// validade do argumento
List<T> entities = checkNullOrEmptyArgument(true, ids);
// resultado
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
- linha 3: a natureza do argumento mudou: Long... ids;
- linha 5: a validade desse argumento é verificada;
- linha 7: é chamado o método [getShortEntitiesById] que acabamos de descrever. Mais uma vez, utilizamos a classe [Lists] da biblioteca [Google Guava]. Observe que é necessário fazer um cast explícito para o tipo [Iterable<Long>] para ajudar o compilador a escolher o método correto, pois o método [getShortEntitiesById] possui três assinaturas na classe:
- List<T> getShortEntitiesById(Long... ids);
- List<T> getShortEntitiesById(Iterable<Long> ids);
- List<T> getShortEntitiesById(List<Long> ids), que é abstrata e implementada pela classe filha;
Não faremos mais comentários sobre a classe abstrata [AbstractDao], classe pai das classes [DaoProduit] e [DaoCategorie]. Basta lembrar que, às vezes, é interessante fatorar comportamentos comuns a várias classes em uma classe pai, seja ela abstrata ou não. Após esse trabalho, as classes filhas precisam implementar apenas os seguintes métodos:
// métodos implementados pelas classes filhas ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
O código do parágrafo 4.8 mostra os diferentes tipos de transação utilizados para cada método. Destacamos alguns pontos:
- os métodos que leem o banco de dados são anotados com [@Transactional(readOnly = true)];
- os métodos que modificam o banco de dados são anotados com [@Transactional];
- os métodos [delete] não são anotados e, portanto, não ocorrem em uma transação. A ideia é que, se uma exclusão falhar, o usuário provavelmente não desejará cancelar todas as que foram bem-sucedidas anteriormente;
4.9. A classe [DaoCategorie]
![]() |
![]() |
A classe [DaoCategorie] implementa a interface [IDao<Categorie>], que garante oacesso aos dados da tabela [CATEGORIES] do banco de dados MySQL [dbproduitscategories]. Sua estrutura é a seguinte:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
// constantes
// injeções
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
@Autowired
private IDao<Produit> daoProduit;
@Override
public List<Categorie> getAllShortEntities() {
...
}
@Override
public List<Categorie> getAllLongEntities() {
...
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
...
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
...
}
// --------------------- mappers
class ShortCategorieMapper implements RowMapper<Categorie> {
....
}
class LongCategorieMapper implements RowMapper<Categorie> {
....
}
- linha 28: a classe [DaoCategorie] é um componente Spring e, como tal, poderá ser injetada em outros componentes Spring;
- linha 29: a classe [DaoCategorie] estende a classe abstrata [AbstractDao<Categorie>], o que a torna uma implementação da interface [IDao<Categorie>];
- linhas 34-37: injeção de beans definidos na classe [AppConfig] descrita no parágrafo 4.4;
- linhas 38-39: injeção de uma referência à classe [DaoProduit], que implementa a interface [IDao<Produit>], responsável pelo gerenciamento do acesso aos dados da tabela [PRODUITS];
- linhas 41-89: implementação da interface [IDao<Categorie>];
- linhas 95-101: duas classes internas que implementam a interface [RowMapper<T>];
Vamos examinar os métodos, um por um.
4.9.1. O método [getAllShortEntities]
O método [getAllShortEntities] retorna todas as categorias da tabela [CATEGORIES] em sua versão abreviada:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Todos os métodos se baseiam no objeto [namedParameterJdbcTemplate], definido no arquivo de configuração do Spring e fornecido pela biblioteca Spring JDBC. Esse objeto possui vários métodos. O utilizado acima é o seguinte:
![]()
- [sql] é a instrução SQL a ser executada;
- [rowMapper] é uma instância da seguinte interface [RowMapper<T>]:

A ideia é a seguinte:
- o método [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) executa a instrução SQL do tipo [Select]. Ela lida com eventuais exceções, bem como com a abertura e o fechamento da conexão com o SGBD. A única coisa que ela não pode fazer éencapsular os elementos do [ResultSet] dos objetos que ela obtém em um tipo [Categorie], pois ela não conhece a relação existente entre os campos do tipo [Categorie] e as colunas do [Resultset]. Veremos posteriormente que essa relação é criada com a tecnologia JPA, o que tornará automática a encapsulação dos elementos de um [ResultSet] em instâncias do tipo T. Por enquanto, o segundo parâmetro do método [query] é uma instância da interface [RowMapper<T>] capaz de realizar esse encapsulamento;
Voltemos ao código:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
A ordem SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] é a seguinte:
public final static String SELECT_ALLSHORTCATEGORIES = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c";
A consulta solicita as colunas [ID, VERSIONING, NOM] dos elementos da tabela [CATEGORIES]. Utilizaremos sistematicamente a sintaxe:
SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...
O importante é a nomenclatura das colunas obtidas pelo SELECT com o atributo [as nom_colonne]. Essa é a única maneira de garantir a portabilidade entre os SGBD, pois todos eles possuem uma forma proprietária de nomear as colunas obtidas por um SELECT, no qual colunas de tabelas diferentes têm o mesmo nome (ID, NOM ou VERSIONING, por exemplo, no nosso caso). Assim, eliminamos essa ambiguidade indicando nós mesmos o nome que essas colunas devem ter.
A classe interna [ShortCategorieMapper] é a seguinte:
class ShortCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSIONING"), rs.getString("c_NOM"), null);
}
}
- linha 1: a classe [ShortCategorieMapper] implementa a interface [RowMapper<Categorie>] e, por isso, deve implementar o método [mapRow] das linhas 4a 5, cuja função é encapsular uma linha do [ResultSet rs] gerada pela ordem [SELECT] em um tipo [Categorie];
- linha 5: esse encapsulamento é realizado. Observe-se que o nome utilizado pelos métodos [rs.getType(nom)] é o mesmo nome usado nos atributos [as nom] das colunas do SELECT;
Assim, obtivemos a lista de categorias em sua versão abreviada, sem precisar lidar com exceções nem com conexões. Esse é o benefício da biblioteca Spring JDBC, que gerencia tudo o que pode ser padronizado no gerenciamento dos elementos de uma tabela e deixa para o desenvolvedor o que não pode ser padronizado.
4.9.2. O método [getAllLongEntities]
O método [getAllLongEntities] retorna todas as categorias da tabela [CATEGORIES] em sua versão completa:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
A ordem SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] é a seguinte:
public final static String SELECT_ALLLONGCATEGORIES = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID";
O objetivo é associar as categorias aos seus produtos. Isso é obtido por meio de uma junção da tabela [CATEGORIES] com a tabela [PRODUITS] por meio da chave estrangeira [CATEGORIE_ID], queliga a tabela [PRODUITS] à tabela [CATEGORIES]. A sintaxe [FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID] permite também recuperar as categorias que não possuem produtos associados. Nesse caso, a consulta SELECT retorna uma categoria e um produto com todas as suas colunas para NULL.
A classe [LongCategorieMapper] é a seguinte:
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// caso da categoria sem produtos
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
- linha 4: o método [mapRow] deve retornar um objeto [Categorie] com seu campo [produits] preenchido, a partir de uma linha do [ResultSet] proveniente da ordem SELECT anterior;
No final, a operação:
[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]
retornará uma lista do tipo:
onde cada categoria [ci] terá um campo [produits] que será uma lista de produtos contendo um único elemento [produitsij]. No entanto, precisamos da seguinte lista:
onde cada categoria [ci] terá um campo [produits] que conterá a lista de produtos [produiti1, produiti2, ...]. Isso é obtido passando a lista de categorias obtida para um método privado [filterCategories]:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
O método [filterCategories] é o seguinte:
private List<Categorie> filterCategories(List<Categorie> categories) {
if (categories.size() == 0) {
return categories;
}
// categorias a serem exibidas
List<Categorie> cats = new ArrayList<Categorie>();
// percorremos a lista de categorias obtidas
for (Categorie categorie : categories) {
boolean trouve = false;
for (Categorie cat : cats) {
if (categorie.equals(cat)) {
cat.addProduit(categorie.getProduits().get(0));
trouve = true;
break;
}
}
// encontrado?
if (!trouve) {
cats.add(categorie);
}
}
// resultado
return cats;
}
- linha 1: [List<Categorie> categories] é a lista de categorias a serem filtradas (ou agrupadas);
- linha 6: a lista de categorias a serem retornadas ao chamador;
- linhas 8-21: processa-se cada categoria da lista a ser filtrada;
- linhas 10-16: verifica-se se a categoria atual [categorie] já está presente na lista de categorias [cats] a ser construída (lembre-se de que duas categorias são consideradas iguais se tiverem a mesma chave primária, cf. parágrafo 4.6);
- linhas 11-14: se já for o caso, então o produto encapsulado em [categorie] é adicionado à lista de produtos de [cat];
- linhas 18-20: se a categoria atual [categorie] ainda não estiver presente na lista de categorias [cats] a ser criada, ela é adicionada a essa lista com sua lista de produtos, que contém um único elemento;
Vejamos o caso em que a ordem SQL Select retorna categorias sem produtos associados. Qual entidade retorna a classe [LongCategorieMapper]?
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// caso da categoria sem produtos
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
No caso em que a consulta SQL Select retornou uma categoria sem produtos, as colunas do produto retornado junto com a categoria contêm todas o valor SQL NULL. Esse caso é tratado nas linhas 7 a 9:
- linha 7: recupera-se a chave primária do produto como um inteiro longo;
- linha 9: verifica-se se o valor lido era SQL NULL (rs.wasNull). Se não for o caso, adiciona-se o produto à lista da linha 6; caso contrário, nada é adicionado e a lista de produtos permanece vazia.
Observe-se que, em todos os casos, é retornada uma categoria com um campo [produits] que não é null.
4.9.3. O método [getShortEntitiesById]
O método [getShortEntitiesById] é análogo ao método [getAllShortEntities], com a diferença de que retorna apenas as entidades cujas chaves primárias estão especificadas em uma lista:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
- na linha 4, a assinatura do método [query] utilizada é a seguinte:

O primeiro parâmetro é uma ordem SQL [Select] configurada. O segundo é um dicionário que associa cada um dos parâmetros a um valor. O terceiro é a instância da classe que realiza o encapsulamento de uma linha do [ResultSet], resultado do [Select], em um objeto do tipo T;
- linha 4: a ordem SQL [Select] configurada é a seguinte:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
Essa ordem extrai da tabela [CATEGORIES] as categorias cujas chaves primárias constam na lista: ids.
- linha 5: o segundo parâmetro do método [query] é, neste caso, um dicionário que associa a chave 'ids' (1º parâmetro) à lista [ids], passada na linha 1 como parâmetro para o método [getShortEntitiesById]. A classe [Collections] pertence à biblioteca [Google Guava], da qual já falamos. [Collections.singleMap] retorna um dicionário com um elemento;
- linha 5: a classe responsável por encapsular uma linha do [ResultSet] — resultado do [Select] — em um objeto do tipo [Categorie] é a classe [ShortCategorieMapper], já estudada;
É tipicamente aqui que entra em ação o bean [maxPreparedStatementParameters]. De fato, o parâmetro [:ids] da ordem SQL, que representa uma lista de chaves primárias, pode conter de 1 a vários milhares de parâmetros. Existe um limite para esse número, que depende de cada SGBD. Para o MySQL, conseguimos passar 10.000 parâmetros sem erros e não testamos valores superiores a esse. Para o SQL Server, o limite oficial é 2.100. Para o Firebird, 1.000 já era demais. Reduzimos para 100. De modo geral, não testamos o limite máximo desse número para os diferentes SGBD.
4.9.4. O método [getLongEntitiesById]
O método [getLongEntitiesById] é análogo ao método [getShortEntitiesById], com a diferença de que ele retorna as versões completas das categorias:
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
Na linha 4, a consulta SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] é a seguinte:
public final static String SELECT_LONGCATEGORIE_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.ID in (:ids)";
4.9.5. O método [getShortEntitiesByName]
O método [getShortEntitiesByName] é análogo ao método [getShortEntitiesById], com a diferença de que as categorias são pesquisadas por seus nomes, em vez de por suas chaves primárias:
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
Na linha 4, a ordem SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] é a seguinte:
public final static String SELECT_SHORTCATEGORIE_BYNAME = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.NOM in (:noms)";
4.9.6. O método [getLongEntitiesByName]
O método [getLongEntitiesByName] é análogo ao método [getShortEntitiesByName], exceto que as categorias são pesquisadas em suas versões completas:
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(215, e, simpleClassName);
}
}
Na linha 4, a ordem SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] é a seguinte:
public final static String SELECT_LONGCATEGORIE_BYNAME = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.NOM in(:noms)";
4.9.7. O método [deleteAllEntities]
O método [deleteAllEntities] exclui todas as categorias da tabela [CATEGORIES]:
@Override
public void deleteAllEntities() {
try {
// excluímos todas as categorias e, consequentemente, todos os produtos
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
- linha 4: o método [namedParameterJdbcTemplate.update] utilizado possui a seguinte assinatura:
![]()
O primeiro parâmetro é uma ordem SQL configurada para atualização (INSERT, UPDATE, DELETE). O segundo parâmetro é o dicionário que associa valores aos diferentes parâmetros da ordem SQL. O método retorna o número de linhas atualizadas pela ordem SQL.
- linha 4: a ordem SQL [ConfigJdbc.DELETE_ALLCATEGORIES] é a seguinte:
public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";
Portanto, não se trata de uma ordem configurada. É por isso que o segundo parâmetro do método [update] tem o valor null.
4.9.8. O método [deleteAllEntitiesById]
O método [deleteAllEntitiesById] exclui as categorias da tabela [CATEGORIES] cujas chaves primárias são passadas:
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYID, Collections.singletonMap("ids", ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
Na linha 4, a ordem SQL [ConfigJdbc.DELETE_CATEGORIESBYID] é a seguinte:
public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";
4.9.9. O método [deleteAllEntitiesByName]
O método [deleteAllEntitiesByName] exclui as categorias da tabela [CATEGORIES] cujos nomes são passados:
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYNAME, Collections.singletonMap("noms", names));
} catch (Exception e) {
throw new DaoException(225, e, simpleClassName);
}
}
Na linha 4, a ordem SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] é a seguinte:
public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";
4.9.10. O método [saveEntities]
4.9.10.1. O código
A assinatura desse método é a seguinte:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
O método recebe como parâmetro uma lista de categorias. Ele realiza as seguintes operações sobre ela:
- se a categoria tiver uma chave primária null, é realizada uma operação SQL INSERT; caso contrário, é realizada uma operação SQL UPDATE;
- essa operação é repetida para cada um dos produtos da categoria;
O método retorna a lista das categorias persistidas ou atualizadas. A lista retornada é o reflexo exato das categorias e produtos presentes nas tabelas, com exceção das versões: estas, de fato, não são modificadas nas entidades atualizadas, embora tenham sido incrementadas no banco de dados.
Este é, de longe, o método mais complexo. Seu código é o seguinte:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// --------------------------------------------- categorias
List<Categorie> insertCategories = new ArrayList<Categorie>();
List<Categorie> updateCategories = new ArrayList<Categorie>();
// as categorias são verificadas
for (Categorie categorie : entities) {
// inserir ou atualizar?
if (categorie.getId() == null) {
insertCategories.add(categorie);
} else {
updateCategories.add(categorie);
}
}
// inserções de categorias
if (insertCategories.size() > 0) {
insertCategories(insertCategories);
}
// atualizações de categorias
if (updateCategories.size() > 0) {
updateCategories(updateCategories);
}
// --------------------------------------------- produtos
// atualizando os produtos das categorias
List<Produit> allProduits = new ArrayList<Produit>();
for (Categorie categorie : entities) {
List<Produit> produits = categorie.getProduits();
Long idCategorie = categorie.getId();
if (produits != null) {
// adiciona-se à lista de todos os produtos
allProduits.addAll(produits);
// os produtos são escaneados um a um para serem associados à respectiva categoria
for (Produit produit : produits) {
// associamos o produto à sua categoria
produit.setIdCategorie(idCategorie);
produit.setCategorie(categorie);
}
}
}
// inserção/atualização dos produtos
daoProduit.saveEntities(allProduits);
// resultado
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(207, e, simpleClassName);
}
}
- linhas 5-23: inserção ou atualização das categorias;
- linhas 26-43: inserção ou atualização dos produtos;
- linhas 35-39: este código vincula cada produto à sua categoria. Na fase anterior de inserção das categorias, estas receberam uma chave primária que deve ser inserida no campo [idCategorie] do produto (linha 37). Além disso, as linhas 37-38 permitem corrigir situações em que o usuário não vinculou corretamente cada produto à sua categoria. Para que essa relação esteja correta, é necessário utilizar o método [Categorie].add(Produto p), mas nada impede que um usuário adicione um produto diretamente à lista de produtos da categoria sem passar por esse método, correndo o risco de que os campos [idCategorie, categorie] do produto p sejam preenchidos incorretamente;
- linha 43: delega-se à instância da interface [IDao<Produit>] a tarefa de persistir / atualizar os produtos. Vale lembrar que essa instância foi injetada na classe [DaoCategorie]:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. Inserção das categorias
As categorias são inseridas na tabela [CATEGORIES] pelo seguinte método privado [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// categorias a serem adicionadas
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// armazenamos a chave primária
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// tudo é OK — atribui-se as chaves primárias às categorias persistentes
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// resultado
return categories;
}
- linha 6: utiliza-se o bean [simpleJdbcInsertCategorie] injetado na classe pelas seguintes linhas:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
Esse bean é definido na classe [AppConfig] do projeto da seguinte maneira:
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_CATEGORIES_ID)
.usingColumns(ConfigJdbc.TAB_CATEGORIES_NOM);
}
- na linha 5, a classe [SimpleJdbcInsert] é uma classe da biblioteca Spring JDBC (linha 1):
- o parâmetro do construtor [SimpleJdbcInsert] é a fonte de dados sobre a qual se opera;
- a cláusula [withTableName] permite indicar a tabela na qual se deseja inserir um elemento, neste caso a tabela [CATEGORIES];
- a cláusula [usingGeneratedKeyColumns] permite especificar a coluna da chave primária gerada automaticamente, neste caso a coluna [ID];
- a cláusula [usingColumns] permite restringir a inserção a determinadas colunas. Aqui, excluímos a coluna [ID], que é gerada automaticamente pela SGBD, e a coluna [VERSIONING], que tem um valor padrão igual a 1;
Voltemos ao código do método [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// categorias a serem adicionadas
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// armazenamos a chave primária
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// tudo é OK - atribui-se as chaves primárias às categorias persistentes
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// resultado
return categories;
}
- linha 6: o método [simpleJdbcInsertCategorie.executeAndReturnKey] é utilizado:
![]()
O método espera, como parâmetro, um dicionário que estabeleça as ligações entre as colunas da tabela e os valores a serem inseridos nelas. Ele retorna como resultado a chave primária na forma de um tipo [Number]. O método [Number.longValue()] permite obter a chave primária na forma de um tipo [Long].
O método [getMapForCategorie] é o seguinte método privado:
private Map<String, ?> getMapForCategorie(Categorie categorie) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
return map;
}
As chaves do dicionário são os nomes das colunas a serem preenchidas ([NOM]), e os valores do dicionário são os valores a serem inseridos nessas colunas.
- linha 8 [insertCategories]: a chave primária recuperada é armazenada em um dicionário. Vamos aguardar até termos certeza de que todas as entidades foram inseridas antes de atribuir suas chaves primárias a elas. De fato, em caso de exceção, todas as inserções serão canceladas e queremos que, nesse caso, as entidades [categories] da linha 1 também permaneçam inalteradas;
- linhas 14-17: agora que temos certeza de que tudo correu bem, atribuímos as chaves primárias geradas às categorias;
- linha 19: retornamos a lista de categorias com suas chaves primárias;
4.9.10.3. Atualização das categorias
As categorias são atualizadas com o seguinte método privado [updateCategories]:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// atualização da categoria no banco de dados
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// conseguiu?
Long idCategorie = null;
if (nbLignes == 0) {
// não deu certo — estamos investigando o motivo
// procurando a categoria no banco de dados
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// a categoria não existe
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// a versão estava incorreta
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
A atualização de uma categoria C1 no banco de dados por uma categoria C2 na memória só é permitida se as categorias C1 e C2 tiverem a mesma versão. Esse número de versão serve para impedir a atualização simultânea da entidade por dois usuários diferentes: dois usuários, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e grava essa modificação no banco de dados: o número de versão passa então para V1+1. U2, por sua vez, modifica E e grava essa modificação no banco de dados: ele receberá uma exceção, pois possui uma versão (V1) diferente daquela no banco de dados (V1+1).
- linhas 2-29: o `try` possui dois `catch`:
- o primeiro, na linha 25, serve para permitir a passagem de uma eventual exceção do tipo [DaoException] lançada pelo código da linha 13;
- o segundo, na linha 27, serve para lidar com os outros tipos de exceção;
- linha 3: são verificadas todas as categorias a serem atualizadas;
- linha 4: atualiza-se a categoria atual com o método [namedParameterJdbcTemplate.update]:

- vamos analisar a instrução:
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES, new BeanPropertySqlParameterSource(categorie));
A ordem SQL [ConfigJdbc.UPDATE_CATEGORIES] é a seguinte:
public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";
A ordem possui três parâmetros (:id, :version, :nom), cujos valores estão nos campos com os mesmos nomes do objeto [categorie] modificado. Essa particularidade é utilizada passando-se como segundo parâmetro [new BeanPropertySqlParameterSource(categorie)], que indica que “os valores dos parâmetros estão nos campos com os mesmos nomes desse Java bean”;
O resultado dessa operação, quando ela ocorre normalmente, é o número de linhas modificadas, ou seja, 0 ou 1.
Voltemos ao código analisado:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// atualização da categoria no banco de dados
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// Deu certo?
Long idCategorie = null;
if (nbLignes == 0) {
// não deu certo — estamos investigando o motivo
// procurando a categoria no banco de dados
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// a categoria não existe
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// a versão estava incorreta
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
- linha 9: verifica-se se a modificação foi bem-sucedida;
- linha 10: a modificação não foi bem-sucedida. Como a cláusula [WHERE] envolve as colunas [ID] e [VERSIONING], procura-se a coluna que causou a falha no [WHERE];
- linhas 12-18: verifica-se se a chave [id] da categoria está no banco de dados. Caso contrário, é executada uma [RuntimeException] com uma mensagem de erro apropriada;
- linhas 19-22: tratam o caso em que a versão estava incorreta;
4.10. A classe [DaoProduit]
![]() |
![]() |
A classe [DaoProduit] implementa a interface [IDao<Produit>], que garante oacesso aos dados da tabela [PRODUITS] do banco de dados MySQL [dbproduitscategories]. Sua estrutura é a seguinte:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
// injeções
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
@Override
public List<Produit> getAllShortEntities() {
...
}
@Override
public List<Produit> getAllLongEntities() {
....
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
....
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
....
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
}
// --------------------- mappers
class ShortProduitMapper implements RowMapper<Produit> {
...
}
class LongProduitMapper implements RowMapper<Produit> {
...
}
O código é muito semelhante ao da classe [DaoCategorie]. Analisaremos apenas alguns métodos.
4.10.1. O método [getShortEntitiesById]
O método [getShortEntitiesById] gera a versão resumida dos produtos cujas chaves primárias são passadas:
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTPRODUIT_BYID,
Collections.singletonMap("ids", ids), new ShortProduitMapper());
} catch (Exception e) {
throw new DaoException(109, e, simpleClassName);
}
}
- linha 4: a ordem SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] é a seguinte:
public final static String SELECT_SHORTPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSIONING, p.NOM as p_NOM, p.CATEGORIE_ID as p_CATEGORIE_ID, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION FROM PRODUITS p WHERE p.ID in (:ids)";
- linha 4: a classe [ShortProduitMapper], responsável por encapsular o [ResultSet] em uma lista de produtos, é a seguinte:
class ShortProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSIONING"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), null);
}
}
4.10.2. O método [getLongEntitiesByName]
O método [getShortEntitiesById] gera a versão completa dos produtos cujos nomes são passados:
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
- linha 4: o comando SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] é o seguinte:
public final static String SELECT_LONGPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p, CATEGORIES c WHERE p.ID in (:ids) AND p.CATEGORIE_ID=c.ID";
- linha 4: a classe [LongProduitMapper], responsável por encapsular os elementos do [ResultSet] em produtos (versão longa), é a seguinte:
class LongProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSION"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null));
}
}
4.10.3. O método [saveEntities]
O método [saveEntities] é utilizado tanto para inserir novos produtos (id==null) quanto para atualizar produtos existentes (id!=null):
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
try {
// produtos a serem inseridos
List<Produit> insertProduits = new ArrayList<Produit>();
// produtos a serem atualizados
List<Produit> updateproduits = new ArrayList<Produit>();
// estamos verificando a lista de entidades recebidas
for (Produit produit : entities) {
Long id = produit.getId();
if (id == null) {
insertProduits.add(produit);
} else {
updateproduits.add(produit);
}
}
// adições
insertProduits(insertProduits);
// alterações
updateProduits(updateproduits);
// resultado
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
Na linha 18, os produtos a serem inseridos são inseridos pelo seguinte método privado [insertProduits]:
private List<Produit> insertProduits(List<Produit> produits) {
Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
try {
// produtos a serem adicionados
for (Produit produit : produits) {
Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
// registra-se a chave primária
mapProduits.put(newId.longValue(), produit);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// tudo é OK — atribui-se as chaves primárias aos produtos persistentes
for (Long id : mapProduits.keySet()) {
Produit produit = mapProduits.get(id);
produit.setId(id);
}
// resultado
return produits;
}
private Map<String, ?> getMapForProduit(Produit produit) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_PRODUITS_NOM, produit.getNom());
map.put(ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, produit.getIdCategorie());
map.put(ConfigJdbc.TAB_PRODUITS_PRIX, produit.getPrix());
map.put(ConfigJdbc.TAB_PRODUITS_DESCRIPTION, produit.getDescription());
return map;
}
Esse método é análogo ao método [insertCategories] abordado no parágrafo 4.9.10.3.
- linha 4: utiliza-se o bean [simpleJdbcInsertProduit], que foi injetado na classe:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
Esse bean foi definido na classe [AppConfig], que configura o projeto:
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource)
.withTableName(ConfigJdbc.TAB_PRODUITS)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_PRODUITS_ID)
.usingColumns(ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION,ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID);
}
- linhas 3-6: o bean [simpleJdbcInsertProduit]
- está vinculado à fonte de dados do banco de dados [dbproduitscategories] (linha 3) e à tabela [ConfigJdbc.TAB_PRODUITS] dessa fonte (linha 4);
- a chave primária dessa tabela é gerada na coluna [ConfigJdbc.TAB_PRODUITS_ID] (linha 5);
- só são atribuídos valores às colunas [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (linha 6);
O método [updateProduits], que atualiza os produtos (linha 20 de [saveEntities]), é o seguinte:
private void updateProduits(List<Produit> updateProduits) {
try {
// os produtos são verificados
for (Produit produit : updateProduits) {
// atualização do produto no banco de dados
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
new BeanPropertySqlParameterSource(produit));
// foi bem-sucedido?
Long idProduit = null;
if (nbLignes == 0) {
// não deu certo — estamos investigando o motivo
// procurando o produto no banco de dados
idProduit = produit.getId();
List<Produit> produitsInBd = getShortEntitiesById(idProduit);
if (produitsInBd.size() == 0) {
// o produto não existe
throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
idProduit));
} else {
// a versão estava incorreta
throw new RuntimeException(String.format(
"Erreur de mise à jour. Le produit de clé [%s] n'a pas la bonne version", idProduit));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
É semelhante à que atualiza as categorias (ver parágrafo 4.9.10.3). Na linha 23, a ordem SQL [ConfigJdbc.UPDATE_PRODUITS] executada para atualizar os produtos é a seguinte:
public final static String UPDATE_PRODUITS = "UPDATE PRODUITS SET VERSIONING=VERSIONING+1, NOM=:nom, PRIX=:prix, CATEGORIE_ID=:idCategorie, DESCRIPTION=:description WHERE ID=:id AND VERSIONING=:version";
Os nomes dos parâmetros [:id,:version,:nom,:prix,:idCategorie,:description] são também os nomes dos campos da classe [Produit], o que permite utilizar a instrução das linhas 6-7 para atualizar o produto atual.
4.11. A camada de testes
![]() |
![]() |
A camada de testes é composta por três classes de teste:
- [JUnitTestCheckArguments]: os testes dessa classe chamam os diferentes métodos da camada [DAO] com argumentos inválidos e verificam se eles respondem corretamente;
- [JUnitTestDao]: os testes dessa classe chamam os diversos métodos da camada [DAO] e verificam se eles realizam o que é esperado;
- [JUnitTestPushTheLimits] não tem como objetivo testar a camada [DAO], mas sim medir seu desempenho;
Essa camada de testes desempenha um papel importante neste documento. Na verdade, ela é comum a todas as implementações da interface [IDao<T>]. Existem seis por SGBD (1 implementação JDBC, 3 implementações JPA, 1 implementação Spring MVC, 1 implementação Spring MVC segura), totalizando 36 para as seis implementações SGBD testadas. A camada de testes nos permite verificar se todas as implementações reagem da mesma maneira.
4.11.1. O teste [JUnitTestCheckArguments]
A classe de teste [JUnitTestCheckArguments] possui 48 métodos que testam a reação dos métodos da camada [DAO] quando chamados com argumentos incorretos. Sua estrutura é a seguinte:
package spring.jdbc.tests;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCheckArguments {
// camada [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// dados locais
private Iterable<String> names1 = null;
private Iterable<String> names2 = Lists.newArrayList(new String[0]);
private String[] names3 = null;
private String[] names4 = new String[0];
private Iterable<Long> ids1 = null;
private Iterable<Long> ids2 = Lists.newArrayList(new Long[0]);
private Long[] ids3 = null;
private Long[] ids4 = new Long[0];
private Iterable<Categorie> categories1 = null;
private Iterable<Categorie> categories2 = Lists.newArrayList(new Categorie[0]);
private Categorie[] categories3 = null;
private Categorie[] categories4 = new Categorie[0];
private Iterable<Produit> produits1 = null;
private Iterable<Produit> produits2 = Lists.newArrayList(new Produit[0]);
private Produit[] produits3 = null;
private Produit[] produits4 = new Produit[0];
...
}
- linha 19: o teste JUnit será realizado em integração com o framework Spring;
- linha 18: antes dos testes, os beans definidos na classe [AppConfig] do projeto serão instanciados;
- linhas 23-26: injeção de uma instância de cada uma das duas interfaces da camada [DAO];
- linhas 29-44: parâmetros de chamada dos métodos da camada [DAO] que estão incorretos;
- linha 29: um ponteiro null do tipo [Iterable<String>] como lista de nomes;
- linha 30: uma lista vazia do tipo [Iterable<String>] como lista de nomes;
- linha 29: um ponteiro null do tipo String[] como matriz de nomes;
- linha 30: um array vazio do tipo String[] como matriz de nomes;
- ...
Com o campo [names1], fazemos, por exemplo, o seguinte teste:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- linha 1: indica-se que o teste [getShortProduitsByName1] deve gerar a exceção do tipo [MyIllegalArgumentException]
Com o campo [names2], fazemos, por exemplo, o seguinte teste:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName2() {
daoCategorie.getLongEntitiesByName(names2);
}
Com o campo [names3], realiza-se, por exemplo, o seguinte teste:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName3() {
daoCategorie.getLongEntitiesByName(names3);
}
Com o campo [names4], realiza-se, por exemplo, o seguinte teste:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName4() {
daoProduit.getShortEntitiesByName(names4);
}
Assim, são realizados 48 testes para verificar todos os casos possíveis. Executa-se a configuração de execução denominada [spring-jdbc-generic-04-JUnitTestCheckArguments] [1]. O resultado obtido é o seguinte: [2]:
![]() |
4.11.2. O teste [JUnitTestDao]
O teste [JUnitTestDao] chama os métodos da camada [DAO] com argumentos válidos e verifica se os métodos realizam o que se espera deles. Há, no total, 74 testes que verificam as operações de inserção, seleção, atualização e exclusão de entidades, categorias ou produtos. No total, há mais de 1.000 linhas de código. Vamos analisar apenas alguns desses métodos.
4.11.2.1. A estrutura do teste
A classe [JUnitTestDao] possui a estrutura a seguir:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
// contexto Spring
@Autowired
private ApplicationContext context;
// camada [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// constantes
private final int NB_PRODUITS = 5;
private final int NB_CATEGORIES = 2;
// local
// local
private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
@Before
public void clean() {
// limpa-se o banco de dados antes de cada teste
log("Vidage de la base de données", 1);
// esvazia-se a tabela [CATEGORIES] e, em cadeia, a tabela [PRODUITS]
daoCategorie.deleteAllEntities();
// esvazia-se os dicionários
for (Long id : mapCategories.keySet()) {
mapCategories.remove(id);
}
for (Long id : mapProduits.keySet()) {
mapProduits.remove(id);
}
}
...
}
- linhas 27-28: assim como no teste [JUnitTestCheckArguments], trata-se de um teste integrado ao Spring e configurado pela classe [AppConfig] do projeto;
- linhas 32-33: injeção do contexto do Spring, que dá acesso a todos os seus beans;
- linhas 35-36: injeção da instância da interface [IDao<Produit>] testada pela classe;
- linhas 37-38: injeção da instância da interface [IDao<Categorie>] testada pela classe;
- linhas 41-42: quando um teste precisar de dados do banco de dados, será gerado um banco de dados com a tabela [NB_CATEGORIES] (categorias), cada uma contendo a tabela [NB_PRODUITS] (produtos). Assim, teremos [NB_CATEGORIES] categorias na tabela [CATEGORIES] e [NB_CATEGORIES] * [NB_PRODUITS] produtos na tabela [PRODUITS];
- linhas 46-47: dois dicionários onde serão armazenados os produtos e as categorias;
- linhas 49-62: o método [clean] é executado antes de cada teste (linha 49). Na linha 54, esvazia-se a tabela [CATEGORIES]. É importante lembrar aqui que a tabela [PRODUITS] possui uma chave primária [CATEGORIE_ID] na coluna ID da tabela [CATEGORIES] e que esta está definida da seguinte forma;
![]() |
- (continuação)
- em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Ela aponta para a coluna [ID] da tabela [CATEGORIES] [4-5];
- quando uma categoria é excluída, todos os produtos a ela vinculados também são excluídos [6]. É importante observar esse ponto, pois ele é utilizado na construção da camada [DAO], que utiliza a base [dbproduitscategories];
Portanto, ao excluir o conteúdo da tabela [CATEGORIES], o conteúdo da tabela [PRODUITS] também será excluído.
- linhas 56-58: esvazia-se o dicionário de categorias;
- linhas 59-61: faz-se o mesmo com o dicionário de produtos;
É importante lembrar que, antes de cada teste, temos tabelas vazias no banco de dados e dicionários vazios na memória.
4.11.2.2. O método [verifyClean]
O método [verifyClean] verifica se, após o método [clean], as tabelas estão vazias:
@Test
public void verifyClean() {
log("verifyClean", 1);
List<Categorie> categories = daoCategorie.getAllShortEntities();
Assert.assertEquals(0, categories.size());
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.2.3. O método [fillDataBase]
Este método verifica se o banco de dados foi preenchido corretamente com dados de teste:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// preenchimento do banco de dados e dos dicionários
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// exibição
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// algumas verificações
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// os dicionários devem ter sido esvaziados
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
Este teste utiliza vários métodos privados:
- [fill], linha 4, que preenche o banco de dados com dados de teste;
- [registerCategories], linha 4, que preenche os dicionários com os dados retornados pelo método [fill]. Esses dois dicionários representam as entidades persistentes;
- [showDataBase], linha 6, que lê as duas tabelas [CATEGORIES] e [PRODUITS] e retorna o que leu;
- [checkShortCategorie], linha 13, verifica a categoria lida por [showDataBase]. Ela verifica se a versão abreviada dessa categoria corresponde ao que foi registrado no dicionário de categorias;
- [checkShortProduit], linha 16, faz o mesmo para os produtos;
- quando uma entidade é encontrada em um dicionário, ela é removida do dicionário. As linhas 19-20 verificam se os dois dicionários estão vazios. Se essas duas verificações forem válidas, isso significa que:
- todos os valores lidos por [showDataBase] foram de fato encontrados nos dicionários;
- que estes não contêm outras entidades além daquelas que foram lidas;
O método privado [fill] é o seguinte:
private List<Categorie> fill(int nbCategories, int nbProduits) {
// preenchimento das tabelas
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// adição da categoria — em cascata, os produtos também serão
// inseridos
categories = daoCategorie.saveEntities(categories);
// resultado
return categories;
}
- linhas 3-12: constrói-se uma lista de categorias, cada uma com seus produtos;
- linha 15: essa lista de categorias é persistida. Vimos que o método [daoCategorie.saveEntities] também persistia os produtos das categorias quando estas os possuíam;
- linha 17: a lista de categorias persistida é retornada. As entidades persistidas (categorias e produtos) passam a ter uma chave primária no campo [id];
O método privado [registerCategories] irá inserir essas entidades nos dois dicionários:
private void registerCategories(List<Categorie> categories) {
// dicionários
for (Categorie categorie : categories) {
mapCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
mapProduits.put(produit.getId(), produit);
}
}
}
Cada dicionário tem como chave de acesso a chave primária das entidades.
Feito isso, o banco de dados preenchido anteriormente será lido e exibido pelo seguinte método privado [showDataBase]:
private Object[] showDataBase() throws BeansException, JsonProcessingException {
// lista de categorias
log("Liste des catégories", 2);
List<Categorie> categories = daoCategorie.getAllShortEntities();
affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
// lista de produtos
log("Liste des produits", 2);
List<Produit> produits = daoProduit.getAllShortEntities();
affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
// resultado
return new Object[] { categories, produits };
}
- linhas 4 e 8: recuperam-se as versões curtas das categorias e dos produtos;
- linha 11: retorna-se um array contendo as duas listas de entidades recuperadas;
- linhas 5 e 9: as listas de entidades são exibidas com o método privado [affiche] a seguir:
// exibição de uma lista de elementos do tipo T
private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, mapper);
}
}
// exibição de um elemento do tipo T
private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
System.out.println(mapper.writeValueAsString(element));
}
As entidades são exibidas por um mapeador jSON (linha 10). Esse mapeador é o segundo parâmetro do método [affiche], linha 2. O contexto Spring define quatro mapeadores jSON no arquivo [ConfigJdbc] da dependência Maven [mysql-config-jdbc]:
// filtros jSON -------------------------------------
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
- esses mapeadores jSON (linhas 7-9, 16-18, 26-28, 35-37) possuem um atributo
[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)]
que os transforma em beans instanciados a cada solicitação feita ao contexto do Spring. Isso é uma novidade. Todos os beans do Spring vistos até agora eram singletons: eram criados em uma única instância, e era essa instância que era retornada sempre que se solicitava uma referência ao contexto do Spring. Por que essa mudança? Na verdade, os quatro beans [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] configuram o único mapeador jSON (esse sim é um singleton), definido nas linhas 2 a 5. Esse mapeador precisa ser reconfigurado a cada chamada de um dos quatro beans anteriores, e não apenas uma vez na inicialização do contexto. Se tivéssemos decidido ter quatro mapeadores jSON diferentes, um para cada um dos quatro beans, então esses poderiam ter sido singletons. Isso era perfeitamente possível. Teríamos escrito, então, nas linhas 10, 19, 29 e 38:
ObjectMapper jsonMapper = new ObjectMapper();
- os quatro mapeadores JSON servem para configurar os filtros jSON das entidades [Produit] e [Categorie]. De fato, escrevemos (ver parágrafos 4.6 e 4.6) o seguinte:
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
e
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
A representação jSON da entidade [Categorie] é controlada pelo filtro jSON [jsonFilterCategorie] e a daentidade [produit] pelo filtro jSON [jsonFilterProduit]. Os quatro mapeadores jSON do contexto Spring configuram esses dois filtros da seguinte maneira:
- o mapeador [jsonMapperShortCategorie] configura o filtro jSON [jsonFilterCategorie] para uma versão resumida da categoria: o campo [produits] não será incluído na representação jSON da categoria;
- o mapeador [jsonMapperLongCategorie] configura o filtro jSON [jsonFilterCategorie] para uma versão longa da categoria: o campo [produits] será incluído na representação jSON da categoria;
- o mapeador [jsonMapperShortProduit] configura o filtro jSON [jsonFilterProduit] para uma versão resumida do produto: o campo [categorie] não será incluído na representação jSON do produto;
- o mapeador [jsonMapperLongProduit] configura o filtro jSON [jsonFilterProduit] para uma versão longa do produto: o campo [categorie] será incluído na representação jSON do produto;
Concluímos o método privado [showDataBase]. Voltemos ao código do teste [fillDataBase]:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// preenchimento da base e dos dicionários
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// exibição
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// algumas verificações
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// os dicionários devem ter sido esgotados
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
- linhas 6-8: recuperamos as versões curtas dos produtos e categorias lidos no banco de dados;
- linhas 10-11: primeiras verificações;
- linhas 12-14: cada categoria retornada pelo método [showDataBase] é verificada pelo método privado [checkShortCategorie] a seguir:
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
// não é possível testar o campo [produits] de forma portátil com as implementações jPA
}
- linha 1: [Categorie actual] é a categoria lida no banco de dados e que deve ser idêntica à categoria presente no dicionário [mapCategories];
- linha 2: recupera-se a chave primária da categoria lida;
- linha 3: recupera-se a categoria registrada com essa chave primária no dicionário de categorias;
- linha 4: a chave é removida do dicionário para garantir que nenhuma outra categoria lida utilize essa mesma chave;
- linha 5: verifica-se se as duas categorias têm o mesmo nome;
A versão resumida dos produtos gerada pelo método [showDataBase] é verificada pelo seguinte método privado [checkShortProduit]:
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(actual.getIdCategorie(), expected.getIdCategorie());
// não é possível testar o campo [categorie] de forma portátil com as implementações jPA
}
- linha 1: [Produit actual] é o produto resumido lido do banco de dados;
- linhas 2-3: recupera-se, no dicionário de produtos persistentes, o produto com a mesma chave primária;
- linha 4: a entrada encontrada no dicionário é excluída;
- linhas 5-8: verifica-se se os dois produtos têm os mesmos valores nos campos;
4.11.2.4. O método [getLongCategoriesByName3]
Este teste é o seguinte:
@Test
public void getLongCategoriesByName3() {
// preenchimento básico
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// teste
log("getLongCategoriesByName3", 1);
List<Categorie> categories2 = daoCategorie.getLongEntitiesByName("categorie[0]", "categorie[1]");
Assert.assertEquals(2, categories2.size());
registerCategories(Lists.newArrayList(categories.get(0), categories.get(1)));
for (Categorie categorie : categories) {
checkLongCategorie(categorie);
}
Assert.assertEquals(0, mapCategories.size());
}
- linha 4: preenche-se o banco de dados e recupera-se a lista de categorias e produtos armazenados;
- linha 7: testa-se o método [daoCategorie.getLongEntitiesByName(Iterable<String> names)] da camada [DAO]. Solicita-se uma lista de dois produtos designados por seus nomes em suas versões completas;
- linha 8: verifica-se se a lista retornada por [daoCategorie.getLongEntitiesByName(Iterable<String> names)] contém, de fato, dois elementos;
- linha 9: os dois elementos armazenados na linha 4 são inseridos no dicionário de categorias;
- linhas 10-12: verifica-se se os dois elementos lidos são, de fato, aqueles que foram armazenados;
- linha 13: verifica-se se o dicionário de categorias está vazio, o que significa que todas as categorias lidas foram encontradas no dicionário e que este não contém valores que não tenham sido lidos;
Na linha 11, o método [checkLongCategorie] verifica a versão completa de uma categoria:
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
- a linha 6 verifica se o campo [produits] da categoria não é null. De fato, a leitura de uma categoria no formato longo sempre a retorna com um campo [produits] que não seja null. Se a categoria não tiver produtos, então o campo [produits] é uma lista vazia, mas existente;
4.11.2.5. O método [updateDataBase1]
@Test
public void updateDataBase1() {
// preenchimento
fill(NB_CATEGORIES, NB_PRODUITS);
// teste
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = daoCategorie.getLongEntitiesByName("categorie[1]").get(0);
List<Produit> produits = categorie1.getProduits();
Map<Produit, Long> versions = new HashMap<Produit, Long>();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
versions.put(produit, produit.getVersion());
}
daoProduit.saveEntities(produits);
// revisão
List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
.getProduits();
Assert.assertEquals(produits.size(), produitsInBd.size());
// verificações
for (Produit produit2 : produitsInBd) {
Produit produit = findProduitByName(produit2.getNom(), produits);
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
Assert.assertEquals(produit2.getVersion().longValue(), versions.get(produit) + 1);
}
}
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
O método [updateDataBase1] aumenta o preço dos produtos da categoria denominada categorie[1] em 10% e verifica duas coisas:
- se o preço base realmente mudou;
- que a versão do produto atualizado foi incrementada em 1;
O código realiza as seguintes ações:
- linha 4: preenchimento do banco de dados;
- linha 7: recupera-se do banco de dados a categoria denominada 'categorie[1]';
- linhas 8-13: aumenta-se em 10% o preço de todos os produtos (linha 11). Além disso, cria-se um dicionário que associa um produto à sua versão (linhas 9 e 12);
- linha 14: o método [daoProduit.saveEntities] é chamado. Ele irá atualizar os produtos;
- linha 16: recuperam-se do banco de dados os produtos da categoria denominada 'categorie[1]';
- linhas 20-24: para todos os produtos dessa categoria, verifica-se se o preço foi efetivamente alterado (linha 22) e se a versão foi incrementada em 1 (linha 23);
4.11.2.6. O método [deleteProduitsByProduit1]
O método [deleteProduitsByProduit1] exclui produtos da tabela [PRODUITS]:
@Test
public void deleteProduitsByProduit1() {
// preenchimento
fill(NB_CATEGORIES, NB_PRODUITS);
// exclusão
daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
// verificação
List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
Assert.assertEquals(0, produits.size());
}
- linha 6: são excluídos dois produtos;
- linhas 8-9: verifica-se se eles não estão mais no banco de dados;
4.11.2.7. O método [getLongProduitsById3]
@Test
public void getLongProduitsById3() {
// preenchimento
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// teste
log("getLongProduitsById3", 1);
List<Produit> produits = daoProduit.getLongEntitiesByName("produit[0,3]", "produit[1,4]");
Assert.assertEquals(2, produits.size());
registerProduits(Lists.newArrayList(categories.get(0).getProduits().get(3), categories.get(1).getProduits().get(4)));
produits = daoProduit.getLongEntitiesById(produits.get(0).getId(), produits.get(1).getId());
for (Produit produit : produits) {
checkLongProduit(produit);
}
Assert.assertEquals(0, mapProduits.size());
}
- linha 4: preenche-se o banco de dados e recupera-se a lista de categorias armazenadas;
- linha 7: recupera-se do banco de dados a versão completa de dois produtos identificados por seus nomes;
- linha 9: os produtos [produit[0,3], produit[1,4]] presentes na lista de categorias da linha 4 são inseridos no dicionário de produtos;
- linha 10: esses mesmos dois produtos são pesquisados no banco de dados por meio de suas chaves primárias;
- linhas 11-14: verifica-se se os dados lidos são idênticos aos dados registrados no dicionário;
O método privado [checkLongProduit] é o seguinte:
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertNotNull(actual.getCategorie());
}
4.11.2.8. Conclusion
Vamos parar por aqui. Há 74 testes até o momento e poderíamos adicionar outros, pois provavelmente esqueci alguns casos a serem testados. Mesmo não sendo exaustivos, esses testes permitiram detectar inúmeros erros, geralmente casos-limite que não haviam sido previstos durante a escrita inicial da camada [DAO]. Uma fase de testes exaustivos é indispensável para qualquer projeto.
Para executar o teste, podemos usar a configuração de execução importada e denominada [spring-jdbc-generic-04.JUnitTestDao].
![]() | ![]() |
4.11.3. O teste [JUnitTestPushTheLimits]
O teste [JUnitTestPushTheLimits] é um teste de desempenho. Aproveita-se o fato de que os testes JUnit exibem seu tempo de execução para medir o desempenho da camada [DAO]. Esses resultados serão, em seguida, comparados aos das implementações JPA da camada [DAO].
4.11.3.1. Squelette
A estrutura da classe [JUnitTestPushTheLimits] é a seguinte:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestPushTheLimits {
// camada [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// constantes
private final int NB_CATEGORIES = 2500;
private final int NB_PRODUITS = 2;
// local
private Map<Long, Categorie> hCategories;
private Map<Long, Produit> hProduits;
@Before
public void clean() {
// esvaziando a tabela [CATEGORIES]
daoCategorie.deleteAllEntities();
// dicionários
hCategories = new HashMap<Long, Categorie>();
hProduits = new HashMap<Long, Produit>();
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// preenchimento das tabelas
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, 0L, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, 0L, String.format("produit[%d,%d]", i, j), 0L,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// adição da categoria — os produtos também serão inseridos em cascata
categories = daoCategorie.saveEntities(categories);
// dicionários
for (Categorie categorie : categories) {
hCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
hProduits.put(produit.getId(), produit);
}
}
// resultado
return categories;
}
....
// -------------------- métodos privados
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
Assert.assertNotNull(actual.getCategorie());
}
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
boolean erreur = false;
try {
actual.getCategorie().getNom();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
boolean erreur = false;
try {
actual.getProduits().size();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
}
Aqui encontramos a estrutura da classe [JUnitTestDao]. Já nos deparamos com todos esses métodos. O teste trabalha com um conjunto de 2.500 categorias, cada uma com 2 produtos (linhas 32-33). A tabela [CATEGORIES] terá, portanto, 2.500 linhas, e a tabela [PRODUITS], 5.000 linhas. Poderíamos ter incluído mais linhas, mas o teste já dura quase um minuto. Por isso, optamos por valores aceitáveis para o usuário que aguarda o término do teste.
Há 18 testes no total. Eles são executados com a configuração de execução [1]. Os tempos de execução são mostrados em [2]:
![]() |
4.11.3.2. doNothing [0,114]
O método [doNothing] não realiza nenhuma ação. Ele permite medir a duração do método [clean], executado antes de cada teste e que esvazia o banco de dados. Acima, vemos que a duração dessa operação é insignificante em comparação com as outras.
@Test
public void doNothing() {
// limpar
}
4.11.3.3. perf01 [4,179]
O teste [perf01] serve para medir o tempo de preenchimento do banco de dados:
@Test
public void perf01() {
// inserir
fill(NB_CATEGORIES, NB_PRODUITS);
}
4.11.3.4. perf02 [7,624]
O método [perf02]:
- preenche a base de dados;
- em seguida, altera o nome de todas as categorias e o preço de todos os produtos.
@Test
public void perf02() {
// atualizar
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
for (Categorie categorie : categories) {
categorie.setNom(categorie.getNom() + "*");
for (Produit produit : categorie.getProduits()) {
produit.setPrix(produit.getPrix() * 1.1);
}
}
// atualização
daoCategorie.saveEntities(categories);
}
4.11.3.5. perf03[3,911]
O método [perf03]:
- preenche o banco de dados
- e, em seguida, exclui todas as categorias, uma a uma. Os produtos também são excluídos devido à relação em cascata existente entre a tabela [CATEGORIES] e a tabela [PRODUITS].
Pode ser surpreendente que essa operação demore menos tempo ([3,911 s]) do que a operação [perf01] [4,179 s], que realiza menos tarefas.
@Test
public void perf03() {
// excluir categorias e, em cadeia, os produtos
daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}
Se analisarmos o código do método [daoCategorie.deleteEntitiesByEntity], percebemos que um [PreparedStatement] com 2.500 parâmetros (o número de categorias) será executado. É aí que entra o bean [maxPreparedStatementParameters], que dividirá a ordem SQL em vários [PreparedStatement] com um número de parâmetros que o SGBD específico utilizado possa suportar.
4.11.3.6. perf04[2,426]
O método [perf04]:
- preenche a base de dados;
- em seguida, solicita a versão completa de todas as categorias;
@Test
public void perf04() {
// selecionar
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
ids.add(categorie.getId());
}
daoCategorie.getLongEntitiesById(ids);
}
4.11.3.7. perf05 [3,507]
O método [perf05]:
- preenche o banco de dados;
- em seguida, exclui os 5.000 produtos por meio de suas chaves primárias (portanto, temos potencialmente um [PreparedStatement] com 5.000 parâmetros);
- verifica se a tabela de produtos está vazia;
@Test
public void perf05() {
// excluir produtos
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
for (Produit p : categorie.getProduits()) {
ids.add(p.getId());
}
}
daoProduit.deleteEntitiesById(ids);
// verificação
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.3.8. Résultats
Não vamos continuar apresentando os diferentes testes. Vamos apenas indicar o que eles fazem e sua duração. Essas durações só são relevantes quando comparadas entre si. Seus valores dependem, de fato, do ambiente de teste utilizado (hardware e configuração de software). Mas, quando obtidos no mesmo ambiente, podem ser comparados.
Duração total do teste: 59,995 segundos
função | ||
preenche o banco de dados com 2.500 categorias e 5.000 produtos | ||
preenche e, em seguida, modifica o banco de dados | ||
preenche o banco de dados e, em seguida, exclui todas as categorias e seus produtos | ||
preenche o banco de dados e solicita a versão completa de todas as categorias | ||
preenche o banco de dados e exclui os 5.000 produtos, um por um, por meio de suas chaves primárias | ||
preenche o banco de dados e exclui os 5.000 produtos, um por um, por meio de seus nomes | ||
preenche o banco de dados e exclui os 5.000 produtos, um por um, por meio de seus códigos | ||
preenche o banco de dados e solicita a versão abreviada de todos os produtos por meio de seus nomes | ||
preenche o banco de dados e solicita a versão completa de todos os produtos por meio de seus nomes | ||
preenche o banco de dados e solicita a versão curta de todos os produtos por meio de suas chaves primárias | ||
preenche o banco de dados e solicita a versão completa de todos os produtos por meio de suas chaves primárias | ||
preenche o banco de dados e, em seguida, exclui todas as categorias (e, portanto, os produtos associados) uma a uma por meio de seus nomes | ||
preenche o banco de dados e, em seguida, exclui todas as categorias (e, portanto, os produtos associados) uma a uma por meio de seus códigos | ||
preenche o banco de dados e solicita a versão abreviada de todas as categorias por meio de seus nomes | ||
preenche o banco de dados e solicita a versão completa de todas as categorias por meio de seus nomes | ||
preenche o banco de dados e solicita a versão curta de todas as categorias por meio de suas chaves primárias | ||
preenche o banco de dados e solicita a versão completa de todas as categorias por meio de suas chaves primárias |
Esses resultados às vezes são surpreendentes:
- foi mais rápido obter a versão completa dos produtos (perf09) do que a versão resumida (perf08), embora a versão completa envolva uma junção entre duas tabelas;
- o tempo do primeiro preenchimento (perf01) excede claramente o de todos os outros preenchimentos que se seguirão;
- solicitar a versão curta dos produtos por meio de seus nomes (perf08) leva mais tempo do que solicitá-la por meio das chaves primárias (perf10). Isso parece bastante lógico. Mas, para as versões longas, ocorre o contrário (perf09, perf11);
Portanto, não nos deteremos nesses resultados. No entanto, eles serão úteis para compararmos esta solução [Spring JDBC] com as soluções:
- [Spring JDBC], das outras cinco SGBD;
- [Spring JPA] que virão a seguir;





























