6. Spring Data JPA Hibernate
6.1. Introduzione
Utilizzeremo il database [dbproduitscategories] gestito dal progetto [spring-jdbc-04] e implementeremo le due interfacce [IDao<Category>, IDao<Product>] definite in quel progetto. Questo ci consentirà di fare diverse cose:
- confrontare il codice di implementazione;
- utilizzare lo stesso livello di test;
- confrontare le prestazioni delle due implementazioni;
![]() |
- il livello [JDBC] è implementato dal progetto [mysql-config-jdbc] descritto nella Sezione 3.3;
Passeremo ora agli altri livelli.
6.2. Configurazione dell'ambiente di lavoro
Utilizzando STS, importare il progetto [mysql-config-jpa-hibernate] [1] che si trova nella cartella [<examples>/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.
Successivamente, importare il progetto [spring-jpa-generic] [1] situato nella cartella [<examples>/spring-database-generic/spring-jpa] [2]:
![]() |
Una volta fatto ciò, aggiorna l'ambiente Maven (Alt-F5) per tutti i progetti in [Package Explorer]:
![]() |
Quindi, per verificare l'ambiente di lavoro, esegui la configurazione di build denominata [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Questa configurazione esegue il test [JUnitTestDao]. Il test dovrebbe avere esito positivo:
![]() |
6.3. Il progetto di configurazione del livello JPA
![]() |
Lo scopo di questo progetto è configurare il livello JPA dell'architettura illustrata 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>
<!-- dépendances variables ********************************************** -->
<!-- JPA provider -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- dépendances constantes ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- configuration JDBC inherited -->
<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 per le altre implementazioni JPA (Eclipselink e OpenJpa) useranno questo stesso artefatto. Ciò significa che solo uno di questi progetti può essere attivo in un dato momento. Dovresti quindi evitare di averli tutti presenti in [Package Explorer]. Ne serve solo uno;
- righe 10–14: il progetto Maven principale che specifica le versioni della maggior parte delle dipendenze richieste dal 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 sul progetto di configurazione del livello JDBC, che definisce, tra le altre cose, il driver JDBC per il DBMS in uso e i dettagli di connessione al database;
- righe 35–39: il progetto di configurazione del livello JDBC include la libreria [Spring JDBC], che qui viene sostituita dalla libreria [Spring Data JPA]. Pertanto, specifichiamo di non includerla nelle dipendenze del progetto. Tuttavia, se rimane, ciò non causa alcun errore;
Alla fine, le dipendenze del progetto sono le seguenti:
![]() |
6.3.2. Configurazione di 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 {
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// JPA entity packages
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// 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;
}
// 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();
}
// Transaction manager
@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]. Questi sono i 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: specifica il DBMS collegato a Hibernate. Questa configurazione è importante. Consente a Hibernate di utilizzare il dialetto SQL del DBMS MySQL, comprese le sue caratteristiche proprietarie. Inoltre, lo informa sui tipi SQL e sugli oggetti DBMS che potrà utilizzare. È questa capacità dell'implementazione JPA di adattarsi a un DBMS specifico che le conferisce un'elevata portabilità tra i DBMS;
- riga 28: Hibernate può generare o meno le tabelle per il database di destinazione dalle entità JPA che trova. Questa generazione avviene solo se le tabelle sono assenti. Se esistono già, non viene fatto nulla. Useremo questa capacità di generare tabelle quando mostreremo come sono stati creati gli script di generazione SQL per i vari database utilizzati in questo documento;
- riga 33: il pacchetto contenente le entità JPA per il database [dbproduitscategories];
- Righe 36–49: L'origine dati [tomcat-jdbc] collegata al database [dbproduitscategories];
- righe 52–60: il bean denominato [entityManagerFactory] (deve essere denominato in questo modo) è il bean che creerà l'oggetto [EntityManager], che gestisce il contesto di persistenza JPA. Tutte le operazioni JPA passano attraverso di esso. Poiché stiamo utilizzando [Spring Data JPA], non useremo mai questo oggetto noi stessi. Tuttavia, dobbiamo configurarlo. Deve conoscere quanto segue:
- l'implementazione JPA utilizzata (riga 55);
- la fonte dati utilizzata (riga 57);
- le entità JPA per questa origine (riga 56);
- riga 58: inizializza l'EntityManager con queste informazioni;
- riga 59: restituisce il singleton [entityManagerFactory];
- righe 63–68: definiscono il gestore delle transazioni. Deve essere denominato [transactionManager];
- riga 65: viene creato un gestore delle transazioni JPA;
- riga 66: viene collegato alla fonte dati della riga 37 tramite il bean [entityManagerFactory] (righe 53 e 57);
Solo il bean nelle righe 23–30 dipende dall'implementazione JPA utilizzata. Gli altri bean si basano quindi su di esso.
6.3.3. Entità nel livello [JPA]
![]() |
![]() |
Il database di destinazione è il database [dbproduitscategories] con le sue due tabelle [CATEGORIES] e [PRODUITS]. Abbiamo visto che contiene anche altre tre tabelle [USERS, ROLES, USERS_ROLES] che saranno utilizzate per proteggere il servizio web da distribuire sul web. Per ora ignoreremo queste tabelle. A titolo di promemoria, ecco la struttura delle tabelle [CATEGORIES] e [PRODUCTS]:
La tabella [PRODUCTS] è la seguente:
![]() |
- [ID]: la chiave primaria autoincrementale della tabella [2];
- [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 sottolineare 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;
Descriveremo ora le entità JPA [Product] e [Category], che corrispondono alle tabelle [PRODUCTS] e [CATEGORIES].
![]() |
6.3.3.1. L'interfaccia [AbstractCoreEntity]
L'interfaccia [AbstractCoreEntity] è implementata dalle entità JPA [Category] e [Product]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getters and setters for [id], [version], [entityType] fields
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, elenca semplicemente i metodi per la lettura e la scrittura dei campi [id], [version] e [entityType] di queste entità. Il ruolo del campo [entityType] verrà spiegato più avanti;
6.3.3.2. L'entità JPA [Product]
La classe [Product] è l'entità JPA associata a una riga nella tabella [PRODUCTS]:
![]() |
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 {
// properties
@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();
// properties
@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;
// the category
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// manufacturers
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;
}
// 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);
}
// ------------------------------------------------------------
// redefine [equals] and [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 and setters
...
public void setCategorie(Categorie categorie) {
// entity type
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 [Product] un'entità gestita dal livello [JPA]. È anche possibile scrivere [@Entity(name="MyProduct")], che assegna all'entità il nome [MyProduct]. In assenza di questa informazione, il nome dell'entità è il nome della classe, in questo caso [Product]. Questa convenzione di denominazione diventa necessaria quando tra le entità ci sono due classi di pacchetti diversi che condividono lo stesso nome;
- riga 22: l'annotazione [@Table(name = "PRODUCTS")] indica che la classe [Product] è la rappresentazione a oggetti di una riga nella tabella [PRODUCTS] nel database;
- riga 23: il nome del filtro JSON da applicare all'entità. Vedremo che la proprietà [categorie] alla riga 58 non è sempre disponibile. Deve quindi essere esclusa dalla rappresentazione JSON dell'oggetto. Per farlo, abbiamo bisogno di un filtro. Useremo quindi un filtro denominato [jsonFilterCategorie] per specificare se vogliamo o meno la proprietà [categorie];
- riga 26: l'annotazione [@Id] rende il campo annotato il campo associato alla chiave primaria della tabella alla riga 19;
- riga 27: l'annotazione [@GeneratedValue(strategy = GenerationType.IDENTITY)] imposta la modalità di generazione automatica per la chiave primaria nella tabella [PRODUITS]. L'attributo [strategy] determina questa impostazione. Esistono diverse modalità:

La strategia [IDENTITY] non è disponibile per tutti i DBMS. Tra i sei DBMS testati, era disponibile per [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 garantire la portabilità tra le implementazioni JPA, non si dovrebbe utilizzare la strategia [AUTO], poiché lascia la scelta della strategia di generazione della chiave primaria all'implementazione JPA. 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 per impostazione predefinita una tabella denominata [SEQUENCE] che deve essere interrogata per recuperare 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"] imposta il nome della colonna nella tabella [PRODUCTS] da associare al campo [id];
- riga 29: per la chiave primaria viene utilizzato il tipo [Long] anziché [long]. Questo perché le chiavi primarie [null] hanno un significato specifico per JPA. Pertanto, è preferibile 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à viene modificata. Questo numero 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 diventa quindi V1+1. A sua volta, U2 modifica E e tenta di salvare questa modifica nel database: riceverà un'eccezione perché la sua versione (V1) differisce da quella nel database (V1+1);
- Riga 36: il tipo di entità. Ce ne saranno due: POJO e PROXY. Per impostazione predefinita, l'istanza Product sarà un POJO (Plain Old Java Object). In alcuni casi, le istanze [Product] recuperate dal database saranno di tipo [PROXY]. Questo accadrà quando la proprietà [Category] alla riga 58 non è stata inizializzata con una categoria a causa dell'attributo [fetch = FetchType.LAZY] alla riga 56. In questo caso, le implementazioni JPA da testare differiscono:
- [Hibernate, OpenJPA]: l'accesso alla categoria di un prodotto di tipo [PROXY] genera un'eccezione. Hibernate utilizza il termine "proxy" per riferirsi a un'istanza JPA ottenuta in modalità [LAZY]. Questo è il motivo per cui ho utilizzato questo termine per riferirmi a questo tipo di entità;
- [EclipseLink]: l'accesso alla categoria di un prodotto di tipo [PROXY] attiva una ricerca di quella categoria nel database e non viene generata alcuna eccezione;
Poiché volevo un livello di test indipendente dall'implementazione JPA utilizzata, avevo bisogno di 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 del DBMS;
- riga 40: la classe [Product] genera una [ProxyException] che richiede il nome della classe;
- riga 38: come prima, indichiamo che l'implementazione JPA deve ignorare questo campo;
- riga 39: l'annotazione [@JsonIgnore] indica che il serializzatore/deserializzatore JSON per un'istanza [Product] deve ignorare questo campo;
- riga 43: l'annotazione [@Column] associa il campo [name] alla colonna [NAME] nella tabella [PRODUCTS]. 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 qui. Gli attributi [unique = true, length = 30, nullable = false] vengono utilizzati solo quando l'implementazione JPA genera la tabella [CATEGORIES] dall'entità [Product]. Essi saranno tradotti negli attributi SQL [UNIQUE, VARCHAR(30), NOT NULL], che garantiscono che la colonna [NAME] avrà al massimo 30 caratteri, sarà unica nella tabella e non potrà avere il valore NULL;
- righe 46–47: il campo [idCategorie] è collegato alla colonna [CATEGORIE_ID]. Torneremo su questi attributi più avanti;
- righe 49–50: il campo [price] è associato alla colonna [PRICE];
- 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 a cui fa riferimento l'annotazione alla riga 57 [@JoinColumn(name = "CATEGORIE_ID")] è una chiave esterna dalla tabella [PRODUITS] dell'entità [Product] alla tabella [CATEGORIES] associata all'entità alla riga 58. Questa annotazione deve essere applicata a un'entità JPA. Pertanto, la classe alla riga 58 deve essere un'entità JPA;
- Riga 56: L'annotazione [fetch = FetchType.LAZY] specifica che quando un prodotto viene recuperato dalla tabella [PRODUCTS], la sua categoria (riga 58) non viene recuperata immediatamente (caricamento pigro). Viene quindi recuperata durante la prima chiamata al metodo [getCategory]. Per ottenere questo risultato, in fase di esecuzione, il livello JPA potenzia il metodo iniziale [getCategorie] (che restituisce semplicemente il campo categoria) effettuando una chiamata al DBMS per recuperare la categoria — una tecnica nota come "proxying". Le implementazioni JPA differiscono nel modo in cui gestiscono questa funzionalità, come menzionato in precedenza. Questo attributo non è obbligatorio. L'implementazione JPA utilizzata può 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] nella tabella [PRODUITS] viene aggiornata automaticamente quando un prodotto viene inserito o aggiornato. Riceve il valore di [categorie.getId()], dove [categorie] è il campo alla riga 58. La specifica JPA richiede che questa colonna di join non possa essere aggiornata con nessun altro mezzo. Impone quindi gli attributi [insertable = false, updatable = false] alla riga 46, che assicurano che la colonna [CATEGORIE_ID] (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à [Product] è definita come 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] nelle tre implementazioni JPA [Hibernate, EclipseLink, OpenJpa]. Per un'entità [Product] di tipo [PROXY], impediremo che il valore del campo [category] venga modificato. 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 la trattazione di questa entità, va notato che le annotazioni e i loro attributi vengono utilizzati in due casi distinti:
- per creare tabelle di database;
- per interrogarle. In questo caso, l'implementazione JPA si aspetta di trovare le tabelle esattamente come le avrebbe generate essa stessa. Pertanto, non possiamo associare una tabella [PRODUCTS] qualsiasi alla precedente entità [Product]. Deve avere almeno (potrebbe averne altre) le caratteristiche della tabella [PRODUCTS] che JPA avrebbe generato. Quando si lavora con JPA, l'approccio ideale è quello di iniziare con un database vuoto in cui JPA genera le tabelle. Discuteremo di questa generazione un po' più avanti. Lo script SQL fornito per il DBMS MySQL è stato generato dalle tabelle generate da JPA.
Tutti gli attributi dell'entità [Product] vengono utilizzati per generare la tabella [PRODUCTS]. Una volta fatto ciò, gli attributi di generazione come [unique = true, length = 30, nullable = false] non vengono più utilizzati durante l'interrogazione delle tabelle.
6.3.3.3. L'entità JPA [Category]
La classe [Category] è un'entità JPA associata a una riga nella 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();
// properties
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// related products
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// manufacturers
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;
}
// signature
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// methods
public void addProduit(Produit produit) {
// entity type
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// 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);
}
}
// ------------------------------------------------------------
// redefine [equals] and [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 and setters
...
}
- riga 24: la classe è un'entità JPA;
- riga 25: associata alla tabella [CATEGORIES];
- riga 26: la rappresentazione JSON dell'entità [Category] è controllata dal filtro denominato [jsonFilterCategory]. Questo deve essere configurato prima di qualsiasi richiesta di una rappresentazione JSON dell'entità. Il filtro [jsonFilterCategory] verrà utilizzato per determinare se includere o meno il campo [products] della riga 40 nella rappresentazione JSON dell'entità [Category];
- righe 29–32: il campo [id] è associato alla chiave primaria [ID] della tabella [CATEGORIES]. La modalità di generazione selezionata è [IDENTITY], che corrisponde a [AUTO_INCREMENT] per MySQL;
- righe 34–36: il campo [version] è collegato alla colonna [VERSIONING] nella tabella [CATEGORIES];
- righe 38-39: il tipo dell'entità [Categorie];
- righe 41–43: il nome abbreviato della classe [Categorie];
- Righe 46–47: Il campo [name] è collegato alla colonna [NAME] nella tabella [CATEGORIES]. Gli assegniamo gli attributi JPA [unique = true, length = 30, nullable = false] in modo che, quando viene generata la tabella [CATEGORIES], la colonna [NAME] abbia gli attributi SQL [UNIQUE, VARCHAR(30), NOT NULL];
- righe 50-51: i prodotti che appartengono alla categoria;
- riga 50: l'annotazione [@OneToMany] rappresenta la relazione inversa della relazione [@ManyToOne] che abbiamo incontrato nell'entità [Product]. L'attributo [mappedBy = "category"] specifica il campo nell'entità [Product] annotato dalla relazione inversa [@ManyToOne]. L'attributo [cascade = { CascadeType.ALL }] specifica che le operazioni (persist, merge, remove) eseguite su un'entità @Entity [Category] devono propagarsi a cascata ai [products] nella 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 una categoria viene recuperata dalla tabella [CATEGORIES], i suoi prodotti non vengono recuperati immediatamente. Vengono recuperati durante la prima chiamata al metodo [getProduits]. Per ottenere questo risultato, in fase di esecuzione, il livello JPA potenzia il metodo [getProduits] iniziale (che restituisce semplicemente il campo products) effettuando una chiamata al DBMS per recuperare i prodotti della categoria. Questo attributo è obbligatorio. L'implementazione JPA non può ignorarlo. Poiché la proprietà [products] può essere inizializzata o meno, abbiamo introdotto il filtro JSON alla riga 26, che ci permette di specificare se vogliamo o meno questa proprietà e il tipo di entità alla riga 39;
- righe 71–88: il metodo [addProduct] consente di aggiungere un prodotto alla categoria;
- righe 73–76: per standardizzare la gestione dei proxy nelle diverse implementazioni JPA, abbiamo deciso che i prodotti non possono essere aggiunti a un'entità [Category] di tipo PROXY;
- righe 92–112: due entità [Category] sono considerate uguali se hanno la stessa chiave primaria [id];
6.3.4. Il file [persistence.xml]
![]() |
Le applicazioni JPA devono definire alcune proprietà del provider JPA utilizzato, nonché le entità JPA da utilizzare, in un file [META-INF/persistence.xml] situato nel classpath dell'applicazione. In precedenza, è stato collocato nella cartella [src/main/resources], che di fatto fa parte del classpath di un progetto Eclipse. Quando si utilizza JPA in combinazione con 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 gestisce 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 utilizzando 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 [attributo name], supporta un tipo di transazione [attributo transaction-type], ha proprietà e definisce le entità associate alle tabelle del database gestite dall'unità di persistenza. Qui, poiché l'accesso al database sarà gestito da [Spring JPA Hibernate], queste ultime due informazioni possono essere collocate altrove. Esistono due tipi di transazioni:
- [RESOURCE_LOCAL]: le transazioni sono gestite dall'applicazione stessa. È il caso qui, dove Spring gestirà le transazioni;
- [JTA] (Java Transaction API): il contenitore EJB (Enterprise Java Bean) che esegue l'applicazione gestisce automaticamente le transazioni sulla base delle annotazioni Java presenti nel codice. In questo caso non stiamo utilizzando 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]
Ricapitoliamo ciò che vogliamo fare. Vogliamo implementare la seguente architettura:
![]() |
in cui il livello [DAO] implementerebbe l'interfaccia [IDao<Product>, IDao<Category>] studiata nel Capitolo 4. L'obiettivo è 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] discusso nella Sezione 3.3;
- il livello [JPA] è implementato dal progetto [mysql-config-jpa-hibernate] descritto nella Sezione 6.3;
Il progetto [spring-jpa-generic] gestisce 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>
<!-- configuration JPA of 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, ovvero il progetto che configura il livello [JPA] dell'applicazione, che abbiamo appena esaminato. Si tratta di un'applicazione generica:
- modifichiamo il DBMS modificando il progetto di configurazione del livello [JDBC];
- modifichiamo 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] viene utilizzata per designare i pacchetti contenenti le interfacce [CrudRepository] di Spring Data. Questo 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 scansionato alla ricerca di componenti Spring. Verranno trovati i componenti [DaoCategory] e [DaoProduct];
- riga 13: vengono importati i bean dalla classe di configurazione [ConfigJpa]. Questi includono il bean per l'implementazione JPA in uso (Hibernate, Eclipselink, OpenJpa), l'origine dati da utilizzare, l'EntityManager che gestirà le operazioni JPA e il gestore delle transazioni;
6.4.3. Il livello [Spring Data]
![]() |
![]() |
6.4.3.1. L'interfaccia [CategoriesRepository]
L'interfaccia [CategoriesRepository] gestisce l'accesso 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> {
// categorie avec ses produits
@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 spiegata nella Sezione 5.1.3. Ricordiamo che:
- il primo tipo di parametro dell'interfaccia è l'entità JPA gestita per le operazioni CRUD (findOne, findAll, save, delete, deleteAll),
- il secondo tipo di parametro dell'interfaccia è la chiave primaria dell'entità JPA, in questo caso un numero intero [Long];
I metodi dell'interfaccia sono implementati utilizzando query JPQL (Java Persistence Query Language). Queste query hanno come oggetto le entità JPA. In una query di questo tipo:
- le tabelle vengono sostituite dalle entità JPA associate;
- le colonne sono sostituite dai campi delle entità JPA utilizzate nella query;
Prendiamo l'esempio delle righe 31–32: il metodo alla riga 32 recupera tutte le categorie dal database nella loro forma abbreviata. È implementato dalla query JPQL (Java Persistence Query Language) alla riga 31, che assomiglia molto alla sua controparte SQL. Per una comprensione più approfondita di JPQL, consultare [rif2] (vedere la sezione 1.2).
I metodi dell'interfaccia [CategoriesRepository] sono i seguenti:
- Righe 13-14: Il metodo [getLongCategoryById] restituisce la versione estesa di una categoria indicata dal suo [id] (chiave primaria), ovvero la categoria insieme ai relativi prodotti. Ricordiamo che nell’entità [Category], il campo [products] aveva l’attributo [fetch = FetchType.LAZY] (caricamento differito). Nella query JPQL, forziamo il caricamento dei prodotti utilizzando la parola chiave [fetch]. Il parametro ?1 della query verrà sostituito in fase di esecuzione dal valore del primo parametro del metodo alla riga 12, ovvero il parametro [Long id];
- righe 16–17: il metodo [getLongCategoryByName] restituisce la versione lunga di una categoria a cui si fa riferimento tramite il suo nome [name];
- righe 19–20: il metodo [getShortCategoriesByName] restituisce le versioni brevi delle categorie a cui si fa riferimento tramite i loro nomi. Il campo [products] di queste categorie non è nullo. Contiene un riferimento a un proxy (una classe creata dall'implementazione JPA) il cui ruolo è quello di recuperare i prodotti nella categoria quando viene chiamato. Chiamarlo al di fuori del contesto di persistenza JPA genera un'eccezione (Hibernate e OpenJPA, ma non EclipseLink). Per questo motivo, non useremo il campo [products] della versione breve di una categoria;
- righe 22–23: il metodo [getShortCategoriesById] restituisce le versioni abbreviate delle categorie a cui si fa riferimento tramite le loro chiavi primarie [id];
- righe 25–26: il metodo [getLongCategoriesById] restituisce le versioni lunghe delle categorie a cui si fa riferimento tramite le loro chiavi primarie [id];
- righe [28-29]: il metodo [getLongCategoriesByName] restituisce le versioni lunghe delle categorie a cui si fa riferimento tramite i 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 supportano la stessa sintassi JPQL. Pertanto, la seguente sintassi è accettata da Hibernate ed 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 [ProductsRepository]
L'interfaccia [ProductsRepository] gestisce l'accesso alla tabella [PRODUCTS]:
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 produit avec sa catégorie
@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 [getLongProductById] restituisce la versione completa di un prodotto identificato dalla sua chiave primaria [id], inclusa la categoria. Ricordiamo che nell'entità [Product], il campo [category] aveva l'attributo [fetch = FetchType.LAZY] (caricamento differito). Nella query JPQL, forziamo il caricamento della categoria utilizzando la parola chiave [fetch];
- righe 18-19: il metodo [getLongProductByName] restituisce la versione lunga 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 [category] non è nullo. Contiene un riferimento a un proxy generato dall'implementazione JPA, che, se chiamato, recupererà la categoria del prodotto. Questa chiamata può essere effettuata solo all'interno del contesto di persistenza JPA. Effettuarla altrove causa un'eccezione (Hibernate e OpenJPA, ma non EclipseLink). Pertanto, nel livello [DAO] o altrove, non useremo il campo [category] 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. Questo ci permette di recuperare successivamente questa categoria dal 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 lunga dei prodotti identificati dalle loro chiavi primarie;
- righe 30-31: il metodo [getLongProductsByName] restituisce la versione estesa dei prodotti identificati dai loro nomi;
- righe 33-34: il metodo [getAllLongProducts] restituisce la versione completa di tutti i prodotti;
- righe 36-37: il metodo [getAllShortProducts] restituisce la versione breve di tutti i prodotti;
Queste interfacce saranno implementate da classi generate dall'implementazione JPA in fase di esecuzione. Tali classi sono chiamate classi [proxy]. Per impostazione predefinita, i metodi dell'interfaccia [CrudRepository] vengono eseguiti all'interno di una transazione. Il fatto che le interfacce [ProductsRepository] e [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à trattata nell'implementazione del livello [DAO] utilizzando Spring JDBC (vedi sezione 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
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);
}
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 l'accesso alla tabella [PRODUITS];
- la classe [DaoCategorie], che implementa l'interfaccia [IDao<Categorie>] e gestisce l'accesso alla tabella [CATEGORIES];
Il suo codice è quello descritto nella Sezione 4.8, con la seguente piccola differenza: nessun metodo ha l'attributo [@Transactional], che fa sì che il metodo venga eseguito all'interno di una transazione. Qui, sfruttiamo il fatto che le interfacce [CrudRepository] di Spring Data vengono eseguite all'interno di una transazione per impostazione predefinita.
6.4.4.3. La classe [DaoCategorie]
![]() |
La classe [DaoCategorie] implementa l'interfaccia [IDao<Categorie>] come segue:
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 significa che implementa l'interfaccia [IDao<Categorie>];
- righe 20–24: iniezione dei riferimenti alle due interfacce [CrudRepository] da [Spring Data]. Questa iniezione avviene quando gli oggetti Spring vengono istanziati, in genere all'inizio dell'esecuzione del progetto Spring;
- Tutti i metodi della classe delegano il lavoro ai metodi con lo stesso nome nelle interfacce [CrudRepository];
- Tutti i metodi che restituiscono entità nella loro forma abbreviata lo indicano impostando il tipo di entità su [EntityType.PROXY] (righe 29, 63, 72);
Il metodo [saveEntities] merita una spiegazione:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// on note les produits qui vont être insérés
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);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories;
}
- riga 2: le categorie passate come parametri sono sia categorie da inserire [id==null] che categorie da aggiornare [id!=null];
- riga 20: salviamo le categorie utilizzando il metodo [categoriesRepository.save(entities)]. Durante il test, osserviamo che il campo [idCategorie] dei prodotti salvati (id==null) non viene popolato. Per risolvere questo problema, nelle righe 4–17 annotiamo i prodotti da inserire e, una volta salvati, popoliamo il loro campo [idCategorie] (righe 25–27);
- righe 5–17: iteriamo attraverso l'elenco delle categorie;
- righe 8–16: per ogni categoria, percorriamo il suo elenco di prodotti. Qui sta la 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, contenendo quindi un riferimento a un metodo proxy nel campo [products]. Usarlo con Hibernate causa quindi un'eccezione, perché la categoria utilizzata non si trova più nel contesto di persistenza JPA, che è stato chiuso alla fine della transazione per il metodo che ha recuperato le versioni abbreviate delle categorie. Utilizziamo quindi il campo [EntityType] dell'entità [Category] alla riga 8 per determinare se possiamo o meno accedere all'elenco dei prodotti della categoria;
- riga 14: colleghiamo il prodotto alla sua categoria. Normalmente, questo dovrebbe già essere il caso. Ma non sappiamo come sia stato creato questo prodotto né se sia stato collegato alla sua categoria. Quindi, per evitare eventuali problemi di " " (per gestire l'entità [Product], JPA richiede che essa faccia riferimento all'entità [Category] a cui è collegata), stabiliamo noi stessi questo collegamento.
Confrontando questo codice con quello della classe [DaoProduit] nell'implementazione Spring JDBC (vedi sezione 4.9), possiamo notare che la libreria Spring Data JPA semplifica notevolmente la scrittura del livello [DAO].
6.4.4.4. La classe [ProductDao]
![]() |
La classe [DaoProduct] implementa l'interfaccia [IDao<Product>] come segue:
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) {
// re-establish (if necessary) the link between a product and its category
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// we persist products
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 è simile a quello della classe [DaoCategorie]:
- per le versioni lunghe delle categorie, i test mostrano che il campo [idCategorie] dei prodotti non viene popolato. 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à [Product] sia collegata a un'entità [Category]. Poiché non sappiamo se l'utente lo abbia fatto, lo facciamo noi stessi alle righe 110–113. Tutto ciò che dobbiamo fare è collegare [Product] a un'entità [Category] la cui chiave primaria corrisponda al campo [idCategory] di [Product]. Durante il test, abbiamo riscontrato che si verifica un errore se impostiamo la versione della categoria su null. Quindi qui la impostiamo su 0, ma possiamo impostarla su qualsiasi valore desideriamo. A parte la chiave primaria, nessun campo dell'entità [Category] è richiesto dal livello JPA per inserire o aggiornare un'entità [Product];
6.4.5. Il livello di test
![]() |
![]() |
I test sopra riportati sono identici a quelli dell'implementazione JDBC di Spring. Se necessario, consultare le pagine seguenti:
- [JUnitTestCheckArguments]: sezione 4.11.1;
- [JUnitTestDao]: sezione 4.11.2;
- [JUnitTestPushTheLimits]: sezione 4.11.3;
Utilizziamo le seguenti configurazioni di test:
![]() | ![]() |
![]() | ![]() |
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 può notare che quest'ultima offre prestazioni migliori. Giungiamo quindi a una conclusione iniziale: è significativamente più facile sviluppare un livello [DAO] con Spring Data JPA, ma le prestazioni sono inferiori rispetto a un'implementazione Spring JDBC.
Il test [JUnitTestProxies] è un test JUnit fittizio. Serve a dimostrare come si comporta ciascuna implementazione JPA quando gestisce i proxy, ovvero le 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 {
// layer [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@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();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
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, 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);
}
// adding the category - by cascading the products will also be
// inserted
daoCategorie.saveEntities(categories);
// result
return categories;
}
@Test
public void getShortCategoriesByName1() {
// filling
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() {
// filling
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() {
// filling
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() {
// filling
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) {
// poster message
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 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]
Qui possiamo vedere 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, viene generata un'eccezione [org.hibernate.LazyInitializationException] in entrambi i casi (righe 7 e 17).



































