Skip to content

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;

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:

Image

  • [sql] é a instrução SQL a ser executada;
  • [rowMapper] é uma instância da seguinte interface [RowMapper<T>]:

Image

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:

1
2
3
4
5
6
7
c1, produits11
c1, produit12
...
c1,produits1n
c2, produits21
c2, produits22
...

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:

c1, produits1
c2, produits2

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:

Image

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:

Image

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:

Image

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

Image

  • 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

test
função
durée (s)
perf01
preenche o banco de dados com 2.500 categorias e 5.000 produtos
4,179
perf02
preenche e, em seguida, modifica o banco de dados
7,624
perf03
preenche o banco de dados e, em seguida, exclui todas as categorias e seus produtos
3,911
perf04
preenche o banco de dados e solicita a versão completa de todas as categorias
2,426
perf05
preenche o banco de dados e exclui os 5.000 produtos, um por um, por meio de suas chaves primárias
3,507
perf06
preenche o banco de dados e exclui os 5.000 produtos, um por um, por meio de seus nomes
3,947
perf07
preenche o banco de dados e exclui os 5.000 produtos, um por um, por meio de seus códigos
3,633
perf08
preenche o banco de dados e solicita a versão abreviada de todos os produtos por meio de seus nomes
4,054
perf09
preenche o banco de dados e solicita a versão completa de todos os produtos por meio de seus nomes
2,643
perf10
preenche o banco de dados e solicita a versão curta de todos os produtos por meio de suas chaves primárias
3,463
perf11
preenche o banco de dados e solicita a versão completa de todos os produtos por meio de suas chaves primárias
2,777
perf12
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
3,806
perf13
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
2,828
perf14
preenche o banco de dados e solicita a versão abreviada de todas as categorias por meio de seus nomes
2,731
perf15
preenche o banco de dados e solicita a versão completa de todas as categorias por meio de seus nomes
2,603
perf16
preenche o banco de dados e solicita a versão curta de todas as categorias por meio de suas chaves primárias
2,462
perf17
preenche o banco de dados e solicita a versão completa de todas as categorias por meio de suas chaves primárias
3,287

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;