6. Spring Data JPA Hibernate
6.1. Introduction
Riprenderemo il database [dbproduitscategories] gestito dal progetto [spring-jdbc-04] e implementeremo le due interfacce [IDao<Categorie>, IDao<Produit>] definite in questo progetto. Questo ci consentirà di:
- confrontare i codici di implementazione;
- utilizzare lo stesso livello di test;
- confrontare le prestazioni delle due implementazioni;
![]() |
- il livello [JDBC] è implementato dal progetto [mysql-config-jdbc] esaminato nel paragrafo 3.3;
Passiamo ora agli altri livelli.
6.2. Configurazione dell’ambiente di lavoro
Con STS, importare il progetto [mysl-config-jpa-hibernate] [1] che si trova nella cartella [<exemples>/spring-database-config/mysql/eclipse] [2]:
![]() |
Questo progetto configura il livello [Spring JPA Hibernate] del progetto. Ogni implementazione JPA ha un proprio progetto di configurazione.
Quindi, importare il progetto [spring-jpa-generic] [1] che si trova nella cartella [<exemples>/spring-database-generic/spring-jpa] [2]:
![]() |
Fatto ciò, reinizializzate l’ambiente Maven (Alt-F5) di tutti i progetti presenti in [Package Explorer]:
![]() |
Quindi, per verificare l'ambiente di lavoro, eseguite la configurazione di esecuzione denominata [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Questa configurazione esegue il test [JUnitTestDao]. Il test deve avere esito positivo:
![]() |
6.3. Il progetto di configurazione del livello JPA
![]() |
Questo progetto ha lo scopo di configurare il livello JPA dell'architettura riportata di seguito:
![]() |
6.3.1. Configurazione Maven
Il progetto è un progetto Maven configurato dal seguente file [pom.xml]:
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration mysql openjpa</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- dipendenze variabili ********************************************** -->
<!-- JPA provider -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- dipendenze costanti ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- configurazione ereditata da JDBC -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- righe 5-7: l’artefatto Maven generato da questo progetto. I progetti di configurazione delle altre implementazioni JPA (Eclipselink e OpenJpa) utilizzeranno lo stesso artefatto. Ciò significa che solo uno di questi progetti può essere attivo in un dato momento. È quindi necessario evitare di averli tutti presenti in [Package Explorer]. Ne basta uno solo;
- righe 10-14: il progetto Maven padre che definisce la versione della maggior parte delle dipendenze necessarie al progetto;
- righe 19-22: la libreria Hibernate;
- righe 25-28: la libreria Spring Data;
- righe 32-34: il progetto di configurazione del livello JPA si basa su quello del livello JDBC, che definisce, tra le altre cose, il driver JDBC del SGBD utilizzato e le coordinate del database da utilizzare;
- righe 35-39: il progetto di configurazione del livello JDBC include la libreria [Spring JDBC], che in questo caso viene sostituita dalla libreria [Spring Data JPA]. Si raccomanda quindi di non includerla nelle dipendenze del progetto. Se rimane, ciò non causa tuttavia errori;
In definitiva, le dipendenze del progetto sono le seguenti:
![]() |
6.3.2. Configurazione Spring
![]() |
La classe [ConfigJpa] configura il progetto Spring:
package generic.jpa.config;
import javax.persistence.EntityManagerFactory;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
// il provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// pacchetti delle entità JPA
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// fonte dati
@Bean
public DataSource dataSource() {
// fonte dati TomcatJdbc
DataSource dataSource = new DataSource();
// configurazione dell'accesso JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// connessioni inizialmente aperte
dataSource.setInitialSize(5);
// risultato
return dataSource;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Gestore delle transazioni
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- riga 18: la classe è una classe di configurazione Spring;
- riga 19: importa i bean definiti dalla classe di configurazione [ConfigJdbc], utilizzata per configurare il progetto Spring [mysql-config-jdbc]. Si tratta dei filtri jSON;
- righe 23-30: definiscono l'implementazione JPA utilizzata, in questo caso l'implementazione Hibernate (riga 25);
- riga 26: è possibile scegliere se visualizzare o meno le operazioni SQL eseguite dall’implementazione Hibernate;
- riga 27: si indica a Hibernate il SGBD connesso. Questa configurazione è importante. Consente a Hibernate di utilizzare il dialetto SQL di SGBD MySQL, compresa la sua parte proprietaria. Inoltre, ciò gli fornisce informazioni sui tipi SQL e sugli oggetti di SGBD che potrà utilizzare. È proprio questa capacità dell’implementazione JPA di adattarsi a un SGBD specifico che le conferisce una grande portabilità tra i SGBD;
- riga 28: Hibernate può generare o meno le tabelle del database di destinazione a partire dalle entità JPA che troverà. Questa generazione avviene solo se le tabelle sono assenti. Se sono già presenti, non viene eseguita alcuna operazione. Utilizzeremo questa capacità di generare le tabelle quando illustreremo come sono stati generati gli script SQL per la creazione dei diversi database utilizzati in questo documento;
- riga 33: il pacchetto in cui si trovano le entità JPA del database [dbproduitscategories];
- righe 36-49: la fonte dati [tomcat-jdbc] collegata al database [dbproduitscategories];
- righe 52-60: il bean denominato [entityManagerFactory] (deve chiamarsi proprio così) è il bean che creerà l’oggetto [EntityManager] che gestisce il contesto di persistenza JPA. Tutte le operazioni JPA passano attraverso di esso. L'utilizzo di [Spring Data JPA] fa sì che non useremo mai direttamente questo oggetto. Dobbiamo tuttavia configurarlo. Esso necessita di conoscere le seguenti informazioni:
- l’implementazione JPA utilizzata (riga 55);
- la fonte di dati utilizzata (riga 57);
- le entità JPA di tale fonte (riga 56);
- riga 58: inizializza EntityManager con queste informazioni;
- riga 59: restituisce il singleton [entityManagerFactory];
- righe 63-68: definiscono il gestore delle transazioni. Deve chiamarsi [transactionManager];
- riga 65: viene creato un gestore di transazioni JPA;
- riga 66: viene collegato alla fonte dati della riga 37 tramite il bean [entityManagerFactory] (righe 53 e 57);
Solo il bean delle righe 23-30 dipende dall'implementazione JPA utilizzata. Gli altri bean si basano quindi su di esso.
6.3.3. Le entità del livello [JPA]
![]() |
![]() |
Il database di destinazione è il database [dbproduitscategories] con le sue due tabelle [CATEGORIES] e [PRODUITS]. Abbiamo visto che essa contiene anche altre tre tabelle, [USERS, ROLES, USERS_ROLES], che saranno utilizzate per proteggere il servizio web che verrà implementato sul web. Per il momento tralasceremo queste tabelle. Ricordiamo, a titolo di riepilogo, la struttura delle tabelle [CATEGORIES] e [PRODUITS]:
La tabella [PRODUITS] è la seguente:
![]() |
- [ID]: la chiave primaria autoincrementale della tabella [2];
- [NOM]: il nome univoco del prodotto [4];
- [PRIX]: 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 verrà modificato, il suo numero di versione verrà incrementato dal codice che gestisce la tabella;
- [CATEGORIE_ID]: la chiave esterna nella tabella [CATEGORIES] per indicare la categoria a cui appartiene il prodotto;
![]() |
- in [1-3], la chiave esterna [CATEGORIE_ID] della tabella [PRODUITS]. Si riferisce 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 poiché viene utilizzato nella costruzione del livello [DAO] che sfrutta il database [dbproduitscategories];
La tabella delle categorie [CATEGORIES] è la seguente:
![]() |
- [ID]: chiave primaria autoincrementale;
- [VERSIONING]: numero di versione della categoria;
- [NOM]: nome univoco della categoria;
Descriveremo ora le entità JPA, [Produit] e [Categorie], immagini delle tabelle [PRODUITS] e [CATEGORIES].
![]() |
6.3.3.1. L'interfaccia [AbstractCoreEntity]
L'interfaccia [AbstractCoreEntity] è implementata dalle entità JPA, [Categorie] e [Produit]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getter e setter dei campi [id], [version], [entityType]
public Long getId();
public void setId(Long id);
public Long getVersion();
public void setVersion(Long version);
public enum EntityType {
PROXY, POJO
}
public EntityType getEntityType();
public void setEntityType(EntityType entityType);
}
Questa interfaccia, implementata dalle due entità JPA, serve semplicemente a elencare i metodi per leggere/scrivere i campi [id], [version] e [entityType] di tali entità. Il ruolo del campo [entityType] verrà spiegato in seguito;
6.3.3.2. L’entità JPA [Produit]
La classe [Produit] è l’entità JPA associata a una riga della tabella [PRODUITS]:
![]() |
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
// proprietà
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// proprietà
@Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
private String nom;
@Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
private Long idCategorie;
@Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
private double prix;
@Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
private String description;
// categoria
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// produttori
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
this.id = id;
this.version = version;
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// firma
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);
}
// ------------------------------------------------------------
// ridefinizione di [equals] e [hashcode]
@Override
public int hashCode() {
Long id = getId();
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;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters e setters
...
public void setCategorie(Categorie categorie) {
// tipo dell'entità
if (entityType == EntityType.PROXY) {
throw new ProxyException(1005, new RuntimeException(
"On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
}
this.categorie = categorie;
}
}
- riga 21: l'annotazione [@Entity] rende la classe [Produit] un'entità gestita dal livello [JPA]. Si può scrivere anche [@Entity(name="MonProduit")], il che assegna all’entità il nome [MonProduit]. In assenza di questa informazione, il nome dell’entità è il nome della classe, in questo caso [Produit]. Questa denominazione diventa necessaria quando tra le entità sono presenti due classi di pacchetti diversi che portano lo stesso nome;
- riga 22: l’annotazione [@Table(name = "PRODUITS")] indica che la classe [Produit] è l’oggetto corrispondente a una riga della tabella [PRODUITS] del database;
- riga 23: il nome del filtro jSON da applicare all’entità. Vedremo che la proprietà [categorie] della riga 58 non è sempre disponibile. È quindi necessario escluderla dalla rappresentazione jSON dell’oggetto. A tal fine abbiamo bisogno di un filtro. È quindi in un filtro denominato [jsonFilterCategorie] che indicheremo se desideriamo o meno la proprietà [categorie];
- riga 26: l’annotazione [@Id] rende il campo annotato il campo associato alla chiave primaria della tabella della riga 19;
- riga 27: l'annotazione [@GeneratedValue(strategy = GenerationType.IDENTITY)] definisce la modalità di generazione automatica della chiave primaria nella tabella [PRODUITS]. È l'attributo [strategy] a determinarla. Esistono diverse modalità:

La strategia [IDENTITY] non è disponibile per tutti i SGBD. Tra i sei SGBD testati, era disponibile per i SGBD e [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. Per gli altri due [Oracle Express 11g Release 2, Firebird 2.5.4] è stato necessario utilizzare la strategia [SEQUENCE]. Per la portabilità tra implementazioni JPA, non si deve adottare la strategia [AUTO], che lascia alla discrezione dell’implementazione JPA la scelta della strategia di generazione della chiave primaria. Pertanto, con MySQL 5 e la strategia [AUTO]:
- Hibernate sceglie la strategia [IDENTITY] con la modalità [AUTO_INCREMENT] per la chiave primaria;
- EclipseLink sceglie la strategia [TABLE], che crea una tabella denominata per impostazione predefinita [SEQUENCE], sulla quale è necessario eseguire una query per ottenere le chiavi primarie.
In definitiva, la struttura del database gestita da queste due implementazioni JPA non è la stessa. Se è stata generata da Hibernate, non sarà utilizzabile da EclipseLink e viceversa.
- riga 28: l’annotazione [@Column(name="ID"] specifica il nome della colonna della tabella [PRODUITS] da associare al campo [id];
- riga 29: si utilizza il tipo [Long] anziché [long] per la chiave primaria. Infatti, le chiavi primarie [null] hanno un significato particolare per JPA. Si preferirà quindi utilizzare qui un tipo oggetto piuttosto che un tipo semplice;
- riga 31: l'annotazione [@Version] indica che il campo [version] è associato a una colonna di versioning. L'implementazione JPA incrementerà questo numero di versione ogni volta che l'entità verrà modificata. Questo numero serve a impedire l’aggiornamento simultaneo 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 tale modifica nel database: il numero di versione passa quindi a V1+1. U2 modifica a sua volta E e salva tale modifica nel database: riceverà un'eccezione poiché possiede una versione (V1) diversa da quella presente nel database (V1+1);
- riga 36: il tipo dell'entità. Ce ne saranno due: POJO e PROXY. Per impostazione predefinita, l'istanza generata sarà un POJO (Plain Old Java Object). In alcuni casi, le istanze [Produit] recuperate dal database saranno di tipo [PROXY]. Ciò accadrà nel caso in cui la proprietà [Categorie categorie] della riga 58 non sia stata inizializzata con una categoria a causa dell'attributo [fetch = FetchType.LAZY] della riga 56. In questo caso, le implementazioni JPA che verranno testate differiscono:
- [Hibernate, OpenJPA]: l’accesso alla categoria di un prodotto di tipo [PROXY] genera un’eccezione. Hibernate utilizza il termine «proxy» per indicare un’istanza JPA ottenuta in modalità [LAZY]. Ecco perché ho utilizzato questo termine per indicare questo tipo di entità;
- [EclipseLink]: l’accesso alla categoria di un prodotto di tipo [PROXY] comporta la ricerca di tale categoria nel database e non si verifica alcuna eccezione;
Poiché volevo disporre di un livello di test indipendente dall’implementazione JPA utilizzata, ho voluto conoscere il tipo di ciascuna entità: POJO o PROXY. Ecco perché ho aggiunto il campo [entityType] alle entità JPA;
- riga 35: l’annotazione [@Transient] indica che l’implementazione JPA deve ignorare questo campo. Infatti, esso non esiste nelle tabelle di SGBD;
- riga 40: la classe [Produit] genera un'eccezione di tipo [ProxyException] che richiede il nome della classe;
- riga 38: come in precedenza, si indica che l’implementazione JPA deve ignorare questo campo;
- riga 39: l'annotazione [@JsonIgnore] indica che il serializzatore/deserializzatore jSON di un'istanza [Produit] deve ignorare questo campo;
- riga 43: l'annotazione [@Column] associa il campo [nom] alla colonna [NOM] della tabella [PRODUITS]. Quando il campo ha lo stesso nome della colonna associata (senza distinzione tra maiuscole e minuscole), l'annotazione [@Column] può essere omessa. Questo sarebbe il caso in questione. Gli attributi [unique = true, length = 30, nullable = false] vengono utilizzati solo quando l’implementazione JPA deve generare la tabella [CATEGORIES] a partire dall’entità [Produit]. Essi saranno tradotti dagli attributi SQL e [UNIQUE, VARCHAR(30), NOT NULL], che fanno sì che la colonna [NOM] abbia al massimo 30 caratteri, sia univoca nella tabella e non possa assumere il valore NULL;
- righe 46-47: il campo [idCategorie] è associato alla colonna [CATEGORIE_ID]. Torneremo sui suoi attributi più avanti;
- righe 49-50: il campo [prix] è associato alla colonna [PRIX];
- righe 52-53: il campo [description] è associato alla colonna [DESCRIPTION];
- righe 56-58: la categoria del prodotto;
- riga 56: l'annotazione [@ManyToOne] indica che la colonna dell'annotazione della riga 57, [@JoinColumn(name = "CATEGORIE_ID")], è una chiave esterna della tabella [PRODUITS] dell'entità [Produit] sulla tabella [CATEGORIES] associata all’entità della riga 58. Questa annotazione deve riferirsi a un’entità JPA. Pertanto, la classe della riga 58 deve essere un’entità JPA;
- riga 56: l'annotazione [fetch = FetchType.LAZY] richiede che, quando si recupera un prodotto dalla tabella [PRODUITS], la sua categoria (riga 58) non venga recuperata immediatamente (lazy loading). Viene quindi recuperata al primo richiamo del metodo [getCategorie]. A tal fine, in fase di esecuzione, il livello JPA arricchisce il metodo iniziale [getCategorie] (che si limita a restituire il campo categorie) con una chiamata al metodo SGBD per recuperare la categoria, una tecnica denominata «proxying». Le implementazioni JPA differiscono nell’implementazione di questa caratteristica, come abbiamo detto in precedenza. Questo attributo non è vincolante. L’implementazione JPA utilizzata ha la facoltà di ignorarlo. È proprio perché la proprietà [categorie] può essere presente o meno che abbiamo introdotto il filtro jSON alla riga 23. La colonna di join [CATEGORIE_ID] della tabella [PRODUITS] viene aggiornata automaticamente al momento dell’inserimento o dell’aggiornamento del prodotto. Essa riceve il valore di [categorie.getId()], dove [categorie] è il campo della riga 58. La specifica JPA impone che questa colonna di join non possa essere aggiornata in altro modo. Inoltre, impone gli attributi [insertable = false, updatable = false] della riga 46, che fanno sì che la colonna [CATEGORIE_ID] (ovvero la colonna di join) associata al campo [idCategorie] non possa essere modificata dal campo [idCategorie]. Sarà possibile solo il trasferimento della colonna [CATEGORIE_ID] nel campo [idCategorie];
- righe 91-104: l’uguaglianza tra le entità [Produit] è definita come l’uguaglianza tra le loro chiavi primarie [id];
- righe 108-115: per rendere portabile il nostro livello di test, gestiremo in modo uniforme le entità [PROXY] delle tre implementazioni JPA e [Hibernate, EclipseLink, OpenJpa]. Per un tipo [Produit] di tipo [PROXY], verrà impedita la modifica del valore del campo [categorie]. La classe [ProxyException] è la seguente:
![]() |
package generic.jpa.infrastructure;
import generic.jdbc.infrastructure.UncheckedException;
public class ProxyException extends UncheckedException {
private static final long serialVersionUID = 7278276670314994574L;
public ProxyException() {
}
public ProxyException(int code, Throwable e, String simpleClassName) {
super(code, e, simpleClassName);
}
}
Per concludere l’analisi di questa entità, va notato che le annotazioni e i relativi attributi vengono utilizzati in due casi ben distinti:
- per creare le tabelle del database;
- per utilizzarle. In questo caso, l’implementazione JPA si aspetta di trovare le tabelle così come le avrebbe generate essa stessa. Non è quindi possibile associare all’entità [Produit] precedente una tabella [PRODUITS] qualsiasi. È necessario che quest’ultima possieda almeno (può averne altre) le caratteristiche della tabella [PRODUITS] che avrebbe generato. Quando si lavora con JPA, l’ideale è partire da una base vuota in cui si lascia che JPA generi le tabelle. Affronteremo questa generazione un po’ più avanti. Lo script SQL fornito per SGBD e MySQL è stato generato a partire dalle tabelle generate da JPA.
Tutti gli attributi dell’entità [Produit] vengono utilizzati per la generazione della tabella [PRODUITS]. Una volta completata questa operazione, gli attributi di generazione come [unique = true, length = 30, nullable = false] non vengono più utilizzati durante l’elaborazione delle tabelle.
6.3.3.3. L'entità JPA [Categorie]
La classe [Categorie] è un’entità JPA associata a una riga della tabella [CATEGORIES]:
![]() |
Il suo codice è il seguente:
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// proprietà
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// prodotti associati
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// costruttori
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
this.id = id;
this.version = version;
this.nom = nom;
this.produits = produits;
}
// firma
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// metodi
public void addProduit(Produit produit) {
// tipo di entità
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// aggiunta di un prodotto
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// si aggiunge il prodotto
produits.add(produit);
// si imposta la categoria
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// ridefinizione di [equals] e [hashcode]
@Override
public int hashCode() {
Long id = getId();
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;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getter e setter
...
}
- riga 24: la classe è un'entità JPA;
- riga 25: associata alla tabella [CATEGORIES];
- riga 26: la rappresentazione jSON dell'entità [Categorie] è controllata dal filtro denominato [jsonFilterCategorie]. Quest’ultimo dovrà essere configurato prima di qualsiasi richiesta di rappresentazione jSON dell’entità. Il filtro [jsonFilterCategorie] verrà utilizzato per escludere o meno dalla rappresentazione jSON dell’entità [Categorie] il campo [produits] della riga 40;
- righe 29-32: il campo [id] è associato alla chiave primaria [ID] della tabella [CATEGORIES]. La modalità di generazione selezionata è la modalità [IDENTITY], quindi la modalità [AUTO_INCREMENT] per MySQL;
- righe 34-36: il campo [version] è collegato alla colonna di versioning [VERSIONING] della tabella [CATEGORIES];
- righe 38-39: il tipo dell'entità [Categorie];
- righe 41-43: il nome semplice della classe [Categorie];
- righe 46-47: il campo [nom] è collegato alla colonna [NOM] della tabella [CATEGORIES]. Gli vengono assegnati gli attributi JPA e [unique = true, length = 30, nullable=false] affinché, al momento della generazione della tabella [CATEGORIES], la colonna [NOM] abbia gli attributi SQL e [UNIQUE, VARCHAR(30), NOT NULL];
- righe 50-51: i prodotti che appartengono alla categoria;
- riga 50: l'annotazione [@OneToMany] è la relazione inversa della relazione [@ManyToOne] che abbiamo incontrato nell'entità [Produit]. L'attributo [mappedBy = "categorie"] indica il campo dell'entità [Produit] annotato dalla relazione inversa [@ManyToOne]. L'attributo [cascade = { CascadeType.ALL }] richiede che le operazioni (persist, merge, remove) eseguite su un'@Entity [Categorie] vengano propagate in cascata alle [produits] della riga 51. È possibile specificare cascate parziali utilizzando le costanti [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- riga 50: l'attributo [fetch = FetchType.LAZY] specifica che, quando si recupera una categoria dalla tabella [CATEGORIES], i relativi prodotti non vengano recuperati immediatamente. Essi vengono infatti recuperati solo al primo richiamo del metodo [getProduits]. A tal fine, in fase di esecuzione, il livello JPA arricchisce il metodo iniziale [getProduits] (che si limita a restituire il campo produits) con una chiamata al metodo SGBD per recuperare i prodotti della categoria. Questo attributo è vincolante. L’implementazione JPA non può ignorarlo. Poiché la proprietà [produits] può essere inizializzata o meno, abbiamo introdotto il filtro jSON alla riga 26, che ci consentirà di specificare se desideriamo o meno questa proprietà e il tipo dell’entità alla riga 39;
- righe 71-88: il metodo [addProduit] consente di aggiungere un prodotto alla categoria;
- righe 73-76: per uniformare la gestione dei proxy tra diverse implementazioni di JPA, si è deciso che non è possibile aggiungere prodotti a un’entità [Categorie] di tipo PROXY;
- righe 92-112: due entità [Categorie] saranno considerate uguali se hanno la stessa chiave primaria [id];
6.3.4. Il file [persistence.xml]
![]() |
Le applicazioni JPA devono definire alcune proprietà del fornitore JPA utilizzato, nonché le entità JPA da utilizzare, in un file [META-INF/persistence.xml] presente nel Classpath dell'applicazione. Nell’esempio sopra riportato, il file è stato inserito nella cartella [src/main/resources], che fa effettivamente parte del Classpath di un progetto Eclipse. Quando si utilizza JPA insieme a Spring, alcune informazioni che dovrebbero trovarsi nel file [persistence.xml] vengono collocate altrove, nelle classi di configurazione di Spring. In un’applicazione Spring JPA, è Spring a gestire JPA. Con Spring JPA Hibernate, il file [persistence.xml] può essere ridotto alla sua forma più semplice:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
- righe 1-5: un file [persistence.xml] deve avere un tag radice <persistence>. Gli attributi del tag alla riga 2 non saranno utilizzati in questa applicazione;
- un file di persistenza può definire una o più unità di persistenza tramite il tag <persistence-unit> (riga 4). Un’unità di persistenza gestisce l’accesso a un database specifico. Se l’applicazione gestisce due database contemporaneamente, avrà due unità di persistenza;
- riga 4: un’unità di persistenza ha un nome [attribut name], supporta un tipo di transazione [attribut transaction-type], possiede proprietà e definisce le entità associate alle tabelle del database gestito dall’unità di persistenza. In questo caso, poiché gli accessi al database saranno gestiti da [Spring JPA Hibernate], queste ultime due informazioni possono essere collocate altrove. Esistono due tipi di transazione:
- [RESOURCE_LOCAL]: le transazioni sono gestite dall’applicazione stessa. È il caso in questione, in cui sarà Spring a gestire le transazioni;
- [JTA] (Java Transaction API): è il contenitore EJB (Enterprise Java Bean) che esegue l’applicazione a gestire automaticamente le transazioni in base alle annotazioni Java presenti nel codice. In questo caso non ci troviamo in questa configurazione;
Vedremo in seguito che il contenuto di questo file [persistence.xml] dipende dall'implementazione JPA utilizzata.
6.4. Il progetto [spring-jpa-generic]
Ricordiamo cosa vogliamo fare. Vogliamo implementare la seguente architettura:
![]() |
in cui il livello [DAO] implementerebbe l’interfaccia [IDao<Produit>, IDao<Categorie>] studiata nel capitolo 4. Si tratta di confrontare due implementazioni di questa interfaccia:
- una realizzata con Spring JDBC;
- l’altra realizzata con Spring JPA;
Nell'architettura sopra riportata:
- il livello [JDBC] è implementato dal progetto [mysql-config-jdbc] analizzato nel paragrafo 3.3;
- il livello [JPA] è implementato dal progetto [mysql-config-jpa-hibernate] analizzato nel paragrafo 6.3;
Il progetto [spring-jpa-generic] garantisce l'implementazione dei livelli [DAO] e [Spring Data].
![]() |
6.4.1. Configurazione Maven
Il progetto [spring-jpa-generic] è 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-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jpa-generic</name>
<description>démo spring data avec tables de catégories et de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- configurazione JPA del SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- righe 22-26: il progetto ha una sola dipendenza, quella relativa al progetto che configura il livello [JPA] dell’applicazione e che abbiamo appena esaminato. Si tratta di un’applicazione generica:
- si passa da SGBD modificando il progetto di configurazione del livello [JDBC];
- si cambia l’implementazione JPA modificando il progetto di configurazione del livello [JPA];
Alla fine, le dipendenze sono le seguenti:
![]() |
6.4.2. Configurazione Spring
![]() |
La classe [AppConfig] configura il progetto Spring:
package spring.data.config;
import generic.jpa.config.ConfigJpa;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
- riga 11: la classe è una classe di configurazione Spring;
- riga 10: l'annotazione [@EnableJpaRepositories] serve a indicare i pacchetti contenenti le interfacce [CrudRepository] di Spring Data. Ciò li rende componenti Spring che possono essere iniettati in altri componenti Spring;
- riga 12: l'annotazione [@ComponentScan] indica che il pacchetto [spring.data.dao] deve essere analizzato alla ricerca di componenti Spring. Verranno individuati i componenti [DaoCategorie] e [DaoProduit];
- riga 13: vengono importati i bean della classe di configurazione [ConfigJpa]. Tra questi si troverà il bean dell’implementazione JPA utilizzata (Hibernate, Eclipselink, OpenJpa), la fonte dati da utilizzare, il bean EntityManager che gestirà le operazioni JPA, il gestore delle transazioni;
6.4.3. Il livello [Spring Data]
![]() |
![]() |
6.4.3.1. L'interfaccia [CategoriesRepository]
L'interfaccia [CategoriesRepository] gestisce gli accessi alla tabella [CATEGORIES]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Categorie;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// categoria con i relativi prodotti
@Query("select c from Categorie c left join fetch c.produits where c.id=?1")
public Categorie getLongCategorieById(Long id);
@Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
public Categorie getLongCategorieByName(String nom);
@Query("select c from Categorie c where c.nom in ?1")
public List<Categorie> getShortCategoriesByName(Iterable<String> names);
@Query("select c from Categorie c where c.id in ?1")
public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
@Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
public List<Categorie> getLongCategoriesById(List<Long> names);
@Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
public List<Categorie> getLongCategoriesByName(List<String> names);
@Query("select c from Categorie c")
public List<Categorie> getAllShortCategories();
@Query("select distinct c from Categorie c left join fetch c.produits")
public List<Categorie> getAllLongCategories();
}
- riga 10: l'interfaccia [CrudRepository] è stata utilizzata e illustrata nel paragrafo 5.1.3. Si ricorda che:
- il primo tipo di parametro dell'interfaccia è l'entità JPA gestita per gli accessi CRUD (findOne, findAll, salva, elimina, deleteAll),
- il secondo tipo di parametro dell'interfaccia è quello della chiave primaria dell'entità JPA, in questo caso un numero intero [Long];
I metodi dell'interfaccia sono implementati tramite query JPQL (Java Persistence Query Language). Queste query interrogano le entità JPA. In una query di questo tipo:
- le tabelle vengono sostituite dalle entità JPA ad esse associate;
- le colonne sono sostituite dai campi delle entità JPA utilizzate nella query;
Prendiamo l’esempio delle righe 31-32: il metodo della riga 32 restituisce tutte le categorie del database nella loro versione abbreviata. È implementato dalla query JPQL (Java Persistence Query Language) della riga 31, che assomiglia molto alla sua controparte SQL. Per approfondire JPQL, si può consultare [ref2] (cfr. paragrafo 1.2).
I metodi dell’interfaccia [CategoriesRepository] sono i seguenti:
- righe 13-14: il metodo [getLongCategorieById] restituisce la versione estesa di una categoria indicizzata dalla sua chiave primaria [id], ovvero la categoria con i relativi prodotti. Ricordiamo che nell’entità [Categorie], il campo [produits] aveva l’attributo [fetch = FetchType.LAZY] (lazy loading). Nella query JPQL, si forza il caricamento dei prodotti con la parola chiave [fetch]. Il parametro ?1 della query verrà sostituito in fase di esecuzione con il valore del primo parametro del metodo alla riga 12, quindi con il parametro [Long id];
- righe 16-17: il metodo [getLongCategorieByName] restituisce la versione estesa di una categoria indicata con il nome [nom];
- righe 19-20: il metodo [getShortCategoriesByName] restituisce le versioni brevi delle categorie indicate con i loro nomi. Il campo [produits] di queste categorie non è null. Esso contiene il riferimento a un proxy (una classe creata dall’implementazione JPA) il cui ruolo è quello di restituire i prodotti della categoria quando viene chiamato. La sua chiamata al di fuori del contesto di persistenza JPA provoca un’eccezione (Hibernate e OpenJpa, ma non EclipseLink). Per questo motivo, non utilizzeremo il campo [produits] della versione breve di una categoria;
- righe 22-23: il metodo [getShortCategoriesById] restituisce le versioni brevi delle categorie referenziate tramite le loro chiavi primarie [id];
- righe 25-26: il metodo [getLongCategoriesById] restituisce le versioni lunghe delle categorie referenziate tramite le loro chiavi primarie [id];
- righe [28-29]: il metodo [getLongCategoriesByName] restituisce le versioni lunghe delle categorie identificate dai loro nomi;
- righe 31-32: il metodo [getAllShortCategories] restituisce le versioni brevi di tutte le categorie;
- righe 34-35: il metodo [getAllLongCategories] restituisce le versioni lunghe di tutte le categorie;
Nota: non tutte le implementazioni JPA accettano la stessa sintassi di JPQL. Pertanto, la seguente sintassi è accettata da Hibernate e da EclipseLink, ma non da OpenJpa:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
OpenJpa non accetta l’alias [p] sopra indicato.
6.4.3.2. L'interfaccia [ProduitsRepository]
L'interfaccia [ProduitsRepository] gestisce gli accessi alla tabella [PRODUITS]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// un prodotto con la relativa categoria
@Query("select p from Produit p left join fetch p.categorie where p.id=?1")
public Produit getLongProduitById(Long id);
@Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
public Produit getLongProduitByName(String nom);
@Query("select p from Produit p where p.id in ?1")
public List<Produit> getShortProduitsById(List<Long> ids);
@Query("select p from Produit p where p.nom in ?1")
public List<Produit> getShortProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
public List<Produit> getLongProduitsById(List<Long> ids);
@Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
public List<Produit> getLongProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie")
public List<Produit> getAllLongProduits();
@Query("select p from Produit p")
public List<Produit> getAllShortProduits();
}
- righe [15-16]: il metodo [getLongProduitById] restituisce la versione estesa di un prodotto identificato dalla sua chiave primaria [id], quindi con la relativa categoria. Ricordiamo che nell’entità [Produit], il campo [categorie] aveva l’attributo [fetch = FetchType.LAZY] (lazy loading). Nella query JPQL, si forza il caricamento della categoria con la chiave [fetch];
- righe 18-19: il metodo [getLongProduitByName] restituisce la versione estesa di un prodotto identificato dal suo nome;
- righe 21-22: il metodo [getShortProduitsById] restituisce la versione breve dei prodotti identificati dalla loro chiave primaria [id]. In questa versione breve, il campo [categorie] non ha il valore null. Contiene il riferimento a un proxy generato dall’implementazione JPA, che, se chiamato, recupererà la categoria del prodotto. Questa chiamata può essere effettuata solo nel contesto di persistenza JPA. Effettuarla altrove provoca un'eccezione (Hibernate e OpenJpa, ma non EclipseLink). Pertanto, nel livello [DAO] o altrove, non utilizzeremo il campo [categorie] di un prodotto nella sua versione breve. Nella versione breve del prodotto, il campo [idCategorie] viene inizializzato. Il suo valore è la chiave primaria della categoria a cui appartiene il prodotto. Ciò consente in seguito di richiedere tale categoria al livello [DAO] tramite il metodo [DaoCategorie. getShortCategoriesById(idCategorie)];
- righe 24-25: il metodo [getShortProduitsByName] restituisce la versione breve dei prodotti identificati dai loro nomi;
- righe 27-28: il metodo [getLongProduitsById] restituisce la versione estesa dei prodotti identificati tramite le loro chiavi primarie;
- righe 30-31: il metodo [getLongProduitsByName] restituisce la versione estesa dei prodotti identificati dai loro nomi;
- righe 33-34: il metodo [getAllLongProduits] restituisce la versione estesa di tutti i prodotti;
- righe 36-37: il metodo [getAllShortProduits] restituisce la versione breve di tutti i prodotti;
Queste interfacce saranno implementate da classi generate dall'implementazione JPA al momento dell'esecuzione del progetto. Tali classi sono denominate classi [proxy]. Per impostazione predefinita, i metodi dell’interfaccia [CrudRepository] vengono eseguiti all’interno di una transazione. Il fatto che le interfacce [ProduitsRepository, CategoriesRepository] estendano la classe [CrudRepository] le rende componenti Spring. In quanto tali, possono essere iniettate in altri componenti Spring.
6.4.4. Il livello [DAO]
![]() |
![]() |
6.4.4.1. L'interfaccia [IDao<T>]
L’interfaccia [IDao<T>] è quella già esaminata nell’implementazione del livello [DAO] realizzata con Spring JDBC (cfr. paragrafo 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// elenco di tutte le entità T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// entità specifiche - versione breve
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);
// entità specifiche - versione estesa
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);
// aggiornamento di più entità
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// eliminazione di tutte le entità
public void deleteAllEntities();
// eliminazione di più entità
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);
}
6.4.4.2. La classe astratta [AbstractDao]
![]() |
La classe astratta [AbstractDao] è la classe padre delle classi che implementano il livello [DAO]:
- la classe [DaoProduit], che implementa l'interfaccia [IDao<Produit>] e gestisce gli accessi alla tabella [PRODUITS];
- la classe [DaoCategorie] che implementa l’interfaccia [IDao<Categorie>] e gestisce gli accessi alla tabella [CATEGORIES];
Il suo codice è quello descritto al paragrafo 4.8, con la seguente precisazione: nessun metodo possiede l’attributo [@Transactional] che fa sì che il metodo venga eseguito all’interno di una transazione. Qui si sfrutta il fatto che le interfacce [CrudRepository] di Spring Data vengono eseguite per impostazione predefinita all’interno di una transazione.
6.4.4.3. La classe [DaoCategorie]
![]() |
La classe [DaoCategorie] implementa l'interfaccia [IDao<Categorie>] nel modo seguente:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Categorie> getAllShortEntities() {
try {
return setShortCategoriesType(categoriesRepository.getAllShortCategories());
} catch (Exception e) {
throw new DaoException(211, e, simpleClassName);
}
}
private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
for (Categorie categorie : categories) {
categorie.setEntityType(EntityType.PROXY);
}
return categories;
}
@Override
public List<Categorie> getAllLongEntities() {
try {
return categoriesRepository.getAllLongCategories();
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return categoriesRepository.getLongCategoriesById(ids);
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return categoriesRepository.getLongCategoriesByName(names);
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
categoriesRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
categoriesRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(212, e, simpleClassName);
}
}
}
- riga 17: l’annotazione [@Component] rende la classe [DaoCategorie] un componente Spring;
- riga 18: la classe [DaoCategorie] estende la classe [AbstractDao<Categorie>], il che fa sì che essa implementi l'interfaccia [IDao<Categorie>];
- righe 20-24: iniezione dei riferimenti alle due interfacce [CrudRepository] e [Spring Data]. Tale iniezione avverrà durante l’istanziazione degli oggetti Spring, in genere all’inizio dell’esecuzione del progetto Spring;
- tutti i metodi della classe delegano il lavoro ai metodi con lo stesso nome delle interfacce [CrudRepository];
- tutti i metodi che riportano le entità nella loro versione abbreviata lo indicano impostando il tipo dell’entità su [EntityType.PROXY] (righe 29, 63, 72);
Il metodo [saveEntities] merita una spiegazione:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// si prendono nota dei prodotti che verranno inseriti
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// si approfitta dell'occasione per ripristinare (se necessario) la relazione prodotto --> categoria
produit.setCategorie(categorie);
}
}
}
// si salvano le categorie e i prodotti
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// si aggiorna il campo [idCategorie] dei prodotti inseriti
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// risultato
return categories;
}
- riga 2: le categorie passate come parametri sono sia categorie da inserire ([id==null]) sia da modificare ([id!=null]);
- riga 20: si salvano le categorie con il metodo [categoriesRepository.save(entities)]. Durante i test, si nota che il campo [idCategorie] dei prodotti salvati (id==null) non è compilato. Per risolvere questo problema, alle righe 4-17 si annotano i prodotti che verranno inseriti e, una volta salvati, si compila il loro campo [idCategorie] (righe 25-27);
- righe 5-17: si scorre l’elenco delle categorie;
- righe 8-16: per ogni categoria si scorre il relativo elenco di prodotti. Qui sorge una difficoltà. Il metodo [saveEntities] viene utilizzato sia per salvare che per modificare una categoria. In quest’ultimo caso, la categoria potrebbe essere stata recuperata nella sua versione abbreviata, con il riferimento a un metodo proxy nel campo [produits]. Utilizzarlo con Hibernate provoca quindi un'eccezione, poiché la categoria utilizzata non si trova più nel contesto di persistenza JPA, che è stato chiuso al termine della transazione del metodo che ha restituito le versioni abbreviate delle categorie. Si utilizza quindi il campo [EntityType] dell’entità [Categorie] alla riga 8 per verificare se è possibile o meno accedere all’elenco dei prodotti della categoria;
- riga 14: si associa il prodotto alla sua categoria. Normalmente dovrebbe già essere così. Tuttavia, non si sa come sia stato creato questo prodotto e se sia stato associato alla sua categoria. Pertanto, per evitare qualsiasi problema (per gestire l’entità [Produit], JPA necessita che questa faccia riferimento all’entità [Categorie] a cui è collegata), effettuiamo noi stessi tale collegamento.
Confrontando questo codice con quello della classe [DaoProduit] dell’implementazione Spring JDBC (cfr. paragrafo 4.9) si può notare che la libreria Spring Data JPA semplifica enormemente la scrittura del livello [DAO].
6.4.4.4. La classe [DaoProduit]
![]() |
La classe [DaoProduit] implementa l’interfaccia [IDao<Produit>] nel modo seguente:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> getAllShortEntities() {
try {
return setShortProduitsType(produitsRepository.getAllShortProduits());
} catch (Exception e) {
throw new DaoException(102, e, simpleClassName);
}
}
private List<Produit> setShortProduitsType(List<Produit> produits) {
for (Produit produit : produits) {
produit.setEntityType(EntityType.PROXY);
}
return produits;
}
@Override
public List<Produit> getAllLongEntities() {
try {
return produitsRepository.getAllLongProduits();
} catch (Exception e) {
throw new DaoException(117, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
} catch (Exception e) {
throw new DaoException(104, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
} catch (Exception e) {
throw new DaoException(105, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
for (Produit produit : produits) {
Categorie categorie = produit.getCategorie();
if (categorie != null) {
produit.setCategorie(categorie);
produit.setIdCategorie(categorie.getId());
}
}
return produits;
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// si ripristina (se necessario) il collegamento tra un prodotto e la sua categoria
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// si salvano i prodotti
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
produitsRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(113, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
produitsRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(118, e, simpleClassName);
}
}
}
Il codice è analogo a quello della classe [DaoCategorie]:
- per le versioni estese delle categorie, dai test emerge che il campo [idCategorie] dei prodotti non è compilato. Il metodo [linkLongProduitsToCategories] alle righe 96-105 risolve questo problema;
- il metodo [saveEntities] alle righe 108-121 inserisce nuovi prodotti o modifica quelli esistenti. Il livello JPA richiede che ogni entità [Produit] sia collegata a un'entità [Categorie]. Poiché non sappiamo se l'utente lo abbia fatto, lo facciamo noi stessi alle righe 110-113. È sufficiente collegare il [Produit] a un'entità [Categorie] la cui chiave primaria sia uguale al campo [idCategorie] del [Produit]. Durante i test, si nota che si verifica un errore se si inserisce null come versione della categoria. Pertanto, qui gli si assegna il valore 0, ma è possibile inserire qualsiasi valore. A parte la chiave primaria, nessun campo dell’entità [Categorie] è necessario al livello JPA per inserire/modificare un’entità [Produit];
6.4.5. Il livello di test
![]() |
![]() |
I test sopra riportati sono identici a quelli dell'implementazione Spring JDBC. Se necessario, si rimanda alle seguenti pagine:
- [JUnitTestCheckArguments]: paragrafo 4.11.1;
- [JUnitTestDao]: paragrafo 4.11.2;
- [JUnitTestPushTheLimits]: paragrafo 4.11.3;
Utilizziamo le seguenti configurazioni di esecuzione:
![]() | ![]() |
![]() | ![]() |
I risultati ottenuti nei vari test sono i seguenti:
![]() | ![]() |
![]() |
In [1], il test [JUnitTestPushTheLimits] con l'implementazione Spring Data JPA Hibernate e in [2], con l'implementazione Spring JDBC. Si nota che quest'ultima è più performante. Si giunge quindi a una prima conclusione: è decisamente più facile sviluppare un livello [DAO] con Spring Data JPA, ma è meno performante di un'implementazione Spring JDBC.
Il test [JUnitTestProxies] è un test fittizio JUnit. Serve a mostrare il comportamento di ciascuna implementazione JPA rispetto ai proxy, ovvero alle versioni abbreviate delle entità:
package spring.data.tests;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
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.data.config.AppConfig;
import spring.data.dao.IDao;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
// livello [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// si pulisce il database prima di ogni test
log("Vidage de la base de données", 1);
// si svuota la tabella [CATEGORIES] e, di conseguenza, la tabella [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// si compilano le tabelle
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);
categorie.setProduits(new ArrayList<Produit>());
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);
}
// si aggiunge la categoria; di conseguenza, anche i prodotti verranno
// inseriti
daoCategorie.saveEntities(categories);
// risultato
return categories;
}
@Test
public void getShortCategoriesByName1() {
// compilazione
fill(1, 1);
// test
log("getShortCategoriesByName1", 1);
Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getShortProduitsByName1() {
// compilazione
fill(1, 1);
// test
log("getShortProduitsByName1", 1);
Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongCategoriesByName1() {
// riempimento
fill(1, 1);
// test
log("getLongCategoriesByName1", 1);
Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongProduitsByName1() {
// riempimento
fill(1, 1);
// test
log("getLongProduitsByName1", 1);
Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
private void log(String message, int mode) {
// visualizza messaggio
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
I risultati ottenuti sono i seguenti:
Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]
Si nota qui che, quando si accede al campo [Categorie.produits] di una categoria di tipo PROXY e al campo [Produit.categorie] di un prodotto di tipo PROXY, si verifica un'eccezione di tipo [org.hibernate.LazyInitializationException] in entrambi i casi (righe 7 e 17).



































