Skip to content

4. Introdução ao Spring JDBC

Neste capítulo, iremos examinar a seguinte arquitetura:

Esta é a mesma arquitetura de antes. Iremos introduzir duas alterações:

  • a base de dados terá duas tabelas ligadas por uma relação de chave estrangeira;
  • a camada [DAO] será implementada utilizando a biblioteca [Spring JDBC], que simplifica a gestão da API JDBC;

4.1. Configurar o ambiente de desenvolvimento

Utilizando o STS, importe o projeto [spring-jdbc-04] localizado na pasta [<examples>/spring-database-generic/spring-jdbc]

Além disso, precisamos de criar uma nova base de dados MySQL utilizando o cliente [MyManager] (ver secção 3.1):

  • Em [3], os exemplos seguintes utilizam uma base de dados MySQL denominada [dbproduitscategories];
  • Em [9], introduza a palavra-passe do utilizador root (esta palavra-passe é «root» neste documento);
  • em [18], a base de dados [dbproduitscategories] foi criada vazia. Criamos tabelas e preenchemo-la com um script SQL [19-20];
  • Em [21], aceda à pasta [<examples>/spring-database-config/mysql/databases];
  • em [25], certifique-se de que está na base de dados [dbproduitscategories] e não na base de dados [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 a base de dados [dbproduitscategories] na web;

4.2. A base de dados [dbproduitscategories]

A base de dados [dbproduitscategories] é uma extensão da base de dados [dbproduits] discutida anteriormente. Enquanto na tabela [PRODUITS] o produto tinha uma categoria identificada por um número sem significado específico, aqui esse número será uma chave estrangeira na tabela [CATEGORIES].

A tabela [PRODUCTS] é a seguinte:

  • [ID]: a chave primária autoincremental da tabela [PRODUCTS];
  • [NAME]: o nome único do produto [4];
  • [PRICE]: o preço do produto;
  • [DESCRIPTION]: a descrição do produto;
  • [VERSIONING] é o número de versão do produto. A sua versão inicial é 1 [3]. Sempre que o produto é modificado, o seu número de versão é incrementado pelo código que opera a tabela;
  • [CATEGORY_ID]: a chave estrangeira na tabela [CATEGORIES] para identificar a categoria à qual o produto pertence;
  • em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Ela faz referência à coluna [ID] da tabela [CATEGORIES] [4-5];
  • quando uma categoria é eliminada, todos os produtos a ela associados são também eliminados [6]. É importante ter este ponto em conta, pois é utilizado na construção da camada [DAO] que utiliza a base de dados [dbproduitscategories];

A tabela [CATEGORIES] é a seguinte:

  • [ID]: chave primária autoincremental;
  • [VERSIONING]: número da versão da categoria;
  • [NAME]: nome único 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 ficheiro [pom.xml]:

  

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- 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 depende do projeto [mysql-config-jdbc], que configura a camada JDBC;
  • linhas 34–37: o artefacto [spring-boot-starter-jdbc] fornece as bibliotecas 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 {
 
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
 
    // JdbcTemplate
    @Bean
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
        return new NamedParameterJdbcTemplate(dataSource);
    }
 
    // product insertion
    @Bean
    public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
                ConfigJdbc.TAB_PRODUITS_ID);
    }
 
    // insertion category
    @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 Spring;
  • linha 17: o pacote [spring.jdbc.dao] será analisado em busca de componentes Spring que não estejam presentes na classe [AppConfig]. É aqui que encontraremos o componente que implementa a camada [DAO];
  • linha 18: não iremos gerir as transações nós próprios, mas deixá-las-emos a cargo do Spring JDBC. A única coisa a fazer será anotar os métodos que precisam de ser executados dentro de uma transação com a anotação [@Transactional] do Spring. A linha 18 garante que esta anotação é processada e não ignorada. A gestão de transações é tratada por uma das dependências do projeto Spring JDBC importadas através do ficheiro [pom.xml];
  • linha 19: importamos os beans já definidos na classe [generic.jdbc.config.ConfigJdbc] do projeto [mysql-config-jdbc];
  • linhas 23–36: a fonte de dados [tomcat-jdbc] apresentada no exemplo [spring-jdbc-02];
  • linhas 40–42: o gestor de transações associado à fonte de dados definida anteriormente. O bean deve ser denominado [transactionManager], pois este é o nome utilizado pela anotação [@EnableTransactionManagement]. O [DataSourceTransactionManager] é fornecido pela biblioteca Spring JDBC (linha 12);
  • linhas 45–48: o bean [namedParameterJdbcTemplate], no qual se baseará a implementação da camada [DAO]. Este bean é fornecido pela biblioteca Spring JDBC (linha 10). Este bean também está ligado à fonte de dados definida anteriormente (linha 47);
  • linhas 51–55: o bean [simpleJdbcInsertProduit] (nome arbitrário) será utilizado para inserir um produto na tabela [PRODUITS] e recuperar a chave primária gerada. Os vários 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 da chave primária da tabela [PRODUCTS]. Note-se que, para o PostgreSQL, o nome desta coluna deve estar em 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. Exceções do projeto

  

Já vimos as classes [UncheckedException, DaoException, ShortException] no projeto [spring-jdbc-03]. Vamos adicionar uma nova:


package spring.jdbc.infrastructure;
 
public class MyIllegalArgumentException extends UncheckedException {
 
    private static final long serialVersionUID = 1L;
 
    // manufacturers
    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 verificada. Será utilizada para sinalizar uma chamada com argumentos incorretos a um método na camada [DAO]. Não a denominámos [IllegalArgumentException] porque esta exceção já existe no JDK e isso, por vezes, levava o compilador a gerar uma [import] incorreta;

4.6. Entidades do projeto

  

As classes no pacote [spring.jdbc.entities] representam as linhas nas tabelas da base de dados [dbproduitscategories]. Por enquanto, vamos ignorar as tabelas [USERS, ROLES, USERS_ROLE].

Todas as entidades estendem a classe pai [AbstractCoreEntity]:


package spring.jdbc.entities;
 
public abstract class AbstractCoreEntity {
    // properties
    protected Long id;
    protected Long version;
 
    // manufacturers
    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;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [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 and setters
...
}
  • linha 5: o campo [id] será associado à coluna [ID], a chave primária das tabelas;
  • linha 6: o campo [version] será associado à coluna [VERSIONING] das tabelas;
  • linhas 8–26: vários construtores e métodos para construir ou inicializar um objeto [AbstractCoreEntity];
  • linhas 35–47: o método [equals] estabelece 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 tabela onde [id] é a chave primária e onde, portanto, não pode haver duas linhas com o mesmo [id];
  • linhas 30–33: uma proposta para [hashCode];

A classe [Product] representará uma linha na tabela [PRODUCTS]:


package spring.jdbc.entities;
 
import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
    // properties
    private String nom;
    private Long idCategorie;
    private double prix;
    private String description;
    private Categorie categorie;
 
    // manufacturers
    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;
    }
 
    // signature
    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 and setters
...
}
  • linha 6: a classe [Product] estende a classe [AbstractCoreEntity];
  • linhas 8–12: os campos [id, version, name, categoryId, price, description] correspondem às colunas [ID, VERSIONING, NAME, CATEGORY_ID, PRICE, DESCRIPTION] na tabela [PRODUCTS];
  • linha 12: o objeto do tipo [Category] com a chave primária [categoryId]. Este campo pode ou não ser preenchido, dependendo do caso. Quando está preenchido, referimo-nos a um produto de formato longo [LongProduct]; caso contrário, a um produto de formato curto [ShortProduct];
  • linha 5: um filtro JSON. Note que o projeto [mysql-config-jdbc] inclui uma biblioteca JSON. O filtro é necessário porque o campo [category] pode ou não estar preenchido. Neste caso, a representação JSON do produto difere. Para lidar com estes dois casos, vamos configurar o filtro [jsonFilterProduct] na linha 5. Um filtro JSON permite-nos especificar dinamicamente quais os campos a excluir da representação JSON. Quando sabemos que o campo [category] não foi preenchido, iremos excluí-lo da representação JSON do produto;

A classe [Category] representa uma linha na 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 {
 
    // properties
    private String nom;
    public List<Produit> produits;
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(Long id, Long version, String nom, List<Produit> produits) {
        super(id, version);
        this.nom = nom;
        this.produits = produits;
    }
 
    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
    }
 
    // methods
    public void addProduit(Produit produit) {
        // add a product
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // we add the product
            produits.add(produit);
            // set your category
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }
 
    // getters and setters
...
}
  • linha 9: a classe [Category] estende a classe [AbstractCoreEntity];
  • linha 12: os campos [id, version, name] correspondem às colunas [ID, VERSIONING, NAME] na tabela [CATEGORIES];
  • linha 13: o campo [products] representa a lista de produtos na categoria. Este campo nem sempre é preenchido. Quando não o é, referimo-nos a uma categoria de forma curta [ShortCategorie]; caso contrário, a uma categoria de forma longa [LongCategorie];
  • linhas 32–44: o método [addProduct] permite adicionar um produto à categoria (linha 39) e definir as características da categoria (categoryID e category) no produto adicionado;
  • linha 8: um filtro JSON. Quando a biblioteca JSON precisa de serializar/deserializar um objeto [Category], temos de lhe indicar como lidar com o filtro denominado [jsonFilterCategory];

4.7. A interface Idao<T>

  

A interface [IDao] da camada [DAO] tem a seguinte assinatura:


package spring.jdbc.dao;
 
import java.util.List;
 
import spring.jdbc.entities.AbstractCoreEntity;
 
public interface IDao<T extends AbstractCoreEntity> {
 
    // list of all T entities
    public List<T> getAllShortEntities();
 
    public List<T> getAllLongEntities();
 
    // special entities - short version
    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);
 
    // special entities - long version
    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);
 
    // update of several entities
    public List<T> saveEntities(Iterable<T> entities);
 
    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities();
 
    // deletion of multiple entities
    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: Aqui temos uma interface [IDao] parametrizada por um tipo T com uma condição: este tipo deve estender a classe [AbstractCoreEntity] ou implementar a interface [AbstractCoreEntity]. A palavra-chave [extends] é utilizada em ambos os casos. Aqui, T será instanciado pelo tipo [Product] ou pelo tipo [Category]. De facto, torna-se rapidamente evidente que realizamos os mesmos tipos de operações (inserção, modificação, eliminação, seleção) nos tipos [Product] e [Category]. Por isso, faz sentido agrupar estes métodos numa interface genérica;
  • dependendo do contexto, os termos [LongEntity] e [ShortEntity] referem-se a situações diferentes:
    • quando T é do tipo [Product]:
      • [ShortEntity] é o produto sem o campo [Category] preenchido;
      • [LongEntity] é o produto com o seu campo [Category] preenchido;
    • quando T é o tipo [Category]:
      • [ShortEntity] é a categoria sem o campo [List<Product> products] preenchido;
      • [LongEntity] é o produto com o campo [List<Product> products] preenchido;

Temos, portanto, uma interface com 19 métodos. A maioria dos métodos são duplicados. Tomemos o exemplo do 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 de chaves primárias das entidades para as quais queremos a versão curta. Esta lista é apresentada de duas formas diferentes:
    • linha 1: uma lista que implementa a interface [Iterable<Long>]. O tipo [List<Long>] implementa esta interface, mas existem muitos outros. Se tivéssemos escrito [List<Long> ids], isso teria sido suficiente para os nossos exemplos, mas teria obrigado o utilizador dos nossos exemplos a realizar conversões caso o seu parâmetro não fosse exatamente do tipo esperado;
    • linha 3: infelizmente, o tipo `Long[]` não implementa a interface `Iterable<Long>`. Neste caso, utilizaremos a versão da linha 3. O parâmetro formal [Long... ids] (3 pontos) pode aceitar valores tanto de uma matriz como de uma sequência de IDs: getShortEntitiesById(id1, id2, ...);

Esta mesma interface IDao<T> será implementada pela seguinte arquitetura:

onde uma camada [JPA] (Java Persistence API) será inserida entre a camada [DAO] e o controlador JDBC do SGBD. Isto permitir-nos-á ter uma camada de teste comum para ambas as arquiteturas. Em ambos os casos, a camada [DAO] apresentará duas interfaces:

  • IDao<Product> para aceder à tabela [PRODUCTS];
  • IDao<Category> para aceder à tabela [CATEGORIES];

4.8. Implementação da interface IDao<T>

  
  • a interface IDao<Product> é implementada pela classe [DaoProduct];
  • A interface IDao<Category> é implementada pela classe [DaoCategory];

As classes [DaoProduct] e [DaoCategory] estendem ambas a seguinte classe abstrata [ AbstractDao]:


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> {
 
    // injections
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;
 
    // local
    protected String simpleClassName = getClass().getSimpleName();
 
    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // argument validity
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // obtaining by tranches
        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)));
        }
        // result
        return entities;
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Long... ids) {
        // argument validity
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // result
        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éthodes privées ----------------------------------------------
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
    }
 
    @SuppressWarnings("unchecked")
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
    ...
    }
 
    // méthodes protégées ----------------------------------------------
    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`). Como tal, não pode ser instanciada. Só pode ser derivada. Esta classe tem várias funções:
    • definir a natureza da transação na qual cada método é executado;
    • para tratar o maior número possível de tarefas comuns para ambas as implementações das interfaces [IDao<Product>] e [IDao<Category>]. Isto envolve, principalmente, a validação dos argumentos. Não são aceites argumentos nulos nem listas vazias;
    • Unificar os tipos dos parâmetros `T... params` e `Iterable<T> params` num único tipo: `List<T> params`;
    • Delegar o trabalho às classes filhas assim que se tornar específico de uma das duas interfaces;

Graças à padronização dos parâmetros dos vários métodos realizada pela classe [AbstractDao], as classes filhas [DaoProduit] e [DaoCategorie] terão apenas 10 métodos para implementar em vez de 19:


    // methods implemented by child classes ----------------------------------------------
    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();

Vamos ver alguns métodos da classe [AbstractDao].

Método [getShortEntitiesById]

Este método recupera a versão resumida das entidades para as quais são fornecidas chaves primárias.


    // injections
    @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: Injetamos o bean [maxPreparedStatementParameters] definido no ficheiro de configuração [ConfigJdbc], que configura a camada JDBC para um SGBD específico:

    // max number of parameters of a [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 define o número máximo de parâmetros que podem ser passados para um [PreparedStatement]. Este requisito não surgiu com o SGBD MySQL, que aceitava 10 000 parâmetros para um [PreparedStatement]. Durante os testes com o SGBD SQL Server, foi lançada uma exceção indicando que o número máximo de parâmetros para um [PreparedStatement] era 2.100. Por conseguinte, este número tornou-se um parâmetro de configuração para os vários SGBDs. Deve, portanto, ser colocado no projeto de configuração [sgbd-config-jdbc] para cada SGBD;

Voltemos ao código do método [getShortEntitiesById]:


    // injections
    @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 para 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 dentro de uma transação de leitura apenas. Poder-se-ia questionar a utilidade de tal transação, uma vez que o método apenas realiza leituras e, por isso, em caso de falha, não há nada a reverter. O autor da biblioteca [Spring Data] recomenda isto e explica o motivo. Segui o seu conselho;

O corpo do método é o seguinte:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // validité de l'argument
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
...
}
  • Linha 5: A validade do parâmetro [ids] é verificada pelo seguinte método:

    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
        // elements null ?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
        }
        // elements vide ?
        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>();
            }
        }
        // résultat par défaut
        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 para o método. Pode ser [Long, String, AbstractCoreEntity];
  • linha 1: o método [checkNullOrEmptyArgument] recebe dois parâmetros:
    • [Iterable<T2> elements]: o parâmetro a ser testado;
    • [checkEmpty]: definido como true se precisarmos de verificar se o parâmetro anterior é uma lista não vazia;
  • linhas 4–6: verificamos se o parâmetro [elements] não é nulo. Se for, é lançada uma [MyIllegalArgumentException];
  • linhas 8–15: se a lista estiver vazia e devêssemos verificar se ela não estava vazia, lançamos uma [MyIllegalArgumentException];
  • linha 13: se a lista estiver vazia e não devêssemos verificar se ela era diferente de vazia, então retornamos uma lista vazia de elementos do tipo T. A interface [Iterable<T2>] possui um método [iterator()] que permite que um iterador percorra os elementos da lista que implementa a interface. Dois métodos desse iterador são úteis:
    • [iterator].hasNext(): retorna true se a lista ainda tiver um elemento para processar, false caso contrário;
    • [iterator].next(): retorna o elemento atual da lista e avança o iterador em um elemento;
  • Por fim,
    • se o argumento [T2... elements] for nulo ou vazio, é lançada uma [MyIllegalArgumentException];
    • se o argumento [T2... elements] for uma lista vazia e isso for válido, então é devolvida uma lista vazia de elementos do tipo T;

Existe um método semelhante 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) {
        // argument validity
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        // obtaining by tranches
        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)));
        }
        // result
        return entities;
}
  • linha 7: se chegarmos a este ponto, significa que o argumento [Iterable<Long> ids] é válido;
  • linhas 7–14: veremos mais tarde que o método [getShortEntitiesById] será implementado por um tipo [PreparedStatement] que receberá como parâmetros a lista de chaves primárias a pesquisar. 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 real será do tipo List<Long>. Cada elemento desta lista será passado como um parâmetro ? num [PreparedStatement]. No entanto, especificámos que este tipo aceita um número máximo de parâmetros, um número definido pelo campo [maxPreparedStatementParameters] da classe;

  • linha 7: a lista de entidades T que será devolvida pelo método [getShortEntitiesById]. Esta lista será construída em blocos de [maxPreparedStatementParameters] elementos;
  • Linha 9: A partir do argumento [Iterable<Long> ids], criamos um tipo [List<Long> listIds]. A classe [Lists] é uma classe da biblioteca Google Guava que oferece vários 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 pesquisar na base de dados;
  • linhas 11–13: são pesquisadas em grupos de [size = maxPreparedStatementParameters] elementos;
  • linha 12: um cálculo para evitar ultrapassar o fim da lista [listIds];
  • linha 13: as entidades T são obtidas chamando [getShortEntitiesById(listIds.subList(i, limit))]. Este método é definido na classe como:

abstract protected List<T> getShortEntitiesById(List<Long> ids);

É, portanto, a classe filha que irá recuperar as entidades T da base de dados:

  • [DaoProduct] se T for do tipo [Product];
  • [DaoCategory] se T for do tipo [Category];

A vantagem desta abordagem na classe pai é dupla:

  • a assinatura do método [getShortEntitiesById] na classe filha é única: o seu argumento é do tipo [List<Long> ids];
  • a classe filha não precisa lidar com a questão dos parâmetros [maxPreparedStatementParameters] de um [PreparedStatement]. A sua classe pai já tratou disso por ela;
  • linha 13: as entidades devolvidas pela classe filha são adicionadas à lista de entidades que serão devolvidas pela classe pai (linha 16);

Agora, vamos ver a implementação do outro método da classe, [getShortEntitiesById]:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Long... ids) {
        // validité de l'argument
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        // résultat
        return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
  • linha 3: o tipo do argumento mudou: Long... ids;
  • linha 5: a validade deste argumento é verificada;
  • linha 7: chamamos o método [getShortEntitiesById] que acabámos de descrever. Aqui, mais uma vez, usamos a classe [Lists] da biblioteca [Google Guava]. Note que temos de realizar um cast explícito para o tipo [Iterable<Long>] para ajudar o compilador a escolher o método correto, uma vez que o método [getShortEntitiesById] tem três assinaturas na classe:
    • List<T> getShortEntitiesById(Long... ids);
    • List<T> getShortEntitiesById(Iterable<Long> ids);
    • List<T> getShortEntitiesById(List<Long> ids), que é abstrato e implementado pela classe filha;

Não faremos mais comentários sobre a classe abstrata [AbstractDao], a classe pai das classes [DaoProduit] e [DaoCategorie]. Limitar-nos-emos a referir que, por vezes, é útil factorizar comportamentos comuns a várias classes numa classe pai, seja ela abstrata ou não. Após este trabalho, as classes filhas têm apenas os seguintes métodos por implementar:


    // methods implemented by child classes ----------------------------------------------
    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 na Secção 4.8 mostra os diferentes tipos de transações utilizados para cada método. Tenha em atenção os seguintes pontos:

  • os métodos que leem a base de dados são anotados com [@Transactional(readOnly = true)];
  • os métodos que modificam a base de dados são anotados com [@Transactional];
  • os métodos [delete] não são anotados e, por isso, não são executados dentro de uma transação. A ideia é que, se uma eliminação falhar, o utilizador provavelmente não vai querer reverter todas as que ocorreram com sucesso anteriormente;

4.9. A classe [DaoCategorie]

  

A classe [DaoCategorie] implementa a interface [IDao<Categorie>], que permite aceder aos dados da tabela [CATEGORIES] da base de dados MySQL [dbproduitscategories]. A sua estrutura básica é 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> {
 
    // constants
 
    // injections
    @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, pode ser injetada noutros componentes Spring;
  • linha 29: a classe [DaoCategorie] estende a classe abstrata [AbstractDao<Categorie>], tornando-a uma implementação da interface [IDao<Categorie>];
  • linhas 34–37: injeção de beans definidos na classe [AppConfig] descrita na secção 4.4;
  • linhas 38–39: injeção de uma referência à classe [DaoProduit], que implementa a interface [IDao<Produit>] que gere o acesso aos dados na tabela [PRODUITS];
  • linhas 41–89: implementação da interface [IDao<Category>];
  • 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] devolve todas as categorias da tabela [CATEGORIES] na sua forma 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 dependem do objeto [namedParameterJdbcTemplate] definido no ficheiro de configuração do Spring e fornecido pela biblioteca JDBC do Spring. Este objeto possui vários métodos. O método 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 [Select]. Este lida com quaisquer exceções, bem como abre e fecha a ligação ao SGBD. A única coisa que não consegue fazer é encapsular os elementos do [ResultSet] — os objetos que obtém — num tipo [Category], porque não conhece o mapeamento entre os campos do tipo [Category] e as colunas do [ResultSet]. Veremos mais tarde que este mapeamento é criado utilizando a tecnologia JPA, que encapsulará automaticamente os 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 esta encapsulação;

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 instrução 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 recupera as colunas [ID, VERSIONING, NOM] da tabela [CATEGORIES]. Iremos utilizar consistentemente a seguinte sintaxe:


SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...

O que é importante é a denominação das colunas devolvidas pela instrução SELECT utilizando o atributo [as column_name]. Esta é a única forma de garantir a portabilidade entre SGBDs, uma vez que todos eles têm a sua própria forma proprietária de nomear colunas devolvidas por uma instrução SELECT, na qual colunas de tabelas diferentes têm o mesmo nome (por exemplo, ID, NAME ou VERSIONING no nosso caso). Resolvemos esta ambiguidade especificando os nomes que estas 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, como tal, deve implementar o método [mapRow] nas linhas 4–5, cuja função é encapsular uma linha do [ResultSet rs] produzido pela instrução [SELECT] num tipo [Categorie];
  • linha 5: esta encapsulação é realizada. Note-se que o nome utilizado pelos métodos [rs.getType(name)] é o nome utilizado nos atributos [as name] das colunas SELECT;

Assim, obtivemos a lista de categorias na sua forma abreviada sem tratar exceções nem gerir a ligação. Esta é a vantagem da biblioteca Spring JDBC, que trata de tudo o que pode ser abstraído na gestão de elementos da tabela e deixa ao programador a tarefa de tratar do que não pode ser.

4.9.2. O método [getAllLongEntities]

O método [getAllLongEntities] devolve todas as categorias da tabela [CATEGORIES] na sua forma longa:


    @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 instrução 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 é recuperar as categorias juntamente com os produtos associados. Isto é conseguido através da junção da tabela [CATEGORIES] com a tabela [PRODUCTS], utilizando a chave estrangeira [CATEGORY_ID] da tabela [PRODUCTS] para a tabela [CATEGORIES]. A sintaxe [FROM PRODUCTS p RIGHT JOIN CATEGORIES c ON p.CATEGORY_ID=c.ID] também recupera categorias que não têm produtos associados. Neste caso, a consulta SELECT devolve uma categoria e um produto com todas as colunas definidas como 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");
        // cas de la catégorie sans produits
        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 devolver um objeto [Category] com o seu campo [products] preenchido, com base numa linha do [ResultSet] devolvido pela instrução SELECT anterior;

Em última análise, a operação:


[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]

devolverá 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 [products] que é uma lista de produtos contendo um único elemento [productsij]. Agora, precisamos da seguinte lista:

c1, produits1
c2, produits2

onde cada categoria [ci] terá um campo [products] que é a lista de produtos [producti1, producti2, ...]. Isto é conseguido passando a lista de categorias 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;
        }
        // catégories à rendre
        List<Categorie> cats = new ArrayList<Categorie>();
        // on parcourt la liste des catégories obtenues
        for (Categorie categorie : categories) {
            boolean trouve = false;
            for (Categorie cat : cats) {
                if (categorie.equals(cat)) {
                    cat.addProduit(categorie.getProduits().get(0));
                    trouve = true;
                    break;
                }
            }
            // trouvé ?
            if (!trouve) {
                cats.add(categorie);
            }
        }
        // résultat
        return cats;
}
  • linha 1: [List<Category> categories] é a lista de categorias a filtrar (ou agrupar);
  • linha 6: a lista de categorias a devolver ao chamador;
  • linhas 8–21: cada categoria na lista a ser filtrada é processada;
  • linhas 10–16: verificamos se a categoria atual [category] já está presente na lista de categorias [cats] a ser construída (note-se que duas categorias são consideradas iguais se tiverem a mesma chave primária, ver secção 4.6);
  • linhas 11–14: se for esse o caso, o produto encapsulado em [categoria] é adicionado à lista de produtos em [cat];
  • linhas 18-20: se a categoria atual [categorie] ainda não estiver presente na lista de categorias [cats] a ser construída, então é adicionada a ela juntamente com a sua lista de produtos, que contém um único elemento;

Vamos considerar o caso em que a instrução SQL SELECT retorna categorias sem produtos associados. Que entidade a classe [LongCategorieMapper] retorna?


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");
        // cas de la catégorie sans produits
        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;
    }
}

Se a instrução SQL SELECT devolver uma categoria sem produtos, as colunas do produto devolvidas com a categoria contêm todas o valor SQL NULL. Este caso é tratado nas linhas 7–9:

  • linha 7: recupera a chave primária do produto como um inteiro longo;
  • linha 9: verificamos se o valor lido era SQL NULL (rs.wasNull). Se não for, adicionamos o produto à lista na linha 6; caso contrário, nada é adicionado e a lista de produtos permanece vazia.

Note que, em todos os casos, devolvemos uma categoria com um campo [products] que não é nulo.

4.9.3. O método [getShortEntitiesById]

O método [getShortEntitiesById] é semelhante ao método [getAllShortEntities], exceto que retorna apenas as entidades cujas chaves primárias estão especificadas numa 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);
        }
}
  • Linha 4: A assinatura do método [query] utilizado é a seguinte:

Image

O primeiro parâmetro é uma instrução SQL [Select] parametrizada. O segundo é um dicionário que associa cada parâmetro a um valor. O terceiro é a instância da classe que encapsula uma linha do [ResultSet] resultante da instrução [Select] num objeto do tipo T;

  • Linha 4: A instrução SQL [Select] parametrizada é 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)";

Esta consulta recupera da tabela [CATEGORIES] as categorias cujas chaves primárias constam na lista :ids.

  • Linha 5: O segundo parâmetro do método [query] aqui é um dicionário que associa a chave 'ids' (primeiro parâmetro) à lista [ids] passada na linha 1 como parâmetro para o método [getShortEntitiesById]. A classe [Collections] pertence à biblioteca [Google Guava], que já discutimos. [Collections.singleMap] retorna um dicionário com um único elemento;
  • Linha 5: A classe responsável por encapsular uma linha do [ResultSet] resultante do [Select] num objeto do tipo [Category] é a classe [ShortCategoryMapper] que já examinámos;

É normalmente aqui que o bean [maxPreparedStatementParameters] entra em ação. De facto, o parâmetro [:ids] da instrução SQL, que representa uma lista de chaves primárias, pode conter entre 1 e vários milhares de parâmetros. Existe um limite para este número que depende de cada SGBD. Para o MySQL, conseguimos passar 10 000 parâmetros sem erros e não testámos além desse limite. Para o SQL Server, o limite oficial é de 2.100. Para o Firebird, 1.000 já era demasiado. Reduzimo-lo para 100. De um modo geral, não testámos o limite máximo deste número para os vários SGBDs.

4.9.4. O método [getLongEntitiesById]

O método [getLongEntitiesById] é análogo ao método [getShortEntitiesById], exceto que devolve as versões longas 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] é semelhante ao método [getShortEntitiesById], exceto que as categorias são recuperadas pelos seus nomes em vez das 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 instrução 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] é semelhante ao método [getShortEntitiesByName], com a diferença de que as categorias são recuperadas nas 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 instrução 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] elimina todas as categorias da tabela [CATEGORIES]:


    @Override
    public void deleteAllEntities() {
        try {
            // on supprime toutes les catégories et par cascade tous les produits
            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 tem a seguinte assinatura:

Image

O primeiro parâmetro é uma instrução SQL de atualização parametrizada (INSERT, UPDATE, DELETE). O segundo parâmetro é o dicionário que associa valores aos vários parâmetros da instrução SQL. O método devolve o número de linhas atualizadas pela instrução SQL.

  • Linha 4: A instrução SQL [ConfigJdbc.DELETE_ALLCATEGORIES] é a seguinte:

public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";

Portanto, esta não é uma consulta parametrizada. É 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] elimina 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 instrução 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] elimina 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 instrução 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 deste método é a seguinte:


    @Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {

O método recebe uma lista de categorias como parâmetro. Ele realiza as seguintes operações sobre elas:

  • se a categoria tiver uma chave primária nula, é executada uma operação SQL INSERT; caso contrário, é executada uma operação SQL UPDATE;
  • esta operação é repetida para cada produto na categoria;

O método devolve a lista de categorias persistidas ou atualizadas. A lista devolvida é uma representação exata das categorias e produtos presentes nas tabelas, independentemente dos números de versão: estes não são, na verdade, modificados nas entidades atualizadas, apesar de terem sido incrementados na base de dados.

Este é, de longe, o método mais complexo. O seu código é o seguinte:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // --------------------------------------------- categories
            List<Categorie> insertCategories = new ArrayList<Categorie>();
            List<Categorie> updateCategories = new ArrayList<Categorie>();
            // on scanne les catégories
            for (Categorie categorie : entities) {
                // insert or update ?
                if (categorie.getId() == null) {
                    insertCategories.add(categorie);
                } else {
                    updateCategories.add(categorie);
                }
            }
            // insertions catégories
            if (insertCategories.size() > 0) {
                insertCategories(insertCategories);
            }
            // updates categories
            if (updateCategories.size() > 0) {
                updateCategories(updateCategories);
            }
 
            // --------------------------------------------- produits
            // on met à jour les produits des catégories
            List<Produit> allProduits = new ArrayList<Produit>();
            for (Categorie categorie : entities) {
                List<Produit> produits = categorie.getProduits();
                Long idCategorie = categorie.getId();
                if (produits != null) {
                    // on l'ajoute à la liste de tous les produits
                    allProduits.addAll(produits);
                    // on scanne les produits un à un pour les relier à leur catégorie
                    for (Produit produit : produits) {
                        // on relie le produit à sa catégorie
                        produit.setIdCategorie(idCategorie);
                        produit.setCategorie(categorie);
                    }
                }
            }
            // insert / update des produits
            daoProduit.saveEntities(allProduits);
            // résultat
            return entities;
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(207, e, simpleClassName);
        }
    }
  • linhas 5–23: inserir ou atualizar categorias;
  • linhas 26–43: inserir ou atualizar produtos;
  • linhas 35-39: este código associa cada produto à sua categoria. Na fase anterior de inserção de categorias, foi atribuída a cada uma delas uma chave primária que deve ser colocada no campo [idCategorie] do produto (linha 37). Além disso, as linhas 37–38 permitem corrigir situações em que o chamador não tenha associado corretamente cada produto à sua categoria. Para garantir que esta relação está correta, deve ser utilizado o método [Category].add(Product p), mas nada impede um utilizador de adicionar um produto diretamente à lista de produtos da categoria sem utilizar este método, correndo o risco de os campos [idCategory, category] do produto p serem preenchidos incorretamente;
  • Linha 43: Delegamos a tarefa de persistir/atualizar os produtos à instância da interface [IDao<Product>]. Recorde-se que esta instância foi injetada na classe [DaoCategory]:

    @Autowired
    private IDao<Produit> daoProduit;

4.9.10.2. Inserção de categorias

As categorias são inseridas na tabela [CATEGORIES] utilizando o seguinte método privado [insertCategories]:


private List<Categorie> insertCategories(List<Categorie> categories) {
        Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
        try {
            // catégories à ajouter
            for (Categorie categorie : categories) {
                Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
                // on mémorise la clé primaire
                mapCategories.put(newId.longValue(), categorie);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // tout est OK - on affecte les clés primaires aux catégories persistées
        for(Long id : mapCategories.keySet()){
            Categorie categorie=mapCategories.get(id);
            categorie.setId(id);
        }        
        // résultat
        return categories;
    }
  • Linha 6: Utilizamos o bean [simpleJdbcInsertCategorie] injetado na classe pelas seguintes linhas:

    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertCategorie;

Este bean está definido na classe [AppConfig] do projeto da seguinte forma:


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);
}
  • Linha 5: A classe [SimpleJdbcInsert] é uma classe da biblioteca Spring JDBC (linha 1):
    • o parâmetro do construtor [SimpleJdbcInsert] é a fonte de dados na qual a operação é realizada;
    • a cláusula [withTableName] especifica a tabela na qual um elemento deve ser inserido, neste caso a tabela [CATEGORIES];
    • a cláusula [usingGeneratedKeyColumns] especifica a coluna de chave primária gerada automaticamente, neste caso a coluna [ID];
    • a cláusula [usingColumns] restringe a inserção a determinadas colunas. Aqui, excluímos a coluna [ID], que é gerada automaticamente pelo SGBD, e a coluna [VERSIONING], que tem um valor padrão de 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 {
            // catégories à ajouter
            for (Categorie categorie : categories) {
                Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
                // on mémorise la clé primaire
                mapCategories.put(newId.longValue(), categorie);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // tout est OK - on affecte les clés primaires aux catégories persistées
        for(Long id : mapCategories.keySet()){
            Categorie categorie=mapCategories.get(id);
            categorie.setId(id);
        }        
        // résultat
        return categories;
}
  • Linha 6: É utilizado o método [simpleJdbcInsertCategorie.executeAndReturnKey]:

Image

O método espera um dicionário como parâmetro que mapeia as colunas da tabela para os valores a serem inseridos nelas. Ele retorna a chave primária como um tipo [Number]. O método [Number.longValue()] é utilizado para obter a chave primária como 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 preencher [NAME], e os valores do dicionário são os valores a inserir nessas colunas.

  • linha 8 [insertCategories]: a chave primária recuperada é armazenada num dicionário. Vamos esperar até termos a certeza de que todas as entidades foram inseridas antes de lhes atribuirmos as suas chaves primárias. De facto, no caso de uma exceção, todas as inserções serão revertidas, e queremos que as entidades [categories] da linha 1 permaneçam inalteradas também;
  • linhas 14–17: agora que temos a certeza de que tudo correu bem, atribuímos as chaves primárias geradas às categorias;
  • linha 19: devolvemos a lista de categorias com as suas chaves primárias;

4.9.10.3. Atualização de categorias

As categorias são atualizadas utilizando o seguinte método privado [updateCategories]:


    private void updateCategories(List<Categorie> categories) {
        try {
            for (Categorie categorie : categories) {
                // basic category update
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
                        new BeanPropertySqlParameterSource(categorie));
                // did we succeed?
                Long idCategorie = null;
                if (nbLignes == 0) {
                    // we didn't succeed - we're trying to find out why
                    // search for the basic category
                    idCategorie = categorie.getId();
                    List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
                    if (categoriesInBd.size() == 0) {
                        // category does not exist
                        throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
                                idCategorie));
                    } else {
                        // the version was no good
                        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 na base de dados com uma categoria C2 na memória só é permitida se as categorias C1 e C2 tiverem a mesma versão. Este número de versão é utilizado para impedir atualizações simultâneas da entidade por dois utilizadores diferentes: dois utilizadores, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e persiste esta modificação na base de dados: o número de versão passa então a ser V1+1. U2, por sua vez, modifica E e grava essa modificação na base de dados: receberá uma exceção porque a sua versão (V1) difere da que se encontra na base de dados (V1+1).

  • Linhas 2–29: O bloco `try` tem dois blocos `catch`:
    • o primeiro, na linha 25, existe para permitir que qualquer exceção [DaoException] lançada pelo código na linha 13 passe;
    • o segundo, na linha 27, existe para tratar outros tipos de exceção;
  • linha 3: verificamos todas as categorias a serem atualizadas;
  • linha 4: atualizamos a categoria atual utilizando o método [namedParameterJdbcTemplate.update]:

Image

  • Vamos analisar a instrução:

            int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,                         new BeanPropertySqlParameterSource(categorie));

A instrução 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 instrução tem três parâmetros (:id, :version, :nom) cujos valores se encontram nos campos com o mesmo nome no objeto [categorie] modificado. Utilizamos esta funcionalidade passando [new BeanPropertySqlParameterSource(categorie)] como segundo parâmetro, o que especifica que «os valores dos parâmetros se encontram nos campos com os mesmos nomes neste Java bean»;

O resultado devolvido por esta operação, quando executada normalmente, é o número de linhas modificadas, ou seja, 0 ou 1.

Voltemos ao código que estamos a examinar:


private void updateCategories(List<Categorie> categories) {
        try {
            for (Categorie categorie : categories) {
                // basic category update
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
                        new BeanPropertySqlParameterSource(categorie));
                // did we succeed?
                Long idCategorie = null;
                if (nbLignes == 0) {
                    // we didn't succeed - we're trying to find out why
                    // search for the basic category
                    idCategorie = categorie.getId();
                    List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
                    if (categoriesInBd.size() == 0) {
                        // category does not exist
                        throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
                                idCategorie));
                    } else {
                        // the version was no good
                        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: verificar se a atualização foi bem-sucedida;
  • linha 10: a atualização falhou. Uma vez que a cláusula [WHERE] envolve as colunas [ID] e [VERSIONING], procuramos a coluna que causou a falha da [WHERE];
  • linhas 12–18: verificamos se a chave [id] da categoria está na base de dados. Caso contrário, lançamos uma [RuntimeException] com uma mensagem de erro apropriada;
  • linhas 19–22: tratamos o caso em que a versão estava incorreta;

4.10. A classe [DaoProduit]

  

A classe [DaoProduit] implementa a interface [IDao<Produit>], que fornece acesso aos dados da tabela [PRODUITS] da base de dados MySQL [dbproduitscategories]. A 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> {
 
    // injections
    @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 [DaoCategory]. Analisaremos apenas alguns métodos.

4.10.1. O método [getShortEntitiesById]

O método [getShortEntitiesById] devolve 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 instrução 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 [ShortProductMapper], responsável por encapsular o [ResultSet] numa 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] devolve a versão longa 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: A instrução SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] é a 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 [LongProductMapper], 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 indistintamente para inserir novos produtos (id==null) ou atualizar produtos existentes (id!=null):


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        try {
            // produits à insérer
            List<Produit> insertProduits = new ArrayList<Produit>();
            // produits à mettre à jour
            List<Produit> updateproduits = new ArrayList<Produit>();
            // on scanne la liste des entités reçues
            for (Produit produit : entities) {
                Long id = produit.getId();
                if (id == null) {
                    insertProduits.add(produit);
                } else {
                    updateproduits.add(produit);
                }
            }
            // ajouts
            insertProduits(insertProduits);
            // modifications
            updateProduits(updateproduits);
            // résultat
            return entities;
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(103, e, simpleClassName);
        }
}

Linha 18: Os produtos a inserir são adicionados utilizando o seguinte método privado [insertProducts]:


private List<Produit> insertProduits(List<Produit> produits) {
        Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
        try {
            // produits à ajouter
            for (Produit produit : produits) {
                Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
                // on note la clé primaire
                mapProduits.put(newId.longValue(), produit);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // tout est OK - on affecte les clés primaires aux produits persistés
        for (Long id : mapProduits.keySet()) {
            Produit produit = mapProduits.get(id);
            produit.setId(id);
        }
        // résultat
        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;
    }

Este método é análogo ao método [insertCategories] discutido na Secção 4.9.10.3.

  • Linha 4: Utilizamos o bean [simpleJdbcInsertProduit] que foi injetado na classe:

    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertProduit;

Este 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 [simpleJdbcInsertProduct]
    • está ligado à base de dados [dbproduitscategories] (linha 3) e à tabela [ConfigJdbc.TAB_PRODUITS] nessa base de dados (linha 4);
    • a chave primária desta tabela é gerada na coluna [ConfigJdbc.TAB_PRODUITS_ID] (linha 5);
    • são atribuídos valores apenas às colunas [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (linha 6);

O método [updateProducts], que atualiza os produtos (linha 20 de [saveEntities]), é o seguinte:


private void updateProduits(List<Produit> updateProduits) {
        try {
            // we scan products
            for (Produit produit : updateProduits) {
                // basic product update
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
                        new BeanPropertySqlParameterSource(produit));
                // did we succeed?
                Long idProduit = null;
                if (nbLignes == 0) {
                    // we didn't succeed - we're trying to find out why
                    // we search for the basic product
                    idProduit = produit.getId();
                    List<Produit> produitsInBd = getShortEntitiesById(idProduit);
                    if (produitsInBd.size() == 0) {
                        // the product does not exist
                        throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
                                idProduit));
                    } else {
                        // the version was no good
                        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 ao que atualiza as categorias (ver secção 4.9.10.3). Na linha 23, a instrução 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 na classe [Product], o que permite que a instrução nas linhas 6–7 seja utilizada para atualizar o produto atual.

4.11. A camada de teste

  

A camada de teste é composta por três classes de teste:

  • [JUnitTestCheckArguments]: Os testes nesta classe chamam os vários métodos da camada [DAO] com argumentos inválidos e verificam se estes respondem corretamente;
  • [JUnitTestDao]: Os testes nesta classe chamam os vários métodos da camada [DAO] e verificam se estes fazem o que é esperado;
  • [JUnitTestPushTheLimits] não se destina a testar a camada [DAO], mas sim a medir o seu desempenho;

Esta camada de testes desempenha um papel importante neste documento. Na verdade, é 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), ou seja, 36 para os seis SGBDs testados. A camada de testes permite-nos verificar se todas as implementações se comportam da mesma forma.

4.11.1. O teste [JUnitTestCheckArguments]

A classe de teste [JUnitTestCheckArguments] possui 48 métodos que testam como os métodos da camada [DAO] reagem quando chamados com argumentos incorretos. A 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 {
 
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    // local data
    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á executado 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 na camada [DAO];
  • linhas 29–44: parâmetros de chamada incorretos para os métodos da camada [DAO];
  • linha 29: um ponteiro nulo 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 nulo do tipo String[] como matriz de nomes;
  • linha 30: uma matriz vazia do tipo String[] como lista de nomes;
  • ...

Com o campo [names1], realizamos o seguinte teste, por exemplo:


    @Test(expected = MyIllegalArgumentException.class)
    public void getShortProduitsByName1() {
        daoProduit.getShortEntitiesByName(names1);
}
  • Linha 1: Especificamos que o teste [getShortProduitsByName1] deve lançar uma [MyIllegalArgumentException]

Com o campo [names2], realizamos o seguinte teste, por exemplo:


    @Test(expected = MyIllegalArgumentException.class)
    public void getLongCategoriesByName2() {
        daoCategorie.getLongEntitiesByName(names2);
}

Com o campo [names3], realizamos o seguinte teste, por exemplo:


    @Test(expected = MyIllegalArgumentException.class)
    public void getLongCategoriesByName3() {
        daoCategorie.getLongEntitiesByName(names3);
}

Com o campo [names4], realizamos o seguinte teste, por exemplo:


    @Test(expected = MyIllegalArgumentException.class)
    public void getShortProduitsByName4() {
        daoProduit.getShortEntitiesByName(names4);
}

Assim, executamos 48 testes para cobrir todos os casos possíveis. Executamos a configuração de teste denominada [spring-jdbc-generic-04-JUnitTestCheckArguments] [1]. O resultado é 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 fazem o que se espera deles. Há um total de 74 testes que verificam as operações de inserção, seleção, atualização e eliminação de entidades, categorias ou produtos. No total, há mais de 1.000 linhas de código. Iremos examinar apenas alguns destes métodos.

4.11.2.1. O esqueleto do teste

A classe [JUnitTestDao] tem o seguinte esqueleto:


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 {
 
    // spring context
    @Autowired
    private ApplicationContext context;
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    // constants
    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() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // we empty table [CATEGORIES] and cascade table [PRODUITS]
        daoCategorie.deleteAllEntities();
        // emptying dictionaries
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }
...
}
  • linhas 27-28: tal como no teste [JUnitTestCheckArguments], este é um teste integrado com o Spring e configurado pela classe [AppConfig] do projeto;
  • linhas 32-33: injeção do contexto Spring, que fornece acesso a todos os seus beans;
  • linhas 35-36: injeção da instância da interface [IDao<Product>] testada pela classe;
  • linhas 37-38: injeção da instância da interface [IDao<Category>] testada pela classe;
  • linhas 41-42: quando um teste requer dados da base de dados, será gerada uma base de dados com [NB_CATEGORIES] categorias, cada uma contendo [NB_PRODUITS] produtos. Teremos, assim, [NB_CATEGORIES] categorias na tabela [CATEGORIES] e [NB_CATEGORIES] * [NB_PRODUITS] produtos na tabela [PRODUITS];
  • linhas 46-47: dois dicionários onde armazenaremos os produtos e as categorias;
  • Linhas 49–62: O método [clean] é executado antes de cada teste (linha 49). Na linha 54, a tabela [CATEGORIES] é limpa. É importante notar aqui que a tabela [PRODUCTS] tem uma chave primária [CATEGORY_ID] na coluna ID da tabela [CATEGORIES], e que esta é definida da seguinte forma;
  • (continuação)
    • em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Esta faz referência à coluna [ID] da tabela [CATEGORIES] [4-5];
    • quando uma categoria é eliminada, todos os produtos a ela associados são também eliminados [6]. É importante ter isto em conta, pois é utilizado na construção da camada [DAO] que utiliza a base de dados [dbproduitscategories];

Portanto, quando o conteúdo da tabela [CATEGORIES] for eliminado, o da tabela [PRODUCTS] também será eliminado.

  • linhas 56-58: limpamos o dicionário de categorias;
  • linhas 59–61: fazemos o mesmo com o dicionário de produtos;

Note que, antes de cada teste, começamos com tabelas vazias na base 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 a base de dados foi devidamente preenchida com dados de teste:


    @Test
    public void fillDataBase() throws BeansException, JsonProcessingException {
        // remplissage base et dictionnaires
        registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
        // affichage
        Object[] data = showDataBase();
        List<Categorie> categories = (List<Categorie>) data[0];
        List<Produit> produits = (List<Produit>) data[1];
        // quelques vérifications
        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);
        }
        // les dictionnaires doivent avoir été épuisés
        Assert.assertEquals(0, mapCategories.size());
        Assert.assertEquals(0, mapProduits.size());
}

Este teste utiliza vários métodos privados:

  • [fill] na linha 4, que preenche a base de dados com dados de teste;
  • [registerCategories] na linha 4, que preenche os dicionários com os dados devolvidos pelo método [fill]. Estes dois dicionários representam as entidades persistentes;
  • [showDataBase] na linha 6, que lê as duas tabelas [CATEGORIES] e [PRODUCTS] e devolve os dados que leu;
  • [checkShortCategorie] na linha 13 verifica a categoria lida por [showDataBase]. Verifica se a versão curta desta categoria corresponde ao que foi armazenado no dicionário de categorias;
  • [checkShortProduct] linha 16 faz o mesmo para os produtos;
  • quando uma entidade é encontrada num dicionário, é removida do dicionário. As linhas 19–20 verificam se ambos os dicionários estão vazios. Se ambas as afirmações forem verdadeiras, isso significa que:
    • todos os valores lidos por [showDataBase] foram de facto encontrados nos dicionários;
    • os dicionários 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) {
        // on remplit les tables
        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);
        }
        // ajout de la catégorie - par cascade les produits vont eux aussi être
        // insérés
        categories = daoCategorie.saveEntities(categories);
        // résultat
        return categories;
}
  • linhas 3–12: criamos uma lista de [nbCategories] categorias, cada uma contendo [nbProduits] produtos;
  • linha 15: esta lista de categorias é persistida. Vimos que o método [daoCategorie.saveEntities] também persiste os produtos associados às categorias, quando estes existem;
  • linha 17: a lista de categorias guardada é devolvida. As entidades guardadas (categorias e produtos) têm agora uma chave primária no seu campo [id];

O método privado [registerCategories] irá adicionar estas entidades a ambos os dicionários:


    private void registerCategories(List<Categorie> categories) {
        // dictionaries
        for (Categorie categorie : categories) {
            mapCategories.put(categorie.getId(), categorie);
            for (Produit produit : categorie.getProduits()) {
                mapProduits.put(produit.getId(), produit);
            }
        }
}

Cada dicionário utiliza a chave primária das entidades como sua chave de acesso.

Depois de feito isto, a base de dados previamente preenchida será lida e apresentada pelo seguinte método privado [showDataBase]:


    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // liste des catégories
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities();
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities();
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // résultat
        return new Object[] { categories, produits };
}
  • linhas 4 e 8: recuperam as versões curtas das categorias e dos produtos;
  • linha 11: devolve um array contendo as duas listas de entidades recuperadas;
  • linhas 5 e 9: as listas de entidades são apresentadas utilizando o seguinte método privado [display]:

    // display a list of elements of type T
    private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, mapper);
        }
}
 
    // display of a T-type element
    private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
        System.out.println(mapper.writeValueAsString(element));
}

As entidades são apresentadas utilizando um mapeador JSON (linha 10). Este mapeador é o segundo parâmetro do método [display], linha 2. O contexto Spring define quatro mapeadores JSON no ficheiro [ConfigJdbc] da dependência Maven [mysql-config-jdbc]:


// filters 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;
    }
  • estes mapeadores JSON (linhas 7–9, 16–18, 26–28, 35–37) têm um atributo

[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)] 

que os torna beans instanciados a cada pedido feito ao contexto Spring. Isto é novo. Todos os beans Spring vistos até agora eram singletons: era criada uma única instância, e essa instância era devolvida sempre que uma referência a ela era solicitada ao contexto Spring. Porquê esta alteração? Na verdade, os quatro beans [jsonMapperShortCategory, jsonMapperLongCategory, jsonMapperShortProduct, jsonMapperLongProduct] configuram o único mapeador JSON (que é, de facto, um singleton) definido nas linhas 2–5. Isto deve ser reconfigurado sempre que um dos quatro beans anteriores for chamado, em vez de apenas uma vez durante a inicialização do contexto. Se tivéssemos decidido ter quatro mapeadores JSON diferentes — um para cada um dos quatro beans —, então estes poderiam ter sido singletons. Isso era perfeitamente possível. Teríamos então escrito as linhas 10, 19, 29, 38:


ObjectMapper jsonMapper = new ObjectMapper();
  • Os quatro mapeadores JSON são utilizados para configurar os filtros JSON das entidades [Product] e [Category]. Na verdade, escrevemos (ver secções 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 [Category] é controlada pelo filtro JSON [jsonFilterCategory], e a da entidade [Product] pelo filtro JSON [jsonFilterProduct]. Os quatro mapeadores JSON no contexto Spring configuram estes dois filtros da seguinte forma:

  • o mapeador [jsonMapperShortCategory] configura o filtro JSON [jsonFilterCategory] para uma versão curta da categoria: o campo [products] 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 [products] será incluído na representação JSON da categoria;
  • o mapeador [jsonMapperShortProduct] configura o filtro JSON [jsonFilterProduct] para uma versão curta do produto: o campo [category] 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;

Já terminámos o método privado [showDataBase]. Voltemos ao código de teste [fillDataBase]:


    @Test
    public void fillDataBase() throws BeansException, JsonProcessingException {
        // remplissage base et dictionnaires
        registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
        // affichage
        Object[] data = showDataBase();
        List<Categorie> categories = (List<Categorie>) data[0];
        List<Produit> produits = (List<Produit>) data[1];
        // quelques vérifications
        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);
        }
        // les dictionnaires doivent avoir été épuisés
        Assert.assertEquals(0, mapCategories.size());
        Assert.assertEquals(0, mapProduits.size());
}
  • linhas 6-8: recuperamos as versões resumidas dos produtos e categorias lidas da base de dados;
  • linhas 10-11: verificações iniciais;
  • linhas 12–14: cada categoria devolvida pelo método [showDataBase] é verificada pelo seguinte método privado [checkShortCategory]:

    private void checkShortCategorie(Categorie actual) {
        Long id = actual.getId();
        Categorie expected = mapCategories.get(actual.getId());
        mapCategories.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        // the [products] field cannot be tested in a portable way with jPA implementations
}
  • linha 1: [Category actual] é a categoria lida da base de dados e deve ser idêntica à categoria no dicionário [mapCategories];
  • linha 2: recuperamos a chave primária da categoria lida;
  • linha 3: recuperamos a categoria armazenada com esta chave primária no dicionário de categorias;
  • linha 4: a chave é removida do dicionário para garantir que outra categoria recuperada não utilize a mesma chave;
  • linha 5: verificamos se as duas categorias têm o mesmo nome;

A versão resumida dos produtos devolvidos pelo método [showDataBase] é verificada pelo seguinte método privado [checkShortProduct]:


    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());
        // the [category] field cannot be tested in a portable way with jPA implementations
}
  • linha 1: [Produto Real] é o produto resumido lido da base de dados;
  • linhas 2-3: recuperamos o produto com a mesma chave primária do dicionário de produtos persistidos;
  • linha 4: eliminamos a entrada encontrada no dicionário;
  • linhas 5-8: verificamos se os dois produtos têm os mesmos valores de campo;

4.11.2.4. O método [getLongCategoriesByName3]

Este teste é o seguinte:


    @Test
    public void getLongCategoriesByName3() {
        // remplissage base
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        // test
        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: Preenchemos a base de dados e recuperamos a lista de categorias e produtos armazenados;
  • linha 7: testamos o método [daoCategorie.getLongEntitiesByName(Iterable<String> names)] da camada [DAO]. Solicitamos uma lista de dois produtos identificados pelos seus nomes completos;
  • linha 8: verificamos se a lista devolvida por [daoCategorie.getLongEntitiesByName(Iterable<String> names)] tem, de facto, dois elementos;
  • linha 9: os dois elementos persistidos na linha 4 são adicionados ao dicionário de categorias;
  • linhas 10–12: verificamos se os dois elementos lidos são, de facto, aqueles que foram persistidos;
  • linha 13: verificamos se o dicionário de categorias está vazio, o que significa tanto que todas as categorias lidas foram encontradas no dicionário como que o dicionário não contém quaisquer valores que não tenham sido lidos;

Linha 11: o método [checkLongCategory] verifica a versão longa 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 [products] da categoria não é nulo. Isto porque a leitura de uma categoria no formato longo devolve-a sempre com um campo [products] não nulo. Se a categoria não tiver produtos, então o campo [products] é uma lista vazia, mas existente;

4.11.2.5. O método [updateDataBase1]


@Test
    public void updateDataBase1() {
        // remplissage
        fill(NB_CATEGORIES, NB_PRODUITS);
        // test
        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);
        // relecture
        List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
                .getProduits();
        Assert.assertEquals(produits.size(), produitsInBd.size());
        // vérifications
        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 os preços dos produtos da categoria denominada categorie[1] em 10% e verifica duas coisas:

  • se o preço base foi efetivamente alterado;
  • que a versão do produto atualizado foi incrementada em 1;

O código faz o seguinte:

  • linha 4: preenche a base de dados;
  • linha 7: recupera a categoria denominada «categorie[1]» da base de dados;
  • linhas 8–13: aumenta o preço de todos os produtos em 10% (linha 11). Além disso, cria um dicionário que associa um produto à sua versão (linhas 9 e 12);
  • linha 14: o método [daoProduit.saveEntities] é chamado. Ele atualizará os produtos;
  • linha 16: os produtos da categoria denominada «category[1]» são recuperados da base de dados;
  • linhas 20–24: para todos os produtos desta categoria, verificamos se o preço foi atualizado (linha 22) e se a versão foi incrementada em 1 (linha 23);

4.11.2.6. O método [deleteProductsByProduct1]

O método [deleteProductsByProduct1] elimina produtos da tabela [PRODUCTS]:


    @Test
    public void deleteProduitsByProduit1() {
        // filling
        fill(NB_CATEGORIES, NB_PRODUITS);
        // delete
        daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
        // check
        List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
        Assert.assertEquals(0, produits.size());
}
  • linha 6: eliminamos dois produtos;
  • linhas 8-9: verificamos se já não se encontram na base de dados;

4.11.2.7. O método [getLongProductsById3]


    @Test
    public void getLongProduitsById3() {
        // remplissage
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        // test
        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: Preencher a base de dados e recuperar a lista de categorias persistidas;
  • linha 7: recuperar a versão longa de dois produtos identificados pelos seus nomes da base de dados;
  • linha 9: os produtos [product[0,3], product[1,4]] presentes na lista de categorias da linha 4 são adicionados ao dicionário de produtos;
  • linha 10: estes mesmos dois produtos são pesquisados na base de dados utilizando as suas chaves primárias;
  • linhas 11–14: verificamos se os dados recuperados correspondem aos dados armazenados no dicionário;

O método privado [checkLongProduct] é 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. Conclusão

Vamos parar por aqui. Existem 74 testes até agora, e poderíamos adicionar mais, uma vez que provavelmente me esqueci de alguns casos de teste. Embora não sejam exaustivos, estes testes detetaram inúmeros erros — na sua maioria casos extremos que não foram previstos quando a camada [DAO] foi inicialmente escrita. Uma fase de testes abrangente é essencial para qualquer projeto.

Para executar o teste, podemos usar a configuração de execução importada denominada [spring-jdbc-generic-04.JUnitTestDao].

4.11.3. O teste [JUnitTestPushTheLimits]

O teste [JUnitTestPushTheLimits] é um teste de desempenho. Aproveitamos o facto de os testes JUnit exibirem o seu tempo de execução para medir o desempenho da camada [DAO]. Estes resultados serão então comparados com os das implementações JPA da camada [DAO].

4.11.3.1. Estrutura

O esqueleto da classe [JUnitTestPushTheLimits] é o 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 {
 
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    // constants
    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() {
        // empty table [CATEGORIES]
        daoCategorie.deleteAllEntities();
        // dictionaries
        hCategories = new HashMap<Long, Categorie>();
        hProduits = new HashMap<Long, Produit>();
    }
 
    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // fill the tables
        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);
        }
        // add the category - the products will be cascaded in as well
        categories = daoCategorie.saveEntities(categories);
        // dictionaries
        for (Categorie categorie : categories) {
            hCategories.put(categorie.getId(), categorie);
            for (Produit produit : categorie.getProduits()) {
                hProduits.put(produit.getId(), produit);
            }
        }
        // result
        return categories;
    }
 
....
 
    // -------------------- private methods
    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 vemos a estrutura da classe [JUnitTestDao]. Já nos deparámos com todos estes métodos. O teste funciona com uma base de dados de 2.500 categorias, cada uma contendo 2 produtos (linhas 32–33). A tabela [CATEGORIES] terá, portanto, 2.500 linhas, e a tabela [PRODUCTS] terá 5.000 linhas. Podíamos ter incluído mais linhas, mas o teste já demora quase um minuto a ser executado. Por isso, escolhemos valores que sejam aceitáveis para o utilizador que aguarda a conclusão do teste.

Existem 18 testes no total. São executados utilizando a configuração de execução [1]. Os tempos de execução são apresentados em [2]:

4.11.3.2. doNothing [0,114]

O método [doNothing] não faz nada. É utilizado para medir a duração do método [clean], que é executado antes de cada teste e limpa a base de dados. Acima, podemos ver que a duração desta operação é insignificante em comparação com as outras.


    @Test
    public void doNothing() {
        // clean
}

4.11.3.3. perf01 [4.179]

O teste [perf01] é utilizado para medir o tempo de preenchimento da base de dados:


    @Test
    public void perf01() {
        // insert
        fill(NB_CATEGORIES, NB_PRODUITS);
}

4.11.3.4. perf02 [7,624]

O método [perf02]:

  • preenche a base de dados;
  • depois modifica o nome de todas as categorias e o preço de todos os produtos.

    @Test
    public void perf02() {
        // update
        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);
            }
        }
        // mise à jour
        daoCategorie.saveEntities(categories);
}

4.11.3.5. perf03[3,911]

O método [perf03]:

  • preenche a base de dados
  • e, em seguida, elimina todas as categorias uma a uma. Os produtos também são eliminados devido à relação em cascata entre a tabela [CATEGORIES] e a tabela [PRODUCTS].

Pode ser surpreendente que esta operação demore menos tempo [3,911 s] do que a operação [perf01] [4,179 s], que faz menos.


    @Test
    public void perf03() {
        // delete categories and cascade products
        daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}

Se analisarmos o código do método [daoCategorie.deleteEntitiesByEntity], vemos que será executado um [PreparedStatement] com 2.500 parâmetros (o número de categorias). É aqui que o bean [maxPreparedStatementParameters] entra em ação; ele dividirá a instrução SQL em vários objetos [PreparedStatement], cada um com um número de parâmetros que o DBMS específico consegue processar.

4.11.3.6. perf04[2.426]

O método [perf04]:

  • preenche a base de dados;
  • depois recupera os detalhes completos de todas as categorias;

    @Test
    public void perf04() {
        // select
        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 a base de dados;
  • depois elimina os 5.000 produtos utilizando as suas chaves primárias (pelo que temos potencialmente um [PreparedStatement] com 5.000 parâmetros);
  • verifica se a tabela de produtos está agora vazia;

    @Test
    public void perf05() {
        // delete products
        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);
        // check
        List<Produit> produits = daoProduit.getAllShortEntities();
        Assert.assertEquals(0, produits.size());
}

4.11.3.8. Resultados

Não continuaremos a apresentar os vários testes. Limitar-nos-emos a indicar o que fazem e a sua duração. Estas durações só fazem sentido quando comparadas entre si. Os seus valores dependem do ambiente de teste utilizado (configuração de hardware e software). No entanto, quando obtidos no mesmo ambiente, podem ser comparados.

Duração total do teste: 59,995 segundos

teste
função
Duração (s)
perf01
preenche a base de dados com 2.500 categorias e 5.000 produtos
4,179
perf02
Preenche e, em seguida, modifica a base de dados
7.624
perf03
preenche a base de dados e, em seguida, elimina todas as categorias e os seus produtos
3.911
perf04
preenche a base de dados e solicita a versão longa de todas as categorias
2.426
perf05
preenche a base de dados e elimina os 5.000 produtos um a um utilizando as suas chaves primárias
3.507
perf06
preenche a base de dados e elimina os 5.000 produtos um a um utilizando os seus nomes
3.947
perf07
preenche a base de dados e elimina os 5.000 produtos um a um utilizando os seus SKUs
3.633
perf08
preenche a base de dados e recupera a versão resumida de todos os produtos pelos seus nomes
4.054
perf09
preenche a base de dados e recupera a versão completa de todos os produtos por nome
2.643
perf10
preenche a base de dados e recupera a versão curta de todos os produtos utilizando as suas chaves primárias
3.463
perf11
preenche a base de dados e recupera a versão completa de todos os produtos utilizando as suas chaves primárias
2.777
perf12
preenche a base de dados e, em seguida, elimina todas as categorias (e, consequentemente, os produtos associados) uma a uma através dos seus nomes
3.806
perf13
preenche a base de dados e, em seguida, elimina todas as categorias (e os produtos associados) uma a uma utilizando os seus SKUs
2.828
perf14
preenche a base de dados e recupera a versão resumida de todas as categorias através dos seus nomes
2.731
perf15
preenche a base de dados e solicita a versão longa de todas as categorias por nome
2.603
perf16
preenche a base de dados e recupera a versão curta de todas as categorias utilizando as suas chaves primárias
2.462
perf17
preenche a base de dados e recupera a versão longa de todas as categorias através das suas chaves primárias
3.287

Estes resultados são, por vezes, surpreendentes:

  • foi mais rápido recuperar a versão longa dos produtos (perf09) do que a versão curta (perf08), apesar de a versão longa envolver uma junção entre duas tabelas;
  • a duração do primeiro preenchimento (perf01) excede significativamente a de todos os preenchimentos subsequentes;
  • a recuperação da versão curta dos produtos através dos seus nomes (perf08) demora mais tempo do que a recuperação através das chaves primárias (perf10). Isto parece bastante lógico. Mas, no caso das versões longas, verifica-se o contrário (perf09, perf11);

Não nos deteremos, portanto, nestes resultados. No entanto, serão úteis para comparar esta solução [Spring JDBC] com a:

  • [Spring JDBC] para os outros cinco SGBDs;
  • [Spring JPA] a seguir;