4. Introduzione a Spring JDBC
In questo capitolo esamineremo la seguente architettura:
![]() |
Si tratta della stessa architettura di prima. Introdurremo due modifiche:
- il database avrà due tabelle collegate da una relazione di chiave esterna;
- il livello [DAO] verrà implementato utilizzando la libreria [Spring JDBC], che semplifica la gestione dell'API JDBC;
4.1. Configurazione dell'ambiente di sviluppo
Utilizzando STS, importare il progetto [spring-jdbc-04] situato nella cartella [<examples>/spring-database-generic/spring-jdbc]
![]() |
Inoltre, è necessario creare un nuovo database MySQL utilizzando il client [MyManager] (vedere la sezione 3.1):
![]() |
- In [3], gli esempi seguenti utilizzano un database MySQL denominato [dbproduitscategories];
![]() |
- In [9], inserire la password dell'utente root (in questo documento la password è "root");
![]() |
![]() |
- in [18], il database [dbproduitscategories] è stato creato vuoto. Creiamo le tabelle e le popoliamo con uno script SQL [19-20];
![]() |
- In [21], accedere alla cartella [<examples>/spring-database-config/mysql/databases];
![]() |
- in [25], assicurarsi di trovarsi nel database [dbproduitscategories] e non nel database [dbproduits];
- in [29], lo script SQL ha creato cinque tabelle. Le tabelle [ROLES, USERS, USERS_ROLES] saranno utilizzate solo quando ci occuperemo della sicurezza del servizio web creato per esporre il database [dbproduitscategories] sul web;
4.2. Il database [dbproduitscategories]
Il database [dbproduitscategories] è un'estensione del database [dbproduits] discusso in precedenza. Mentre nella tabella [PRODUITS] il prodotto aveva una categoria identificata da un numero privo di significato particolare, qui quel numero sarà una chiave esterna nella tabella [CATEGORIES].
La tabella [PRODUCTS] è la seguente:
![]() |
- [ID]: la chiave primaria autoincrementale della tabella [PRODUCTS];
- [NAME]: il nome univoco del prodotto [4];
- [PRICE]: il prezzo del prodotto;
- [DESCRIPTION]: la descrizione del prodotto;
- [VERSIONING] è il numero di versione del prodotto. La sua versione iniziale è 1 [3]. Ogni volta che il prodotto viene modificato, il suo numero di versione viene incrementato dal codice che gestisce la tabella;
- [CATEGORY_ID]: la chiave esterna nella tabella [CATEGORIES] per identificare la categoria a cui appartiene il prodotto;
![]() |
- in [1-3], la chiave esterna [CATEGORIE_ID] della tabella [PRODUITS]. Essa fa riferimento alla colonna [ID] della tabella [CATEGORIES] [4-5];
- quando una categoria viene eliminata, vengono eliminati anche tutti i prodotti ad essa collegati [6]. È importante notare questo punto perché viene utilizzato nella costruzione del livello [DAO] che utilizza il database [dbproduitscategories];
La tabella [CATEGORIES] è la seguente:
![]() |
- [ID]: chiave primaria autoincrementale;
- [VERSIONING]: numero di versione della categoria;
- [NAME]: nome univoco della categoria;
4.3. Il progetto Eclipse
![]() |
Il progetto [spring-jdbc-04] implementa la seguente architettura:
![]() |
Il progetto [spring-jdbc-04] è un progetto Maven configurato dal seguente file [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>
- righe 28–32: il progetto si basa sul progetto [mysql-config-jdbc], che configura il livello JDBC;
- righe 34–37: l'artefatto [spring-boot-starter-jdbc] fornisce le librerie JDBC di Spring;
Alla fine, le dipendenze sono le seguenti:
![]() |
4.4. Configurazione di Spring
![]() |
La classe [AppConfig] che configura il progetto Spring è la seguente:
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);
}
}
- riga 16: la classe è una classe di configurazione Spring;
- riga 17: il pacchetto [spring.jdbc.dao] verrà analizzato alla ricerca di componenti Spring diversi da quelli presenti nella classe [AppConfig]. È qui che troveremo il componente che implementa il livello [DAO];
- riga 18: non gestiremo le transazioni autonomamente, ma le lasceremo a Spring JDBC. L'unica cosa da fare sarà annotare i metodi che devono essere eseguiti all'interno di una transazione con l'annotazione Spring [@Transactional]. La riga 18 assicura che questa annotazione venga elaborata e non ignorata. La gestione delle transazioni è gestita da una delle dipendenze del progetto Spring JDBC importate tramite il file [pom.xml];
- riga 19: importiamo i bean già definiti nella classe [generic.jdbc.config.ConfigJdbc] dal progetto [mysql-config-jdbc];
- righe 23–36: l'origine dati [tomcat-jdbc] introdotta nell'esempio [spring-jdbc-02];
- righe 40–42: il gestore delle transazioni associato alla fonte dati precedentemente definita. Il bean deve essere denominato [transactionManager] poiché questo è il nome utilizzato dall'annotazione [@EnableTransactionManagement]. Il [DataSourceTransactionManager] è fornito dalla libreria Spring JDBC (riga 12);
- righe 45–48: il bean [namedParameterJdbcTemplate], su cui si baserà l'implementazione del livello [DAO]. Questo bean è fornito dalla libreria Spring JDBC (riga 10). Questo bean è anche collegato alla fonte dati definita in precedenza (riga 47);
- righe 51–55: il bean [simpleJdbcInsertProduit] (nome arbitrario) verrà utilizzato per inserire un prodotto nella tabella [PRODUITS] e recuperare la chiave primaria generata. I vari parametri utilizzati sono i seguenti:
- [dataSource]: l'origine dati [tomcat-jdbc] delle righe 24–36;
- [ConfigJdbc.TAB_PRODUITS]: la tabella [PRODUITS];
- [ConfigJdbc.TAB_CATEGORIES_ID]: la colonna della chiave primaria della tabella [PRODUCTS]. Si noti che per PostgreSQL il nome di questa colonna deve essere in minuscolo;
- righe 58–62: il bean [simpleJdbcInsertCategorie] verrà utilizzato per inserire una categoria nella tabella [CATEGORIES] e recuperare la chiave primaria generata;
4.5. Eccezioni del progetto
![]() |
Abbiamo già visto le classi [UncheckedException, DaoException, ShortException] nel progetto [spring-jdbc-03]. Ne aggiungiamo una nuova:
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);
}
}
- La classe [MyIllegalArgumentException] deriva dalla classe [UncheckedException] ed è quindi una classe non controllata. Verrà utilizzata per segnalare una chiamata con argomenti errati a un metodo nel livello [DAO]. Non l'abbiamo chiamata [IllegalArgumentException] perché questa eccezione esiste già nel JDK e questo a volte causava la generazione di un [import] errato da parte del compilatore;
4.6. Entità del progetto
![]() |
Le classi nel pacchetto [spring.jdbc.entities] rappresentano le righe nelle tabelle del database [dbproduitscategories]. Per ora, ignoreremo le tabelle [USERS, ROLES, USERS_ROLE].
Tutte le entità estendono la classe padre [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
...
}
- riga 5: il campo [id] sarà associato alla colonna [ID], la chiave primaria delle tabelle;
- riga 6: il campo [version] sarà associato alla colonna [VERSIONING] delle tabelle;
- righe 8–26: vari costruttori e metodi per la creazione o l'inizializzazione di un oggetto [AbstractCoreEntity];
- righe 35–47: il metodo [equals] stabilisce che due oggetti [AbstractCoreEntity] sono uguali se hanno lo stesso campo [id]. È importante ricordare qui che gli oggetti [AbstractCoreEntity] saranno rappresentazioni di righe di tabella in cui [id] è la chiave primaria e dove, quindi, non possono esserci due righe con lo stesso [id];
- righe 30–33: una proposta per [hashCode];
La classe [Product] rappresenterà una riga nella tabella [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
...
}
- riga 6: la classe [Product] estende la classe [AbstractCoreEntity];
- righe 8–12: i campi [id, version, name, categoryId, price, description] corrispondono alle colonne [ID, VERSIONING, NAME, CATEGORY_ID, PRICE, DESCRIPTION] nella tabella [PRODUCTS];
- riga 12: l'oggetto di tipo [Category] con chiave primaria [categoryId]. Questo campo può essere popolato o meno, a seconda dei casi. Quando è popolato, ci riferiamo a un prodotto in forma estesa [LongProduct]; altrimenti, a un prodotto in forma abbreviata [ShortProduct];
- riga 5: un filtro JSON. Si noti che il progetto [mysql-config-jdbc] include una libreria JSON. Il filtro è necessario perché il campo [category] può essere compilato o meno. In questo caso, la rappresentazione JSON del prodotto differisce. Per gestire questi due casi, configureremo il filtro [jsonFilterProduct] alla riga 5. Un filtro JSON ci permette di specificare dinamicamente quali campi escludere dalla rappresentazione JSON. Quando sappiamo che il campo [category] non è stato compilato, lo escluderemo dalla rappresentazione JSON del prodotto;
La classe [Category] rappresenta una riga nella tabella [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
...
}
- riga 9: la classe [Category] estende la classe [AbstractCoreEntity];
- riga 12: i campi [id, version, name] corrispondono alle colonne [ID, VERSIONING, NAME] nella tabella [CATEGORIES];
- riga 13: il campo [products] rappresenta l'elenco dei prodotti nella categoria. Questo campo non è sempre popolato. Quando non lo è, ci riferiamo a una categoria abbreviata [ShortCategorie]; altrimenti, a una categoria estesa [LongCategorie];
- righe 32–44: il metodo [addProduct] consente di aggiungere un prodotto alla categoria (riga 39) e di impostare le caratteristiche della categoria (categoryID e category) nel prodotto aggiunto;
- riga 8: un filtro JSON. Quando la libreria JSON deve serializzare/deserializzare un oggetto [Category], dobbiamo indicarle come gestire il filtro denominato [jsonFilterCategory];
4.7. L'interfaccia Idao<T>
![]() |
![]() |
L'interfaccia [IDao] del livello [DAO] ha la seguente firma:
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);
}
- Riga 7: Qui abbiamo un'interfaccia [IDao] parametrizzata da un tipo T con una condizione: questo tipo deve estendere la classe [AbstractCoreEntity] o implementare l'interfaccia [AbstractCoreEntity]. La parola chiave [extends] viene utilizzata in entrambi i casi. Qui, T verrà istanziato dal tipo [Product] o dal tipo [Category]. Infatti, diventa subito evidente che eseguiamo gli stessi tipi di operazioni (inserimento, modifica, eliminazione, selezione) sui tipi [Product] e [Category]. Ha quindi senso raggruppare questi metodi in un'interfaccia generica;
- a seconda del contesto, i termini [LongEntity] e [ShortEntity] si riferiscono a situazioni diverse:
- quando T è il tipo [Product]:
- [ShortEntity] è il prodotto senza il campo [Category] compilato;
- [LongEntity] è il prodotto con il campo [Category] compilato;
- quando T è il tipo [Categoria]:
- [ShortEntity] è la categoria senza il campo [List<Product> products] compilato;
- [LongEntity] è il prodotto con il campo [List<Product> products] compilato;
- quando T è il tipo [Product]:
Abbiamo quindi un'interfaccia con 19 metodi. La maggior parte dei metodi sono duplicati. Prendiamo l'esempio del metodo [getShortEntitiesById]:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- Righe 1 e 3: il parametro è l'elenco delle chiavi primarie delle entità di cui vogliamo la versione abbreviata. Questo elenco è presentato in due forme diverse:
- riga 1: un elenco che implementa l'interfaccia [Iterable<Long>]. Il tipo [List<Long>] implementa questa interfaccia, ma ce ne sono molti altri. Se avessimo scritto [List<Long> ids], sarebbe stato sufficiente per i nostri esempi, ma avrebbe costretto l'utente dei nostri esempi a eseguire conversioni se il loro parametro non fosse stato esattamente del tipo previsto;
- riga 3: sfortunatamente, il tipo `Long[]` non implementa l'interfaccia `Iterable<Long>`. In questo caso, useremo la versione della riga 3. Il parametro formale [Long... ids] (3 punti) può accettare valori sia da un array che da una sequenza di ID: getShortEntitiesById(id1, id2, ...);
Questa stessa interfaccia IDao<T> sarà implementata dalla seguente architettura:
![]() |
dove verrà inserito un livello [JPA] (Java Persistence API) tra il livello [DAO] e il driver JDBC del DBMS. Questo ci consentirà di avere un livello di test comune per entrambe le architetture. In entrambi i casi, il livello [DAO] presenterà due interfacce:
- IDao<Product> per accedere alla tabella [PRODUCTS];
- IDao<Category> per accedere alla tabella [CATEGORIES];
4.8. Implementazione dell'interfaccia IDao<T>
![]() |
- l'interfaccia IDao<Product> è implementata dalla classe [DaoProduct];
- L'interfaccia IDao<Category> è implementata dalla classe [DaoCategory];
Le classi [DaoProduct] e [DaoCategory] estendono entrambe la seguente classe astratta [ 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);
}
- Riga 15: La classe [AbstractDao] è astratta (parola chiave `abstract`). In quanto tale, non può essere istanziata. Può solo essere derivata. Questa classe ha diversi ruoli:
- definire la natura della transazione in cui viene eseguito ciascun metodo;
- gestire il maggior numero possibile di operazioni comuni per entrambe le implementazioni delle interfacce [IDao<Product>] e [IDao<Category>]. Ciò comporta principalmente la convalida degli argomenti. Non sono ammessi argomenti nulli né elenchi vuoti;
- Unificare i tipi dei parametri `T... params` e `Iterable<T> params` in un unico tipo: `List<T> params`;
- delegare il lavoro alle classi figlie non appena diventa specifico per una delle due interfacce;
Grazie alla standardizzazione dei parametri dei vari metodi effettuata dalla classe [AbstractDao], le classi figlie [DaoProduit] e [DaoCategorie] avranno solo 10 metodi da implementare invece di 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();
Diamo un'occhiata ad alcuni metodi della classe [AbstractDao].
Metodo [getShortEntitiesById]
Questo metodo recupera la versione abbreviata delle entità per le quali sono fornite le chiavi primarie.
// injections
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// local
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- Righe 2–4: Inseriamo il bean [maxPreparedStatementParameters] definito nel file di configurazione [ConfigJdbc], che configura il livello JDBC per un DBMS specifico:
// 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;
}
- Righe 1–7: definiscono il bean [maxPreparedStatementParameters], che imposta il numero massimo di parametri che possono essere passati a un [PreparedStatement]. Questo requisito non si presentava con il DBMS MySQL, che accettava 10.000 parametri per un [PreparedStatement]. Durante i test con il DBMS SQL Server, è stata generata un'eccezione che indicava che il numero massimo di parametri per un [PreparedStatement] era 2.100. Pertanto, questo numero è diventato un parametro di configurazione per i vari DBMS. Deve quindi essere inserito nel progetto di configurazione [sgbd-config-jdbc] per ciascun DBMS;
Torniamo al codice del metodo [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) {
...
}
- riga 7: il nome della classe. Utilizzato come parametro per uno dei costruttori della classe di eccezione [DaoException];
- riga 10: l'annotazione [@Transactional(readOnly = true)] indica che il metodo deve essere eseguito all'interno di una transazione in sola lettura. Ci si potrebbe chiedere quale sia l'utilità di una transazione di questo tipo, dato che il metodo esegue solo operazioni di lettura e quindi, in caso di errore, non c'è nulla da annullare. L'autore della libreria [Spring Data] lo raccomanda e ne spiega il motivo. Ho seguito il suo consiglio;
Il corpo del metodo è il seguente:
@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;
}
...
}
- Riga 5: La validità del parametro [ids] viene verificata con il seguente metodo:
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;
}
- riga 1: il metodo [checkNullOrEmptyArgument] è un metodo generico parametrizzato dal tipo <T2>. T2 è il tipo degli elementi passati come secondo parametro al metodo. Può essere [Long, String, AbstractCoreEntity];
- riga 1: il metodo [checkNullOrEmptyArgument] accetta due parametri:
- [Iterable<T2> elements]: il parametro da testare;
- [checkEmpty]: impostato su true se occorre verificare che il parametro precedente sia una lista non vuota;
- righe 4–6: verifichiamo che il parametro [elements] non sia nullo. Se lo è, viene generata un'eccezione [MyIllegalArgumentException];
- righe 8–15: se la lista è vuota e dovevamo verificare che non fosse vuota, generiamo un'eccezione [MyIllegalArgumentException];
- riga 13: se la lista è vuota e non dovevamo verificare che non fosse vuota, allora restituiamo una lista vuota di elementi di tipo T. L'interfaccia [Iterable<T2>] ha un metodo [iterator()] che permette a un iterator di iterare sugli elementi della lista che implementa l'interfaccia. Due metodi di quest o sono utili:
- [iterator].hasNext(): restituisce true se la lista ha ancora un elemento da elaborare, false in caso contrario;
- [iterator].next(): restituisce l'elemento corrente della lista e fa avanzare l'iteratore di un elemento;
- Infine,
- se l'argomento [T2... elementi] è nullo o vuoto, viene generata un'eccezione [MyIllegalArgumentException];
- se l'argomento [T2... elements] è una lista vuota e ciò era legale, viene restituita una lista vuota di elementi di tipo T;
Esiste un metodo simile quando l'argomento da testare è di tipo [T2... elements]:
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
Torniamo al codice del metodo [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;
}
- riga 7: se arriviamo a questo punto, significa che l'argomento [Iterable<Long> ids] è valido;
- righe 7–14: vedremo in seguito che il metodo [getShortEntitiesById] sarà implementato da un tipo [PreparedStatement] che accetterà come parametri l'elenco delle chiavi primarie da cercare. Ad esempio:
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 è un parametro il cui valore effettivo sarà di tipo List<Long>. Ogni elemento di questo elenco verrà passato come parametro ? in un [PreparedStatement]. Tuttavia, abbiamo specificato che questo tipo accetta un numero massimo di parametri, un numero impostato dal campo [maxPreparedStatementParameters] della classe;
- riga 7: l'elenco delle entità T che verrà restituito dal metodo [getShortEntitiesById]. Questo elenco verrà costruito in blocchi di [maxPreparedStatementParameters] elementi;
- Riga 9: dall'argomento [Iterable<Long> ids], creiamo un tipo [List<Long> listIds]. La classe [Lists] è una classe della libreria Google Guava che offre numerosi metodi statici per la manipolazione di collezioni di oggetti. La libreria Google Guava è stata importata (pom.xml) dal progetto Maven [mysql-config-jdbc]:
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- riga 10: il numero di entità T da cercare nel database;
- righe 11–13: vengono cercate in gruppi di [size = maxPreparedStatementParameters] elementi;
- riga 12: un calcolo per evitare di andare oltre la fine della lista [listIds];
- riga 13: le entità T vengono ottenute chiamando [getShortEntitiesById(listIds.subList(i, limit))]. Questo metodo è definito nella classe come:
abstract protected List<T> getShortEntitiesById(List<Long> ids);
È quindi la classe figlia che recupererà le entità T dal database:
- [DaoProduct] se T è di tipo [Product];
- [DaoCategory] se T è di tipo [Category];
Il vantaggio di questo approccio nella classe padre è duplice:
- la firma del metodo [getShortEntitiesById] nella classe figlia è unica: il suo argomento è di tipo [List<Long> ids];
- la classe figlia non deve occuparsi della questione dei parametri [maxPreparedStatementParameters] di un [PreparedStatement]. La sua classe padre se ne è occupata per lei;
- riga 13: le entità restituite dalla classe figlia vengono aggiunte all'elenco delle entità che saranno restituite dalla classe padre (riga 16);
Ora, diamo un'occhiata all'implementazione dell'altro metodo della 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));
}
- riga 3: il tipo dell'argomento è cambiato: Long... ids;
- riga 5: viene verificata la validità di questo argomento;
- riga 7: chiamiamo il metodo [getShortEntitiesById] appena descritto. Anche in questo caso, utilizziamo la classe [Lists] della libreria [Google Guava]. Si noti che dobbiamo eseguire un cast esplicito al tipo [Iterable<Long>] per aiutare il compilatore a scegliere il metodo corretto, poiché il metodo [getShortEntitiesById] ha tre firme nella classe:
- List<T> getShortEntitiesById(Long... ids);
- List<T> getShortEntitiesById(Iterable<Long> ids);
- List<T> getShortEntitiesById(List<Long> ids), che è astratto e implementato dalla classe figlia;
Non ci soffermeremo ulteriormente sulla classe astratta [AbstractDao], la classe padre delle classi [DaoProduit] e [DaoCategorie]. Ci limiteremo a osservare che a volte è utile raggruppare i comportamenti comuni a diverse classi in una classe padre, astratta o meno. Dopo questo lavoro, alle classi figlie resta solo da implementare i seguenti metodi:
// 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();
Il codice nella Sezione 4.8 mostra i diversi tipi di transazioni utilizzati per ciascun metodo. Si notino i seguenti punti:
- i metodi che leggono il database sono annotati con [@Transactional(readOnly = true)];
- i metodi che modificano il database sono annotati con [@Transactional];
- i metodi [delete] non sono annotati e quindi non vengono eseguiti all'interno di una transazione. L'idea è che, se un'eliminazione fallisce, l'utente probabilmente non desidera annullare tutte quelle riuscite avvenute in precedenza;
4.9. La classe [DaoCategorie]
![]() |
![]() |
La classe [DaoCategorie] implementa l'interfaccia [IDao<Categorie>], che consente di accedere ai dati contenuti nella tabella [CATEGORIES] del database MySQL [dbproduitscategories]. La sua struttura di base è la seguente:
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> {
....
}
- riga 28: la classe [DaoCategorie] è un componente Spring e, in quanto tale, può essere iniettata in altri componenti Spring;
- riga 29: la classe [DaoCategorie] estende la classe astratta [AbstractDao<Categorie>], rendendola un'implementazione dell'interfaccia [IDao<Categorie>];
- righe 34–37: iniezione di bean definiti nella classe [AppConfig] descritta nella sezione 4.4;
- righe 38–39: iniezione di un riferimento alla classe [DaoProduit], che implementa l'interfaccia [IDao<Produit>] che gestisce l'accesso ai dati nella tabella [PRODUITS];
- righe 41–89: implementazione dell'interfaccia [IDao<Category>];
- righe 95–101: due classi interne che implementano l'interfaccia [RowMapper<T>];
Esaminiamo i metodi uno per uno.
4.9.1. Il metodo [getAllShortEntities]
Il metodo [getAllShortEntities] restituisce tutte le categorie della tabella [CATEGORIES] nella loro forma abbreviata:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Tutti i metodi si basano sull'oggetto [namedParameterJdbcTemplate] definito nel file di configurazione Spring e fornito dalla libreria Spring JDBC. Esso dispone di numerosi metodi. Quello utilizzato sopra è il seguente:
![]()
- [sql] è l'istruzione SQL da eseguire;
- [rowMapper] è un'istanza della seguente interfaccia [RowMapper<T>]:

L'idea è la seguente:
- il metodo [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) esegue l'istruzione SQL [Select]. Gestisce eventuali eccezioni, oltre ad aprire e chiudere la connessione al DBMS. L'unica cosa che non può fare è incapsulare gli elementi del [ResultSet] — gli oggetti che ottiene — in un tipo [Category], poiché non conosce la mappatura tra i campi del tipo [Category] e le colonne del [ResultSet]. Vedremo in seguito che questa mappatura viene creata utilizzando la tecnologia JPA, che incapsulerà automaticamente gli elementi di un [ResultSet] in istanze di tipo T. Per ora, il secondo parametro del metodo [query] è un'istanza dell'interfaccia [RowMapper<T>] in grado di eseguire questa incapsulazione;
Torniamo al codice:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
L'istruzione SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] è la seguente:
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";
La query recupera le colonne [ID, VERSIONING, NOM] dalla tabella [CATEGORIES]. Useremo sempre la seguente sintassi:
SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...
Ciò che conta è la denominazione delle colonne restituite dall'istruzione SELECT utilizzando l'attributo [as column_name]. Questo è l'unico modo per garantire la portabilità tra i DBMS, poiché ognuno di essi ha un proprio metodo proprietario per denominare le colonne restituite da un'istruzione SELECT in cui colonne di tabelle diverse hanno lo stesso nome (ad esempio, ID, NAME o VERSIONING nel nostro caso). Risolviamo questa ambiguità specificando i nomi che queste colonne dovrebbero avere.
La classe interna [ShortCategorieMapper] è la seguente:
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);
}
}
- riga 1: la classe [ShortCategorieMapper] implementa l'interfaccia [RowMapper<Categorie>] e, in quanto tale, deve implementare il metodo [mapRow] nelle righe 4–5, il cui ruolo è quello di incapsulare una riga dal [ResultSet rs] prodotto dall'istruzione [SELECT] in un tipo [Categorie];
- riga 5: viene eseguita questa incapsulazione. Si noti che il nome utilizzato dai metodi [rs.getType(name)] è il nome utilizzato negli attributi [as name] delle colonne SELECT;
Abbiamo così ottenuto l'elenco delle categorie nella loro forma abbreviata senza gestire eccezioni o gestire la connessione. Questo è il vantaggio della libreria Spring JDBC, che gestisce tutto ciò che può essere astratto nella gestione degli elementi della tabella e lascia allo sviluppatore il compito di gestire ciò che non può esserlo.
4.9.2. Il metodo [getAllLongEntities]
Il metodo [getAllLongEntities] restituisce tutte le categorie dalla tabella [CATEGORIES] nella loro forma estesa:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
L'istruzione SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] è la seguente:
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";
L'obiettivo è recuperare le categorie insieme ai prodotti ad esse associati. Ciò si ottiene unendo la tabella [CATEGORIES] con la tabella [PRODUCTS] utilizzando la chiave esterna [CATEGORY_ID] dalla tabella [PRODUCTS] alla tabella [CATEGORIES]. La sintassi [FROM PRODUCTS p RIGHT JOIN CATEGORIES c ON p.CATEGORY_ID=c.ID] recupera anche le categorie che non hanno prodotti associati. In questo caso, la query SELECT restituisce una categoria e un prodotto con tutte le colonne impostate su NULL.
La classe [LongCategorieMapper] è la seguente:
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;
}
}
- riga 4: il metodo [mapRow] deve restituire un oggetto [Category] con il campo [products] popolato, in base a una riga del [ResultSet] restituito dalla precedente istruzione SELECT;
In definitiva, l'operazione:
[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]
restituirà un elenco del tipo:
dove ogni categoria [ci] avrà un campo [products] che è un elenco di prodotti contenente un singolo elemento [productsij]. Ora, abbiamo bisogno del seguente elenco:
dove ogni categoria [ci] avrà un campo [products] che contiene l'elenco dei prodotti [producti1, producti2, ...]. Ciò si ottiene passando l'elenco delle categorie a un metodo privato [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);
}
}
Il metodo [filterCategories] è il seguente:
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;
}
- riga 1: [List<Category> categories] è l'elenco delle categorie da filtrare (o raggruppare);
- riga 6: l'elenco delle categorie da restituire al chiamante;
- righe 8–21: viene elaborata ogni categoria nell'elenco da filtrare;
- righe 10–16: si verifica se la categoria corrente [category] è già presente nell'elenco delle categorie [cats] da costruire (si noti che due categorie sono considerate uguali se hanno la stessa chiave primaria, vedi sezione 4.6);
- righe 11–14: se questo è già il caso, allora il prodotto incapsulato in [categorie] viene aggiunto all'elenco dei prodotti in [cat];
- righe 18-20: se la categoria corrente [categorie] non è già presente nell'elenco delle categorie [cats] da costruire, allora viene aggiunta ad esso insieme al suo elenco di prodotti, che contiene un singolo elemento;
Consideriamo il caso in cui l'istruzione SQL SELECT restituisca categorie senza prodotti associati. Quale entità restituisce la classe [LongCategorieMapper]?
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// 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 l'istruzione SQL SELECT restituisce una categoria senza prodotti, le colonne del prodotto restituite con la categoria contengono tutte il valore SQL NULL. Questo caso viene gestito nelle righe 7–9:
- riga 7: recupera la chiave primaria del prodotto come numero intero lungo;
- riga 9: si verifica se il valore letto era SQL NULL (rs.wasNull). In caso contrario, si aggiunge il prodotto all'elenco alla riga 6; altrimenti, non viene aggiunto nulla e l'elenco dei prodotti rimane vuoto.
Si noti che in tutti i casi restituiamo una categoria con un campo [products] che non è nullo.
4.9.3. Il metodo [getShortEntitiesById]
Il metodo [getShortEntitiesById] è simile al metodo [getAllShortEntities], tranne per il fatto che restituisce solo le entità le cui chiavi primarie sono specificate in un elenco:
@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);
}
}
- Riga 4: La firma del metodo [query] utilizzato è la seguente:

Il primo parametro è un'istruzione SQL [Select] parametrizzata. Il secondo è un dizionario che associa ciascun parametro a un valore. Il terzo è l'istanza della classe che incapsula una riga del [ResultSet] risultante dal [Select] in un oggetto di tipo T;
- Riga 4: L'istruzione SQL [Select] parametrizzata è la seguente:
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)";
Questa query recupera dalla tabella [CATEGORIES] le categorie le cui chiavi primarie sono presenti nell'elenco :ids.
- Riga 5: Il secondo parametro del metodo [query] qui è un dizionario che associa la chiave 'ids' (primo parametro) all'elenco [ids] passato alla riga 1 come parametro al metodo [getShortEntitiesById]. La classe [Collections] appartiene alla libreria [Google Guava], di cui abbiamo già parlato. [Collections.singleMap] restituisce un dizionario con un singolo elemento;
- Riga 5: La classe responsabile dell'incapsulamento di una riga del [ResultSet] risultante dal [Select] in un oggetto di tipo [Category] è la classe [ShortCategoryMapper] che abbiamo già esaminato;
È tipicamente qui che entra in gioco il bean [maxPreparedStatementParameters]. Infatti, il parametro [:ids] dell'istruzione SQL, che rappresenta un elenco di chiavi primarie, può contenere da 1 a diverse migliaia di parametri. Esiste un limite a questo numero che dipende da ciascun DBMS. Per MySQL, siamo riusciti a passare 10.000 parametri senza errori e non abbiamo testato oltre tale limite. Per SQL Server, il limite ufficiale è 2.100. Per Firebird, 1.000 erano già troppi. Li abbiamo ridotti a 100. In generale, non abbiamo testato il limite massimo di questo numero per i vari DBMS.
4.9.4. Il metodo [getLongEntitiesById]
Il metodo [getLongEntitiesById] è analogo al metodo [getShortEntitiesById], tranne per il fatto che restituisce le versioni lunghe delle categorie:
@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);
}
}
Riga 4, la query SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] è la seguente:
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. Il metodo [getShortEntitiesByName]
Il metodo [getShortEntitiesByName] è simile al metodo [getShortEntitiesById], tranne per il fatto che le categorie vengono recuperate in base ai loro nomi anziché alle loro chiavi primarie:
@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);
}
}
Riga 4, l'istruzione SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] è la seguente:
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. Il metodo [getLongEntitiesByName]
Il metodo [getLongEntitiesByName] è simile al metodo [getShortEntitiesByName], con la differenza che le categorie vengono recuperate nella loro versione completa:
@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);
}
}
Riga 4, l'istruzione SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] è la seguente:
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. Il metodo [deleteAllEntities]
Il metodo [deleteAllEntities] elimina tutte le categorie dalla tabella [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);
}
}
- Riga 4: Il metodo [namedParameterJdbcTemplate.update] utilizzato presenta la seguente firma:
![]()
Il primo parametro è un'istruzione SQL di aggiornamento parametrizzata (INSERT, UPDATE, DELETE). Il secondo parametro è il dizionario che associa i valori ai vari parametri dell'istruzione SQL. Il metodo restituisce il numero di righe aggiornate dall'istruzione SQL.
- Riga 4: L'istruzione SQL [ConfigJdbc.DELETE_ALLCATEGORIES] è la seguente:
public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";
Non si tratta quindi di una query parametrizzata. Ecco perché il secondo parametro del metodo [update] ha il valore null.
4.9.8. Il metodo [deleteAllEntitiesById]
Il metodo [deleteAllEntitiesById] elimina dalla tabella [CATEGORIES] le categorie per le quali vengono passati i chiavi primarie:
@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);
}
}
Riga 4, l'istruzione SQL [ConfigJdbc.DELETE_CATEGORIESBYID] è la seguente:
public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";
4.9.9. Il metodo [deleteAllEntitiesByName]
Il metodo [deleteAllEntitiesByName] elimina dalla tabella [CATEGORIES] le categorie i cui nomi sono stati passati:
@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);
}
}
Riga 4, l'istruzione SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] è la seguente:
public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";
4.9.10. Il metodo [saveEntities]
4.9.10.1. Il codice
La firma di questo metodo è la seguente:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
Il metodo accetta come parametro un elenco di categorie. Su di esse esegue le seguenti operazioni:
- se la categoria ha una chiave primaria nulla, viene eseguita un'operazione SQL INSERT; altrimenti, viene eseguita un'operazione SQL UPDATE;
- questa operazione viene ripetuta per ogni prodotto nella categoria;
Il metodo restituisce l'elenco delle categorie salvate o aggiornate. L'elenco restituito è una rappresentazione esatta delle categorie e dei prodotti presenti nelle tabelle, a parte i numeri di versione: questi non vengono effettivamente modificati nelle entità aggiornate, anche se sono stati incrementati nel database.
Questo è di gran lunga il metodo più complesso. Il suo codice è il seguente:
@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);
}
}
- righe 5–23: inserimento o aggiornamento delle categorie;
- righe 26–43: inserimento o aggiornamento dei prodotti;
- righe 35-39: questo codice collega ogni prodotto alla sua categoria. Nella fase precedente di inserimento delle categorie, a queste è stata assegnata una chiave primaria che deve essere inserita nel campo [idCategorie] del prodotto (riga 37). Inoltre, le righe 37-38 consentono di correggere situazioni in cui il chiamante non ha collegato correttamente ciascun prodotto alla sua categoria. Per garantire che questa relazione sia corretta, deve essere utilizzato il metodo [Category].add(Product p), ma nulla impedisce a un utente di aggiungere un prodotto direttamente all’elenco dei prodotti della categoria senza utilizzare questo metodo, con il rischio che i campi [idCategory, category] del prodotto p vengano popolati in modo errato;
- Riga 43: Deleghiamo il compito di salvare/aggiornare i prodotti all'istanza dell'interfaccia [IDao<Product>]. Ricordiamo che questa istanza è stata iniettata nella classe [DaoCategory]:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. Inserimento delle categorie
Le categorie vengono inserite nella tabella [CATEGORIES] utilizzando il seguente metodo privato [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;
}
- Riga 6: Utilizziamo il bean [simpleJdbcInsertCategorie] iniettato nella classe dalle seguenti righe:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
Questo bean è definito nella classe [AppConfig] del progetto come segue:
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);
}
- Riga 5: La classe [SimpleJdbcInsert] è una classe della libreria Spring JDBC (riga 1):
- il parametro del costruttore [SimpleJdbcInsert] è l'origine dati su cui viene eseguita l'operazione;
- la clausola [withTableName] specifica la tabella in cui deve essere inserito un elemento, in questo caso la tabella [CATEGORIES];
- la clausola [usingGeneratedKeyColumns] specifica la colonna della chiave primaria generata automaticamente, in questo caso la colonna [ID];
- la clausola [usingColumns] limita l'inserimento a determinate colonne. In questo caso, escludiamo la colonna [ID], generata automaticamente dal DBMS, e la colonna [VERSIONING], che ha un valore predefinito pari a 1;
Torniamo al codice del metodo [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;
}
- Riga 6: Viene utilizzato il metodo [simpleJdbcInsertCategorie.executeAndReturnKey]:
![]()
Il metodo richiede come parametro un dizionario che mappa le colonne della tabella ai valori da inserire in esse. Restituisce la chiave primaria come tipo [Number]. Il metodo [Number.longValue()] viene utilizzato per ottenere la chiave primaria come tipo [Long].
Il metodo [getMapForCategorie] è il seguente metodo privato:
private Map<String, ?> getMapForCategorie(Categorie categorie) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
return map;
}
Le chiavi del dizionario sono i nomi delle colonne da popolare [NAME], mentre i valori del dizionario sono i valori da inserire in tali colonne.
- riga 8 [insertCategories]: la chiave primaria recuperata viene memorizzata in un dizionario. Aspetteremo di essere sicuri che tutte le entità siano state inserite prima di assegnare loro le chiavi primarie. Infatti, in caso di eccezione, tutti gli inserimenti verranno annullati e vogliamo che anche le entità [categories] della riga 1 rimangano invariate;
- righe 14–17: ora che siamo sicuri che tutto sia andato bene, assegniamo le chiavi primarie generate alle categorie;
- riga 19: restituiamo l'elenco delle categorie con le loro chiavi primarie;
4.9.10.3. Aggiornamento delle categorie
Le categorie vengono aggiornate utilizzando il seguente metodo privato [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);
}
}
L'aggiornamento di una categoria C1 nel database con una categoria C2 in memoria è consentito solo se le categorie C1 e C2 hanno la stessa versione. Questo numero di versione viene utilizzato per impedire aggiornamenti simultanei dell'entità da parte di due utenti diversi: due utenti, U1 e U2, leggono l'entità E con un numero di versione pari a V1. U1 modifica E e salva questa modifica nel database: il numero di versione passa quindi a V1+1. A sua volta, U2 modifica E e salva questa modifica nel database: riceverà un'eccezione perché la sua versione (V1) differisce da quella nel database (V1+1).
- Righe 2–29: Il blocco `try` ha due blocchi `catch`:
- il primo, alla riga 25, serve a far passare qualsiasi eccezione [DaoException] generata dal codice alla riga 13;
- il secondo, alla riga 27, serve a gestire altri tipi di eccezione;
- riga 3: esaminiamo tutte le categorie da aggiornare;
- riga 4: aggiorniamo la categoria corrente utilizzando il metodo [namedParameterJdbcTemplate.update]:

- Analizziamo l'istruzione:
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES, new BeanPropertySqlParameterSource(categorie));
L'istruzione SQL [ConfigJdbc.UPDATE_CATEGORIES] è la seguente:
public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";
L'istruzione ha tre parametri (:id, :version, :nom) i cui valori si trovano nei campi con lo stesso nome nell'oggetto [categorie] modificato. Utilizziamo questa funzionalità passando [new BeanPropertySqlParameterSource(categorie)] come secondo parametro, il che specifica che "i valori dei parametri si trovano nei campi con lo stesso nome in questo Java bean";
Il risultato restituito da questa operazione, quando viene eseguita normalmente, è il numero di righe modificate, ovvero 0 o 1.
Torniamo al codice che stiamo esaminando:
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);
}
}
- riga 9: verifica se l'aggiornamento è andato a buon fine;
- riga 10: l'aggiornamento non è andato a buon fine. Poiché la clausola [WHERE] coinvolge le colonne [ID] e [VERSIONING], cerchiamo la colonna che ha causato il fallimento della clausola [WHERE];
- righe 12–18: verifichiamo che la chiave [id] della categoria sia presente nel database. In caso contrario, generiamo una [RuntimeException] con un messaggio di errore appropriato;
- righe 19–22: gestiamo il caso in cui la versione fosse errata;
4.10. La classe [DaoProduit]
![]() |
![]() |
La classe [DaoProduit] implementa l'interfaccia [IDao<Produit>], che fornisce l'accesso ai dati nella tabella [PRODUITS] del database MySQL [dbproduitscategories]. Il suo scheletro è il seguente:
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> {
...
}
Il codice è molto simile a quello della classe [DaoCategory]. Esamineremo solo alcuni metodi.
4.10.1. Il metodo [getShortEntitiesById]
Il metodo [getShortEntitiesById] restituisce la versione abbreviata dei prodotti le cui chiavi primarie sono state passate:
@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);
}
}
- Riga 4: L'istruzione SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] è la seguente:
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)";
- Riga 4: La classe [ShortProductMapper], responsabile dell'incapsulamento del [ResultSet] in un elenco di prodotti, è la seguente:
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. Il metodo [getLongEntitiesByName]
Il metodo [getShortEntitiesById] restituisce la versione lunga dei prodotti i cui nomi sono stati passati:
@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);
}
}
- Riga 4: L'istruzione SQL SELECT [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] è la seguente:
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";
- Riga 4: La classe [LongProductMapper], responsabile dell'incapsulamento degli elementi del [ResultSet] in prodotti (versione lunga), è la seguente:
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. Il metodo [saveEntities]
Il metodo [saveEntities] viene utilizzato indifferentemente per inserire nuovi prodotti (id==null) o aggiornare prodotti esistenti (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);
}
}
Riga 18: I prodotti da inserire vengono aggiunti utilizzando il seguente metodo privato [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;
}
Questo metodo è analogo al metodo [insertCategories] descritto nella Sezione 4.9.10.3.
- Riga 4: Utilizziamo il bean [simpleJdbcInsertProduit] che è stato iniettato nella classe:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
Questo bean è stato definito nella classe [AppConfig] che configura il progetto:
@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);
}
- righe 3-6: il bean [simpleJdbcInsertProduct]
- è collegato al database [dbproduitscategories] (riga 3) e alla tabella [ConfigJdbc.TAB_PRODUITS] in quel database (riga 4);
- la chiave primaria per questa tabella viene generata nella colonna [ConfigJdbc.TAB_PRODUITS_ID] (riga 5);
- i valori vengono assegnati solo alle colonne [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (riga 6);
Il metodo [updateProducts], che aggiorna i prodotti (riga 20 di [saveEntities]), è il seguente:
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);
}
}
È simile a quello che aggiorna le categorie (vedere la sezione 4.9.10.3). Alla riga 23, l'istruzione SQL [ConfigJdbc.UPDATE_PRODUITS] eseguita per aggiornare i prodotti è la seguente:
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";
I nomi dei parametri [:id,:version,:nom,:prix,:idCategorie,:description] corrispondono anche ai nomi dei campi nella classe [Product], il che consente di utilizzare l'istruzione nelle righe 6–7 per aggiornare il prodotto corrente.
4.11. Il livello di test
![]() |
![]() |
Il livello di test è costituito da tre classi di test:
- [JUnitTestCheckArguments]: i test in questa classe chiamano i vari metodi del livello [DAO] con argomenti non validi e verificano che rispondano correttamente;
- [JUnitTestDao]: i test in questa classe chiamano i vari metodi del livello [DAO] e verificano che facciano ciò che ci si aspetta;
- [JUnitTestPushTheLimits] non ha lo scopo di testare il livello [DAO], ma di misurarne le prestazioni;
Questo livello di test svolge un ruolo fondamentale in questo documento. È, infatti, comune a tutte le implementazioni dell'interfaccia [IDao<T>]. Ce ne sono sei per ogni DBMS (1 implementazione JDBC, 3 implementazioni JPA, 1 implementazione Spring MVC, 1 implementazione Spring MVC sicura), quindi 36 per i sei DBMS testati. Il livello di test ci permette di verificare che tutte le implementazioni si comportino allo stesso modo.
4.11.1. Il test [JUnitTestCheckArguments]
La classe di test [JUnitTestCheckArguments] dispone di 48 metodi che verificano come reagiscono i metodi del livello [DAO] quando vengono chiamati con argomenti errati. La sua struttura è la seguente:
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];
...
}
- riga 19: il test JUnit verrà eseguito in integrazione con il framework Spring;
- riga 18: prima dei test, verranno istanziati i bean definiti nella classe [AppConfig] del progetto;
- righe 23–26: iniezione di un'istanza di ciascuna delle due interfacce nel livello [DAO];
- righe 29–44: parametri di chiamata errati per i metodi del livello [DAO];
- riga 29: un puntatore nullo di tipo [Iterable<String>] come elenco di nomi;
- riga 30: un elenco vuoto di tipo [Iterable<String>] come elenco di nomi;
- riga 29: un puntatore nullo di tipo String[] come array di nomi;
- riga 30: un array vuoto di tipo String[] come elenco di nomi;
- ...
Con il campo [names1], eseguiamo ad esempio il seguente test:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- Riga 1: Specifichiamo che il test [getShortProduitsByName1] deve generare un'eccezione [MyIllegalArgumentException]
Con il campo [names2], eseguiamo ad esempio il seguente test:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName2() {
daoCategorie.getLongEntitiesByName(names2);
}
Con il campo [names3], eseguiamo ad esempio il seguente test:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName3() {
daoCategorie.getLongEntitiesByName(names3);
}
Con il campo [names4], eseguiamo ad esempio il seguente test:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName4() {
daoProduit.getShortEntitiesByName(names4);
}
Eseguiamo quindi 48 test per coprire tutti i casi possibili. Eseguiamo la configurazione di test denominata [spring-jdbc-generic-04-JUnitTestCheckArguments] [1]. Il risultato è il seguente [2]:
![]() |
4.11.2. Il test [JUnitTestDao]
Il test [JUnitTestDao] chiama i metodi del livello [DAO] con argomenti validi e verifica che i metodi facciano ciò che ci si aspetta da loro. Ci sono in totale 74 test che verificano le operazioni di inserimento, selezione, aggiornamento ed eliminazione di entità, categorie o prodotti. In totale, ci sono oltre 1.000 righe di codice. Esamineremo solo alcuni di questi metodi.
4.11.2.1. Lo scheletro del test
La classe [JUnitTestDao] presenta la seguente struttura:
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);
}
}
...
}
- righe 27-28: come nel test [JUnitTestCheckArguments], si tratta di un test integrato con Spring e configurato dalla classe [AppConfig] del progetto;
- righe 32-33: iniezione del contesto Spring, che fornisce l'accesso a tutti i suoi bean;
- righe 35-36: iniezione dell'istanza dell'interfaccia [IDao<Product>] testata dalla classe;
- righe 37-38: iniezione dell'istanza dell'interfaccia [IDao<Category>] testata dalla classe;
- righe 41-42: quando un test richiede dati del database, verrà generato un database di [NB_CATEGORIES] categorie, ciascuna contenente [NB_PRODUITS] prodotti. Avremo quindi [NB_CATEGORIES] categorie nella tabella [CATEGORIES] e [NB_CATEGORIES] * [NB_PRODUITS] prodotti nella tabella [PRODUITS];
- righe 46-47: due dizionari in cui memorizzeremo i prodotti e le categorie;
- Righe 49–62: il metodo [clean] viene eseguito prima di ogni test (riga 49). Alla riga 54, la tabella [CATEGORIES] viene svuotata. È importante notare qui che la tabella [PRODUCTS] ha una chiave primaria [CATEGORY_ID] sulla colonna ID della tabella [CATEGORIES], e che questa è definita come segue;
![]() |
- (continua)
- in [1-3], la chiave esterna [CATEGORIE_ID] della tabella [PRODUITS]. Essa fa riferimento alla colonna [ID] della tabella [CATEGORIES] [4-5];
- quando una categoria viene eliminata, vengono eliminati anche tutti i prodotti ad essa collegati [6]. È importante sottolineare questo punto perché viene utilizzato nella costruzione del livello [DAO] che utilizza il database [dbproduitscategories];
Pertanto, quando il contenuto della tabella [CATEGORIES] viene eliminato, verrà eliminato anche quello della tabella [PRODUCTS].
- righe 56-58: svuotiamo il dizionario delle categorie;
- righe 59–61: facciamo lo stesso con il dizionario dei prodotti;
Si noti che prima di ogni test, si parte con tabelle vuote nel database e dizionari vuoti in memoria.
4.11.2.2. Il metodo [verifyClean]
Il metodo [verifyClean] verifica che, dopo l'esecuzione del metodo [clean], le tabelle siano vuote:
@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. Il metodo [fillDataBase]
Questo metodo verifica che il database sia stato correttamente popolato con i dati di test:
@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());
}
Questo test utilizza diversi metodi privati:
- [fill] alla riga 4, che popola il database con i dati di test;
- [registerCategories] alla riga 4, che popola i dizionari con i dati restituiti dal metodo [fill]. Questi due dizionari rappresentano le entità persistenti;
- [showDataBase] alla riga 6, che legge le due tabelle [CATEGORIES] e [PRODUCTS] e restituisce i dati letti;
- [checkShortCategorie] alla riga 13 controlla la categoria letta da [showDataBase]. Verifica che la versione breve di questa categoria corrisponda a quella memorizzata nel dizionario delle categorie;
- [checkShortProduct] riga 16 fa lo stesso per i prodotti;
- quando un'entità viene trovata in un dizionario, viene rimossa dal dizionario. Le righe 19–20 verificano che entrambi i dizionari siano vuoti. Se entrambe queste asserzioni sono vere, significa che:
- tutti i valori letti da [showDataBase] sono stati effettivamente trovati nei dizionari;
- i dizionari non contengono entità diverse da quelle che sono state lette;
Il metodo privato [fill] è il seguente:
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;
}
- righe 3–12: creiamo un elenco di [nbCategories] categorie, ciascuna contenente [nbProduits] prodotti;
- riga 15: questo elenco di categorie viene salvato. Abbiamo visto che il metodo [daoCategorie.saveEntities] salva anche i prodotti associati alle categorie, se presenti;
- riga 17: viene restituito l'elenco delle categorie salvato. Le entità salvate (categorie e prodotti) ora hanno una chiave primaria nel loro campo [id];
Il metodo privato [registerCategories] aggiungerà queste entità a entrambi i dizionari:
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);
}
}
}
Ogni dizionario utilizza la chiave primaria delle entità come chiave di accesso.
Una volta fatto ciò, il database precedentemente popolato verrà letto e visualizzato dal seguente metodo privato [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 };
}
- righe 4 e 8: recuperano le versioni abbreviate delle categorie e dei prodotti;
- riga 11: restituisce un array contenente i due elenchi di entità recuperate;
- righe 5 e 9: gli elenchi di entità vengono visualizzati utilizzando il seguente metodo privato [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));
}
Le entità vengono visualizzate utilizzando un mappatore JSON (riga 10). Questo mappatore è il secondo parametro del metodo [display], riga 2. Il contesto Spring definisce quattro mappatori JSON nel file [ConfigJdbc] della dipendenza 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;
}
- questi mappatori JSON (righe 7–9, 16–18, 26–28, 35–37) hanno un attributo
[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)]
che li rende bean istanziati ad ogni richiesta effettuata al contesto Spring. Questa è una novità. Tutti i bean Spring visti finora erano singleton: veniva creata una singola istanza, e quell’istanza veniva restituita ogni volta che ne veniva richiesto un riferimento dal contesto Spring. Perché questo cambiamento? Infatti, i quattro bean [jsonMapperShortCategory, jsonMapperLongCategory, jsonMapperShortProduct, jsonMapperLongProduct] configurano l'unico JSON mapper (che è effettivamente un singleton) definito nelle righe 2–5. Questo deve essere riconfigurato ogni volta che viene chiamato uno dei quattro bean precedenti, piuttosto che una sola volta durante l'inizializzazione del contesto. Se avessimo deciso di avere quattro diversi mappatori JSON — uno per ciascuno dei quattro bean — allora questi avrebbero potuto essere singleton. Ciò era del tutto possibile. Avremmo quindi scritto le righe 10, 19, 29, 38:
ObjectMapper jsonMapper = new ObjectMapper();
- I quattro mappatori JSON servono a configurare i filtri JSON per le entità [Product] e [Category]. In realtà abbiamo scritto (vedi sezioni 4.6 e 4.6) quanto segue:
e
La rappresentazione JSON dell'entità [Category] è controllata dal filtro JSON [jsonFilterCategory], mentre quella dell'entità [Product] dal filtro JSON [jsonFilterProduct]. I quattro mappatori JSON nel contesto Spring configurano questi due filtri come segue:
- il mappatore [jsonMapperShortCategory] configura il filtro JSON [jsonFilterCategory] per una versione breve della categoria: il campo [products] non sarà incluso nella rappresentazione JSON della categoria;
- il mapper [jsonMapperLongCategorie] configura il filtro JSON [jsonFilterCategorie] per una versione estesa della categoria: il campo [products] sarà incluso nella rappresentazione JSON della categoria;
- il mapper [jsonMapperShortProduct] configura il filtro JSON [jsonFilterProduct] per una versione breve del prodotto: il campo [category] non sarà incluso nella rappresentazione JSON del prodotto;
- Il mapper [jsonMapperLongProduit] configura il filtro JSON [jsonFilterProduit] per una versione lunga del prodotto: il campo [categorie] sarà incluso nella rappresentazione JSON del prodotto;
Abbiamo finito con il metodo privato [showDataBase]. Torniamo al codice di test [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());
}
- righe 6-8: recuperiamo le versioni abbreviate dei prodotti e delle categorie lette dal database;
- righe 10-11: controlli iniziali;
- righe 12–14: ogni categoria restituita dal metodo [showDataBase] viene controllata dal seguente metodo privato [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
}
- riga 1: [Category actual] è la categoria letta dal database e deve essere identica alla categoria nel dizionario [mapCategories];
- riga 2: recuperiamo la chiave primaria della categoria letta;
- riga 3: recuperiamo la categoria memorizzata con questa chiave primaria nel dizionario delle categorie;
- riga 4: la chiave viene rimossa dal dizionario per garantire che un'altra categoria recuperata non utilizzi la stessa chiave;
- riga 5: verifichiamo che le due categorie abbiano lo stesso nome;
La versione breve dei prodotti restituiti dal metodo [showDataBase] viene verificata dal seguente metodo privato [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
}
- riga 1: [Prodotto effettivo] è il prodotto breve letto dal database;
- righe 2-3: recuperiamo il prodotto con la stessa chiave primaria dal dizionario dei prodotti persistiti;
- riga 4: eliminiamo la voce trovata nel dizionario;
- righe 5-8: verifichiamo che i due prodotti abbiano gli stessi valori di campo;
4.11.2.4. Il metodo [getLongCategoriesByName3]
Questo test è il seguente:
@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());
}
- Riga 4: Popoliamo il database e recuperiamo l'elenco delle categorie e dei prodotti salvati;
- riga 7: testiamo il metodo [daoCategorie.getLongEntitiesByName(Iterable<String> names)] dal livello [DAO]. Richiediamo un elenco di due prodotti identificati dai loro nomi completi;
- riga 8: verifichiamo che l'elenco restituito da [daoCategorie.getLongEntitiesByName(Iterable<String> names)] contenga effettivamente due elementi;
- riga 9: i due elementi salvati alla riga 4 vengono aggiunti al dizionario delle categorie;
- righe 10–12: verifichiamo che i due elementi letti siano effettivamente quelli che sono stati salvati;
- riga 13: verifichiamo che il dizionario delle categorie sia vuoto, il che significa sia che tutte le categorie lette sono state trovate nel dizionario, sia che il dizionario non contiene alcun valore che non sia stato letto;
Riga 11: il metodo [checkLongCategory] controlla la versione lunga di una 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());
}
- La riga 6 verifica che il campo [products] della categoria non sia nullo. Questo perché la lettura di una categoria in formato lungo la restituisce sempre con un campo [products] non nullo. Se la categoria non ha prodotti, il campo [products] è un elenco vuoto ma esistente;
4.11.2.5. Il metodo [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;
}
Il metodo [updateDataBase1] aumenta del 10% i prezzi dei prodotti nella categoria denominata categorie[1] e verifica due cose:
- che il prezzo base sia effettivamente cambiato;
- che la versione del prodotto aggiornato sia stata incrementata di 1;
Il codice esegue le seguenti operazioni:
- riga 4: popola il database;
- riga 7: recupera la categoria denominata 'categorie[1]' dal database;
- righe 8–13: aumenta il prezzo di tutti i prodotti del 10% (riga 11). Inoltre, crea un dizionario che associa un prodotto alla sua versione (righe 9 e 12);
- riga 14: viene chiamato il metodo [daoProduit.saveEntities]. Questo aggiornerà i prodotti;
- riga 16: i prodotti nella categoria denominata 'category[1]' vengono recuperati dal database;
- righe 20–24: per tutti i prodotti di questa categoria, si verifica che il prezzo sia stato aggiornato (riga 22) e che la versione sia stata incrementata di 1 (riga 23);
4.11.2.6. Il metodo [deleteProductsByProduct1]
Il metodo [deleteProductsByProduct1] elimina i prodotti dalla tabella [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());
}
- riga 6: eliminiamo due prodotti;
- righe 8-9: verifichiamo che non siano più presenti nel database;
4.11.2.7. Il metodo [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());
}
- Riga 4: Popolare il database e recuperare l'elenco delle categorie salvate;
- riga 7: recuperare dal database la versione completa di due prodotti identificati dai loro nomi;
- riga 9: i prodotti [product[0,3], product[1,4]] presenti nell'elenco delle categorie della riga 4 vengono aggiunti al dizionario dei prodotti;
- riga 10: questi stessi due prodotti vengono cercati nel database utilizzando le loro chiavi primarie;
- righe 11–14: verifichiamo che i dati recuperati corrispondano a quelli memorizzati nel dizionario;
Il metodo privato [checkLongProduct] è il seguente:
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. Conclusione
Ci fermiamo qui. Finora ci sono 74 test e potremmo aggiungerne altri, dato che probabilmente ho dimenticato alcuni casi di test. Anche se non sono esaustivi, questi test hanno rilevato numerosi errori, per lo più casi limite che non erano stati previsti quando il livello [DAO] è stato scritto inizialmente. Una fase di test completa è essenziale per qualsiasi progetto.
Per eseguire il test, possiamo utilizzare la configurazione di esecuzione importata denominata [spring-jdbc-generic-04.JUnitTestDao].
![]() | ![]() |
4.11.3. Il test [JUnitTestPushTheLimits]
Il test [JUnitTestPushTheLimits] è un test delle prestazioni. Sfruttiamo il fatto che i test JUnit visualizzino il loro tempo di esecuzione per misurare le prestazioni del livello [DAO]. Questi risultati verranno poi confrontati con quelli delle implementazioni JPA del livello [DAO].
4.11.3.1. Struttura
Lo scheletro della classe [JUnitTestPushTheLimits] è il seguente:
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());
}
}
Qui vediamo la struttura della classe [JUnitTestDao]. Abbiamo già incontrato tutti questi metodi. Il test opera su un database di 2.500 categorie, ciascuna contenente 2 prodotti (righe 32–33). La tabella [CATEGORIES] avrà quindi 2.500 righe, mentre la tabella [PRODUCTS] ne avrà 5.000. Avremmo potuto includere più righe, ma l'esecuzione del test richiede già quasi un minuto. Abbiamo quindi scelto valori accettabili per l'utente in attesa del completamento del test.
Ci sono 18 test in totale. Vengono eseguiti utilizzando la configurazione di esecuzione [1]. I tempi di esecuzione sono riportati in [2]:
![]() |
4.11.3.2. doNothing [0,114]
Il metodo [doNothing] non esegue alcuna operazione. Viene utilizzato per misurare la durata del metodo [clean], che viene eseguito prima di ogni test e cancella il database. Come si può vedere sopra, la durata di questa operazione è trascurabile rispetto alle altre.
@Test
public void doNothing() {
// clean
}
4.11.3.3. perf01 [4.179]
Il test [perf01] viene utilizzato per misurare il tempo di riempimento del database:
@Test
public void perf01() {
// insert
fill(NB_CATEGORIES, NB_PRODUITS);
}
4.11.3.4. perf02 [7.624]
Il metodo [perf02]:
- piena il database;
- quindi modifica il nome di tutte le categorie e il prezzo di tutti i prodotti.
@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]
Il metodo [perf03]:
- riempie il database
- quindi elimina tutte le categorie una per una. Anche i prodotti vengono eliminati a causa della relazione a cascata tra la tabella [CATEGORIES] e la tabella [PRODUCTS].
Potrebbe sorprendere il fatto che questa operazione richieda meno tempo [3,911 s] rispetto all'operazione [perf01] [4,179 s], che ne esegue meno.
@Test
public void perf03() {
// delete categories and cascade products
daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}
Se osserviamo il codice del metodo [daoCategorie.deleteEntitiesByEntity], vediamo che verrà eseguito un [PreparedStatement] con 2.500 parametri (il numero di categorie). È qui che entra in gioco il bean [maxPreparedStatementParameters]; esso suddividerà l’istruzione SQL in più oggetti [PreparedStatement], ciascuno con un numero di parametri gestibile dal DBMS specifico.
4.11.3.6. perf04[2.426]
Il metodo [perf04]:
- popolerà il database;
- quindi recupera i dettagli completi di tutte le categorie;
@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]
Il metodo [perf05]:
- popola il database;
- quindi elimina i 5.000 prodotti utilizzando le loro chiavi primarie (quindi potenzialmente abbiamo un [PreparedStatement] con 5.000 parametri);
- verifica che la tabella dei prodotti sia ora vuota;
@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. Risultati
Non continueremo a presentare i vari test. Ci limiteremo a indicare cosa fanno e la loro durata. Queste durate hanno senso solo se confrontate tra loro. I loro valori dipendono dall'ambiente di test utilizzato (configurazione hardware e software). Tuttavia, se ottenuti nello stesso ambiente, possono essere confrontati.
Durata totale del test: 59,995 secondi
ruolo | ||
popolerà il database con 2.500 categorie e 5.000 prodotti | ||
Compila e poi modifica il database | ||
riempie il database, quindi elimina tutte le categorie e i relativi prodotti | ||
piena il database e richiede la versione estesa di tutte le categorie | ||
popola il database ed elimina i 5.000 prodotti uno per uno utilizzando le loro chiavi primarie | ||
piena il database ed elimina i 5.000 prodotti uno per uno utilizzando i loro nomi | ||
piena il database ed elimina i 5.000 prodotti uno per uno utilizzando i loro SKU | ||
popola il database e recupera la versione breve di tutti i prodotti in base ai loro nomi | ||
piena il database e recupera la versione lunga di tutti i prodotti in base al nome | ||
popolazione del database e recupero della versione breve di tutti i prodotti utilizzando le loro chiavi primarie | ||
popolerà il database e recupererà la versione completa di tutti i prodotti utilizzando le loro chiavi primarie | ||
popola il database e poi elimina tutte le categorie (e quindi i prodotti associati) una per una tramite i loro nomi | ||
piena il database e poi elimina tutte le categorie (e i prodotti associati) una per una utilizzando i loro SKU | | |
piena il database e recupera la versione breve di tutte le categorie tramite i loro nomi | ||
piena il database e richiede la versione lunga di tutte le categorie in base al nome | ||
popolazione del database e recupero della versione breve di tutte le categorie utilizzando le loro chiavi primarie | ||
popolazione del database e recupero della versione estesa di tutte le categorie tramite le loro chiavi primarie |
Questi risultati sono talvolta sorprendenti:
- è stato più veloce recuperare la versione lunga dei prodotti (perf09) rispetto alla versione breve (perf08), anche se la versione lunga comporta un join tra due tabelle;
- la durata del primo riempimento (perf01) supera significativamente quella di tutti i riempimenti successivi;
- il recupero della versione breve dei prodotti tramite i loro nomi (perf08) richiede più tempo rispetto al recupero tramite le chiavi primarie (perf10). Ciò sembra abbastanza logico. Ma per le versioni lunghe, vale il contrario (perf09, perf11);
Non ci soffermeremo quindi su questi risultati. Tuttavia, saranno utili per confrontare questa soluzione [Spring JDBC] con:
- [Spring JDBC] per gli altri cinque DBMS;
- [Spring JPA] di cui parleremo in seguito;





























