6. Spring Data JPA Hibernate
6.1. Introduction
We gaan de database [dbproduitscategories], die wordt beheerd door het project [spring-jdbc-04], gebruiken en we gaan de twee interfaces [IDao<Categorie>, IDao<Produit>] implementeren die in dit project zijn gedefinieerd. Dit biedt ons verschillende mogelijkheden:
- de implementatiecodes vergelijken;
- dezelfde testlaag gebruiken;
- de prestaties van twee implementaties vergelijken;
![]() |
- de laag [JDBC] wordt geïmplementeerd door het project [mysql-config-jdbc] dat in paragraaf 3.3 is besproken;
We gaan nu verder met de andere lagen.
6.2. Het opzetten van de werkomgeving
Importeer met STS het project [mysl-config-jpa-hibernate] [1] dat zich in de map [<exemples>/spring-database-config/mysql/eclipse] [2] bevindt:
![]() |
Dit project configureert de laag [Spring JPA Hibernate] van het project. Elke implementatie JPA heeft zijn eigen configuratieproject.
Importeer vervolgens het project [spring-jpa-generic] [1] dat zich in de map [<exemples>/spring-database-generic/spring-jpa] [2] bevindt:
![]() |
Zodra dit is gebeurd, reset je de Maven-omgeving (Alt-F5) van alle projecten in [Package Explorer]:
![]() |
Voer vervolgens, om de werkomgeving te controleren, de uitvoerconfiguratie met de naam [spring-jpa-generic-JUnitTestDao-hibernate] uit:
![]() |
Deze configuratie voert de test [JUnitTestDao] uit. Deze test moet slagen:
![]() |
6.3. Het configuratieproject van de laag JPA
![]() |
Dit project heeft als doel de laag JPA van de onderstaande architectuur te configureren:
![]() |
6.3.1. Maven-configuratie
Het project is een Maven-project en wordt geconfigureerd door het volgende bestand [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>
<!-- variabele afhankelijkheden ********************************************** -->
<!-- JPA-provider -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- constante afhankelijkheden ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- overgenomen configuratie van 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>
- regels 5-7: het door dit project gegenereerde Maven-artefact. De configuratieprojecten van de andere implementaties JPA (Eclipselink en OpenJpa) zullen hetzelfde artefact gebruiken. Dit betekent dat er op een bepaald moment slechts één van deze projecten actief kan zijn. Het is dus belangrijk om te voorkomen dat ze allemaal in [Package Explorer] aanwezig zijn. Er is er maar één nodig;
- regels 10-14: het bovenliggende Maven-project dat de versie vastlegt van de meeste afhankelijkheden die nodig zijn voor het project;
- regels 19-22: de Hibernate-bibliotheek;
- regels 25-28: de Spring Data-bibliotheek;
- regels 32-34: het configuratieproject van de laag JPA is gebaseerd op dat van de laag JDBC, dat onder andere de driver JDBC van de gebruikte SGBD en de gegevens van de te gebruiken database definieert;
- regels 35-39: het configuratieproject van de laag JDBC bevat de bibliotheek [Spring JDBC], die hier wordt vervangen door de bibliotheek [Spring Data JPA]. Daarom wordt aangegeven deze niet op te nemen in de afhankelijkheden van het project. Als deze toch blijft staan, leidt dit echter niet tot fouten;
Uiteindelijk zijn de afhankelijkheden van het project als volgt:
![]() |
6.3.2. Spring-configuratie
![]() |
De klasse [ConfigJpa] configureert het Spring-project:
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 {
// de provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// entiteitspakketten JPA
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// gegevensbron
@Bean
public DataSource dataSource() {
// gegevensbron TomcatJdbc
DataSource dataSource = new DataSource();
// toegangscontrole JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// aanvankelijk geopende verbindingen
dataSource.setInitialSize(5);
// resultaat
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();
}
// Transactiemanager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- regel 18: de klasse is een Spring-configuratieklasse;
- regel 19: deze importeert de beans die zijn gedefinieerd door de configuratieklasse [ConfigJdbc], die is gebruikt om het Spring-project [mysql-config-jdbc] te configureren. Dit zijn de filters jSON;
- regels 23-30: definiëren de gebruikte implementatie JPA, in dit geval de Hibernate-implementatie (regel 25);
- regel 26: hiermee kan al dan niet worden aangegeven of de door de Hibernate-implementatie uitgevoerde bewerkingen SQL moeten worden weergegeven;
- regel 27: hier wordt aan Hibernate aangegeven welke SGBD is aangesloten. Deze configuratie is belangrijk. Hierdoor kan Hibernate het dialect SQL van SGBD MySQL gebruiken, inclusief de propriëtaire aspecten ervan. Bovendien krijgt het hierdoor informatie over de SQL-typen en de SGBD-objecten die het zal kunnen gebruiken. Het is juist dit vermogen van de implementatie JPA om zich aan te passen aan een specifieke SGBD dat haar een grote overdraagbaarheid tussen SGBD-en biedt;
- regel 28: Hibernate kan al dan niet de tabellen van de doeldatabase genereren op basis van de entiteiten JPA die het aantreft. Deze generatie vindt alleen plaats als de tabellen ontbreken. Als ze al aanwezig zijn, gebeurt er niets. We zullen gebruikmaken van deze mogelijkheid om tabellen te genereren wanneer we uitleggen hoe de SQL-scripts voor het genereren van de verschillende databases die in dit document worden gebruikt, tot stand zijn gekomen;
- regel 33: het pakket waarin de entiteiten JPA van de database [dbproduitscategories] zich bevinden;
- regels 36-49: de gegevensbron [tomcat-jdbc] die gekoppeld is aan de database [dbproduitscategories];
- regels 52-60: de bean met de naam [entityManagerFactory] (hij moet zo heten) is de bean die het object [EntityManager] aanmaakt, dat de persistentiecontext JPA beheert. Alle bewerkingen van JPA verlopen via deze bean. Door het gebruik van [Spring Data JPA] zullen we dit object zelf nooit gebruiken. We moeten het echter wel configureren. Het moet de volgende gegevens kennen:
- de gebruikte implementatie JPA (regel 55);
- de gebruikte gegevensbron (regel 57);
- de entiteiten JPA van deze bron (regel 56);
- regel 58: initialiseert de EntityManager met deze informatie;
- regel 59: retourneert het singleton [entityManagerFactory];
- regels 63-68: hierin wordt de transactiebeheerder gedefinieerd. Deze moet de naam [transactionManager] hebben;
- regel 65: er wordt een transactiebeheerder JPA aangemaakt;
- regel 66: deze wordt via de bean [entityManagerFactory] (regels 53 en 57) gekoppeld aan de gegevensbron van regel 37;
Alleen de bean uit de regels 23-30 is afhankelijk van de gebruikte implementatie JPA. De andere beans zijn vervolgens hiervan afhankelijk.
6.3.3. De entiteiten van de laag [JPA]
![]() |
![]() |
De doeldatabase is de database [dbproduitscategories] met de twee tabellen [CATEGORIES] en [PRODUITS]. We hebben gezien dat deze database ook drie andere tabellen bevat, namelijk [USERS, ROLES, USERS_ROLES], die zullen worden gebruikt om de webservice te beveiligen die op het web wordt geïmplementeerd. We laten deze tabellen voorlopig buiten beschouwing. Ter herinnering: de structuur van de tabellen [CATEGORIES] en [PRODUITS] is als volgt:
De tabel [PRODUITS] ziet er als volgt uit:
![]() |
- [ID]: de automatisch oplopende primaire sleutel van de tabel [2];
- [NOM]: de unieke naam van het product [4];
- [PRIX]: de prijs van het product;
- [DESCRIPTION]: de productbeschrijving;
- [VERSIONING] is het versienummer van het product. Het oorspronkelijke versienummer is 1 [3]. Telkens wanneer het product wordt gewijzigd, wordt het versienummer verhoogd door de code die de tabel beheert;
- [CATEGORIE_ID]: de externe sleutel in de tabel [CATEGORIES] om de categorie aan te duiden waartoe het product behoort;
![]() |
- in [1-3], de vreemde sleutel [CATEGORIE_ID] van de tabel [PRODUITS]. Deze verwijst naar de kolom [ID] van de tabel [CATEGORIES] [4-5];
- wanneer een categorie wordt verwijderd, worden alle producten die eraan gekoppeld zijn eveneens verwijderd ([6]). Dit is belangrijk om op te merken, omdat het wordt gebruikt bij de opbouw van de laag [DAO], die gebruikmaakt van de database [dbproduitscategories];
De tabel [CATEGORIES] met categorieën ziet er als volgt uit:
![]() |
- [ID]: automatisch oplopende primaire sleutel;
- [VERSIONING]: versienummer van de categorie;
- [NOM]: unieke naam van de categorie;
We zullen nu de entiteiten JPA, [Produit] en [Categorie] beschrijven, afbeeldingen van de tabellen [PRODUITS] en [CATEGORIES].
![]() |
6.3.3.1. De interface [AbstractCoreEntity]
De interface [AbstractCoreEntity] wordt geïmplementeerd door de entiteiten JPA, [Categorie] en [Produit]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getters en setters van de velden [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);
}
Deze interface, geïmplementeerd door de twee entiteiten JPA, dient uitsluitend om de methoden op te sommen voor het lezen en schrijven van de velden [id], [version] en [entityType] van deze entiteiten. De functie van het veld [entityType] wordt later toegelicht;
6.3.3.2. De entiteit JPA [Produit]
De klasse [Produit] is de entiteit JPA die gekoppeld is aan een rij in de tabel [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 {
// eigenschappen
@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();
// eigenschappen
@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;
// de categorie
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// fabrikanten
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;
}
// handtekening
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);
}
// ------------------------------------------------------------
// herdefinitie van [equals] en [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 en setters
...
public void setCategorie(Categorie categorie) {
// type van de entiteit
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;
}
}
- regel 21: de annotatie [@Entity] maakt van de klasse [Produit] een entiteit die wordt beheerd door de laag [JPA]. Men kan ook [@Entity(name="MonProduit")] schrijven, wat de entiteit de naam [MonProduit] geeft. Bij gebrek aan deze informatie is de naam van de entiteit de naam van de klasse, in dit geval [Produit]. Deze naamgeving is nodig wanneer er onder de entiteiten twee klassen uit verschillende pakketten zijn die dezelfde naam dragen;
- regel 22: de annotatie [@Table(name = "PRODUITS")] geeft aan dat de klasse [Produit] het objectbeeld is van een rij in de tabel [PRODUITS] van de database;
- regel 23: de naam van het filter jSON dat op de entiteit moet worden toegepast. We zullen zien dat de eigenschap [categorie] van regel 58 niet altijd beschikbaar is. Deze moet dan worden uitgesloten van de weergave jSON van het object. Hiervoor hebben we een filter nodig. In een filter met de naam [jsonFilterCategorie] geven we dus aan of we de eigenschap [categorie] al dan niet willen;
- regel 26: de annotatie [@Id] maakt van het geannoteerde veld het veld dat gekoppeld is aan de primaire sleutel van de tabel uit regel 19;
- regel 27: de annotatie [@GeneratedValue(strategy = GenerationType.IDENTITY)] bepaalt de wijze waarop de primaire sleutel automatisch wordt gegenereerd in de tabel [PRODUITS]. Dit wordt bepaald door het attribuut [strategy]. Er zijn verschillende wijzen:

De strategie [IDENTITY] is niet voor alle SGBD beschikbaar. Van de zes geteste SGBD was deze beschikbaar voor de SGBD en [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. Voor de twee andere [Oracle Express 11g Release 2, Firebird 2.5.4]-modellen moest de strategie [SEQUENCE] worden gebruikt. Voor de overdraagbaarheid tussen implementaties JPA mag de strategie [AUTO] niet worden gebruikt, omdat deze de keuze van de strategie voor het genereren van de primaire sleutel aan de implementatie JPA overlaat. Dus met MySQL 5 en de strategie [AUTO]:
- kiest Hibernate de strategie [IDENTITY] met de modus [AUTO_INCREMENT] voor de primaire sleutel;
- EclipseLink kiest de strategie [TABLE], die standaard een tabel met de naam [SEQUENCE] aanmaakt, die moet worden opgevraagd om de primaire sleutels te verkrijgen.
Uiteindelijk is de structuur van de database die door deze twee implementaties JPA wordt beheerd, niet dezelfde. Als deze door Hibernate is gegenereerd, zal deze niet bruikbaar zijn voor EclipseLink en vice versa.
- regel 28: de annotatie [@Column(name="ID"] bepaalt de naam van de kolom in de tabel [PRODUITS] die aan het veld [id] moet worden gekoppeld;
- regel 29: we gebruiken het type [Long] in plaats van [long] voor de primaire sleutel. De primaire sleutels [null] hebben namelijk een speciale betekenis voor JPA. Daarom wordt hier de voorkeur gegeven aan een objecttype boven een eenvoudig type;
- regel 31: de annotatie [@Version] geeft aan dat het veld [version] gekoppeld is aan een versiebeheer-kolom. De implementatie JPA zal dit versienummer verhogen telkens wanneer de entiteit wordt gewijzigd. Dit nummer dient om te voorkomen dat de entiteit gelijktijdig door twee verschillende gebruikers wordt bijgewerkt: twee gebruikers, U1 en U2, lezen de entiteit E met een versienummer gelijk aan V1. U1 wijzigt E en slaat deze wijziging op in de database: het versienummer verandert dan in V1+1. U2 wijzigt op zijn beurt E en slaat deze wijziging op in de database: er wordt een uitzondering gegenereerd omdat de versie (V1) verschilt van die in de database (V1+1);
- regel 36: het type van de entiteit. Er zijn er twee: POJO en PROXY. Standaard is de gegenereerde instantie een POJO (Plain Old Java Object). In sommige gevallen zullen de [Produit]-instanties die uit de database worden opgehaald, van het type [PROXY] zijn. Dit is het geval wanneer de eigenschap [Categorie categorie] op regel 58 niet is geïnitialiseerd met een categorie vanwege het attribuut [fetch = FetchType.LAZY] op regel 56. In dit geval verschillen de JPA-implementaties die getest zullen worden:
- [Hibernate, OpenJPA]: het opvragen van de categorie van een product van het type [PROXY] leidt tot een uitzondering. Hibernate gebruikt de term ‘proxy’ om een instantie van het type JPA aan te duiden die is verkregen in de modus [LAZY]. Daarom heb ik deze term gebruikt om dit type entiteit aan te duiden;
- [EclipseLink]: toegang tot de categorie van een product van het type [PROXY] leidt tot het opzoeken van deze categorie in de database en er treedt geen uitzondering op;
Omdat ik een testlaag wilde hebben die onafhankelijk was van de gebruikte implementatie JPA, wilde ik het type van elke entiteit weten: POJO of PROXY. Daarom heb ik het veld [entityType] toegevoegd aan de entiteiten JPA;
- regel 35: de annotatie [@Transient] geeft aan dat de implementatie JPA dit veld moet negeren. Het bestaat namelijk niet in de tabellen van SGBD;
- regel 40: de klasse [Produit] genereert een uitzondering van het type [ProxyException], waarvoor de naam van de klasse nodig is;
- regel 38: net als eerder wordt aangegeven dat de implementatie JPA dit veld moet negeren;
- regel 39: de annotatie [@JsonIgnore] geeft aan dat de serializer/deserializer jSON van een instantie [Produit] dit veld moet negeren;
- regel 43: de annotatie [@Column] koppelt het veld [nom] aan de kolom [NOM] van de tabel [PRODUITS]. Wanneer het veld dezelfde naam heeft als de bijbehorende kolom (hoofdlettergevoelig), kan de annotatie [@Column] worden weggelaten. Dat zou hier het geval zijn. De attributen [unique = true, length = 30, nullable = false] worden alleen gebruikt wanneer de implementatie JPA de tabel [CATEGORIES] moet genereren op basis van de entiteit [Produit]. Deze worden vertaald door de attributen SQL en [UNIQUE, VARCHAR(30), NOT NULL], waardoor de kolom [NOM] maximaal 30 tekens bevat, uniek is in de tabel en niet de waarde NULL mag hebben;
- regels 46-47: het veld [idCategorie] is gekoppeld aan de kolom [CATEGORIE_ID]. We komen later nog terug op de attributen hiervan;
- regels 49-50: het veld [prix] is gekoppeld aan de kolom [PRIX];
- regels 52-53: het veld [description] is gekoppeld aan de kolom [DESCRIPTION];
- regels 56-58: de productcategorie;
- regel 56: de annotatie [@ManyToOne] geeft aan dat de kolom van de annotatie op regel 57, [@JoinColumn(name = "CATEGORIE_ID")], een vreemde sleutel is van de tabel [PRODUITS] van deentiteit [Produit] in de tabel [CATEGORIES] die gekoppeld is aan de entiteit van regel 58. Deze annotatie moet een entiteit JPA annoteren. De klasse van regel 58 moet dus een entiteit JPA zijn;
- regel 56: de annotatie [fetch = FetchType.LAZY] zorgt ervoor dat wanneer een product uit de tabel [PRODUITS] wordt opgehaald, de categorie ervan (regel 58) niet onmiddellijk wordt opgehaald (lazy loading). Deze wordt dan opgehaald bij de eerste aanroep van de methode [getCategorie]. Hiervoor zorgt, tijdens de uitvoering, vult de laag JPA de oorspronkelijke methode [getCategorie] (die alleen het veld categorie retourneert) aan met een aanroep naar SGBD om de categorie op te halen – een techniek die ‘proxying’ wordt genoemd. De implementaties van JPA verschillen, zoals hierboven vermeld, in de manier waarop ze deze eigenschap implementeren. Dit kenmerk is niet dwingend. De gebruikte implementatie JPA mag het negeren. Omdat de eigenschap [categorie] al dan niet aanwezig kan zijn, hebben we het filter jSON op regel 23 geïntroduceerd. De koppelingskolom [CATEGORIE_ID] van de tabel [PRODUITS] wordt automatisch bijgewerkt bij het invoegen of bijwerken van het product. Deze kolom krijgt de waarde van [categorie.getId()], waarbij [categorie] het veld is in regel 58. De specificatie JPA schrijft voor dat deze koppelingskolom niet op een andere manier kan worden bijgewerkt. Daarnaast legt het de attributen [insertable = false, updatable = false] van regel 46 op, waardoor de kolom [CATEGORIE_ID] (de koppelingskolom dus) die gekoppeld is aan het veld [idCategorie] niet kan worden gewijzigd door het veld [idCategorie]. Alleen de overdracht van de kolom [CATEGORIE_ID] naar het veld [idCategorie] is mogelijk;
- regels 91-104: de gelijkheid tussen entiteiten [Produit] wordt gedefinieerd als de gelijkheid tussen hun primaire sleutels [id];
- regels 108-115: om onze testlaag overdraagbaar te maken, gaan we de entiteiten [PROXY] van de drie implementaties JPA en [Hibernate, EclipseLink, OpenJpa] op uniforme wijze behandelen. Voor een type [Produit] van het type [PROXY] zullen we het wijzigen van de waarde van het veld [categorie] verbieden. De klasse [ProxyException] is als volgt:
![]() |
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);
}
}
Ter afsluiting van de bespreking van deze entiteit moet worden opgemerkt dat de annotaties en hun attributen in twee duidelijk verschillende gevallen worden gebruikt:
- om de tabellen van de database aan te maken;
- om ze te gebruiken. In dit geval verwacht de implementatie JPA dat de tabellen er precies zo uitzien als wanneer zij deze zelf zou hebben gegenereerd. Men kan dus niet zomaar een willekeurige tabel [PRODUITS] koppelen aan de voorgaande entiteit [Produit]. Deze tabel moet ten minste (en kan ook andere hebben) de kenmerken hebben van de tabel [PRODUITS] die de entiteit zelf zou hebben gegenereerd. Wanneer men met JPA werkt, is het het beste om uit te gaan van een lege database waarin men JPA de tabellen laat genereren. We zullen deze generatie iets later bespreken. Het script SQL dat wordt meegeleverd met SGBD en MySQL is gegenereerd op basis van de tabellen die door JPA zijn gegenereerd.
Alle attributen van de entiteit [Produit] worden gebruikt voor het genereren van de tabel [PRODUITS]. Zodra dit is gebeurd, worden generatieattributen zoals [unique = true, length = 30, nullable = false] niet meer gebruikt bij het bewerken van de tabellen.
6.3.3.3. De entiteit JPA [Categorie]
De klasse [Categorie] is een entiteit JPA die gekoppeld is aan een rij in de tabel [CATEGORIES]:
![]() |
De code ervan is als volgt:
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();
// eigenschappen
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// gerelateerde producten
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// constructors
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;
}
// signatuur
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// methoden
public void addProduit(Produit produit) {
// type entiteit
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// een product toevoegen
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// het product wordt toegevoegd
produits.add(produit);
// de categorie wordt vastgesteld
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// herdefinitie van [equals] en [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 en setters
...
}
- regel 24: de klasse is een entiteit JPA;
- regel 25: gekoppeld aan de tabel [CATEGORIES];
- regel 26: de weergave jSON van de entiteit [Categorie] wordt aangestuurd door het filter met de naam [jsonFilterCategorie]. Dit filter moet worden geconfigureerd voordat een weergave jSON van de entiteit wordt aangevraagd. Het filter [jsonFilterCategorie] wordt gebruikt om al dan niet het veld [produits] uit regel 40 uit te sluiten van de weergave jSON van de entiteit [Categorie];
- regels 29-32: het veld [id] is gekoppeld aan de primaire sleutel [ID] van de tabel [CATEGORIES]. De gekozen generatiemodus is de modus [IDENTITY], dus de modus [AUTO_INCREMENT] voor MySQL;
- regels 34-36: het veld [version] is gekoppeld aan de versiebeheerkolom [VERSIONING] van de tabel [CATEGORIES];
- regels 38-39: het type van de entiteit [Categorie];
- regels 41-43: de eenvoudige naam van de klasse [Categorie];
- regels 46-47: het veld [nom] is gekoppeld aan de kolom [NOM] van de tabel [CATEGORIES]. Hieraan worden de attributen JPA en [unique = true, length = 30, nullable=false] toegekend, zodat bij het genereren van de tabel [CATEGORIES], de kolom [NOM] de attributen SQL en [UNIQUE, VARCHAR(30), NOT NULL] krijgt;
- regels 50-51: de producten die tot de categorie behoren;
- regel 50: de annotatie [@OneToMany] is de omgekeerde relatie van de relatie [@ManyToOne] die we zijn tegengekomen in de entiteit [Produit]. Het attribuut [mappedBy = "categorie"] geeft het veld aan van de entiteit [Produit] dat is geannoteerd door de omgekeerde relatie [@ManyToOne]. Het attribuut [cascade = { CascadeType.ALL }] zorgt ervoor dat de bewerkingen (persist, merge, remove) die worden uitgevoerd op een @Entity [Categorie], worden doorgevoerd op de [produits] op regel 51. Gedeeltelijke cascades kunnen worden opgegeven met de constanten [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- regel 50: het attribuut [fetch = FetchType.LAZY] zorgt ervoor dat wanneer een categorie uit de tabel [CATEGORIES] wordt opgehaald, de bijbehorende producten niet onmiddellijk worden opgehaald. Dit gebeurt dan bij de eerste aanroep van de methode [getProduits]. Hiervoor zorgt, tijdens de uitvoering, de laag JPA de oorspronkelijke methode [getProduits] (die alleen het veld produits retourneert) uitbreiden met een aanroep naar SGBD om de producten van de categorie op te halen. Dit attribuut is verplicht. De implementatie JPA mag het niet negeren. Omdat de eigenschap [produits] al dan niet geïnitialiseerd kan zijn, hebben we het filter jSON op regel 26 geïntroduceerd, waarmee we kunnen aangeven of we deze eigenschap al dan niet willen, en het type van de entiteit op regel 39;
- regels 71-88: met de methode [addProduit] kan een product aan de categorie worden toegevoegd;
- regels 73-76: om het beheer van proxies tussen verschillende implementaties van JPA te standaardiseren, is besloten dat er geen producten kunnen worden toegevoegd aan een entiteit van het type [Categorie];
- regels 92-112: twee entiteiten van het type [Categorie] worden als gelijk beschouwd als ze dezelfde primaire sleutel van het type [id] hebben;
6.3.4. Het bestand [persistence.xml]
![]() |
De applicaties JPA moeten bepaalde eigenschappen van de gebruikte provider JPA definiëren, evenals de te gebruiken entiteiten JPA, in een bestand [META-INF/persistence.xml] dat zich in het classpath van de applicatie bevindt. Hierboven is het geplaatst in de map [src/main/resources], die daadwerkelijk deel uitmaakt van het classpath van een Eclipse-project. Wanneer JPA in combinatie met Spring wordt gebruikt, wordt bepaalde informatie die in het bestand [persistence.xml] zou moeten staan, elders in Spring-configuratieklassen geplaatst. In een Spring-toepassing JPA stuurt Spring JPA aan. Met Spring JPA Hibernate kan het bestand [persistence.xml] tot zijn eenvoudigste vorm worden teruggebracht:
<?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>
- regels 1-5: een [persistence.xml]-bestand moet een <persistence>-root-tag bevatten. De attributen van de tag op regel 2 worden in deze toepassing niet gebruikt;
- een persistentiebestand kan één of meerdere persistentie-eenheden definiëren met de tag <persistence-unit> (regel 4). Een persistentie-eenheid beheert de toegang tot een specifieke database. Als de applicatie twee databases tegelijkertijd beheert, zal deze twee persistentie-eenheden hebben;
- regel 4: een persistentie-eenheid heeft een naam [attribut name], ondersteunt een transactietype [attribut transaction-type], heeft eigenschappen en definieert de entiteiten die gekoppeld zijn aan de tabellen van de database die door de persistentie-eenheid wordt beheerd. Aangezien de toegang tot de database hier door [Spring JPA Hibernate] wordt beheerd, kunnen deze laatste twee gegevens elders worden geplaatst. Er zijn twee soorten transacties:
- [RESOURCE_LOCAL]: de transacties worden door de applicatie zelf beheerd. Dit is hier het geval, waarbij Spring de transacties zal beheren;
- [JTA] (Java Transaction API): de EJB-container (Enterprise Java Bean) voert de applicatie uit en beheert de transacties automatisch op basis van Java-annotaties in de code. Dat is hier niet het geval;
We zullen later zien dat de inhoud van dit bestand [persistence.xml] afhankelijk is van de gebruikte implementatie JPA.
6.4. Het project [spring-jpa-generic]
Laten we nog eens herhalen wat we willen doen. We willen de volgende architectuur implementeren:
![]() |
waarin de laag [DAO] de in hoofdstuk 4 besproken interface [IDao<Produit>, IDao<Categorie>] zou implementeren. Het gaat erom twee implementaties van deze interface te vergelijken:
- de ene gebouwd met Spring JDBC;
- de andere gebouwd met Spring JPA;
In de bovenstaande architectuur:
- wordt de laag [JDBC] geïmplementeerd door het project [mysql-config-jdbc] dat in paragraaf 3.3 is besproken;
- de laag [JPA] wordt geïmplementeerd door het project [mysql-config-jpa-hibernate], dat in paragraaf 6.3 is besproken;
Het project [spring-jpa-generic] zorgt voor de implementatie van de lagen [DAO] en [Spring Data].
![]() |
6.4.1. Maven-configuratie
Het project [spring-jpa-generic] is een Maven-project dat is geconfigureerd door het volgende bestand [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>
<!-- configuratie JPA van 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>
- regels 22-26: het project heeft slechts één afhankelijkheid, namelijk die van het project dat de [JPA]-laag van de applicatie configureert en dat we zojuist hebben bekeken. Het is een generieke applicatie:
- we schakelen over van SGBD door het configuratieproject van de laag [JDBC] te wijzigen;
- we wisselen van implementatie JPA door het configuratieproject van de laag [JPA] te wijzigen;
Uiteindelijk zijn de afhankelijkheden als volgt:
![]() |
6.4.2. Spring-configuratie
![]() |
De klasse [AppConfig] configureert het Spring-project:
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 {
}
- regel 11: de klasse is een Spring-configuratieklasse;
- regel 10: de annotatie [@EnableJpaRepositories] wordt gebruikt om de pakketten aan te duiden die de [CrudRepository]-interfaces van Spring Data bevatten. Hierdoor worden het Spring-componenten die in andere Spring-componenten kunnen worden geïnjecteerd;
- regel 12: de annotatie [@ComponentScan] geeft aan dat het pakket [spring.data.dao] moet worden doorzocht op Spring-componenten. De componenten [DaoCategorie] en [DaoProduit] worden gevonden;
- regel 13: de beans van de configuratieklasse [ConfigJpa] worden geïmporteerd. Hierin bevindt zich de bean van de gebruikte implementatie JPA (Hibernate, Eclipselink, OpenJpa), de te gebruiken gegevensbron, de EntityManager die de bewerkingen JPA zal beheren, de transactiebeheerder;
6.4.3. De laag [Spring Data]
![]() |
![]() |
6.4.3.1. De interface [CategoriesRepository]
De interface [CategoriesRepository] beheert de toegang tot de tabel [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 met bijbehorende producten
@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();
}
- regel 10: de interface [CrudRepository] is gebruikt en toegelicht in paragraaf 5.1.3. Ter herinnering:
- het eerste parametertype van de interface de entiteit JPA is, die wordt beheerd voor de toegangen CRUD (findOne, findAll, save, delete, deleteAll),
- het tweede type parameter van de interface is de primaire sleutel van de entiteit JPA, in dit geval een geheel getal [Long];
De methoden van de interface worden geïmplementeerd door middel van JPQL-query's (Java Persistence Query Language). Deze query's vragen entiteiten JPA op. In een dergelijke query:
- worden de tabellen vervangen door de bijbehorende entiteiten JPA;
- worden de kolommen vervangen door velden van de entiteiten JPA die in de query worden gebruikt;
Laten we de regels 31-32 als voorbeeld nemen: de methode op regel 32 haalt alle categorieën uit de database op in hun verkorte vorm. Deze wordt geïmplementeerd door de query JPQL (Java Persistence Query Language) uit regel 31, die sterk lijkt op zijn tegenhanger SQL. Voor meer informatie over JPQL kunt u [ref2] raadplegen (zie paragraaf 1.2).
De methoden van de interface [CategoriesRepository] zijn als volgt:
- regels 13-14: de methode [getLongCategorieById] retourneert de uitgebreide versie van een categorie waarnaar wordt verwezen via de primaire sleutel [id], d.w.z. de categorie met de bijbehorende producten. We herinneren ons dat in de entiteit [Categorie] het veld [produits] het attribuut [fetch = FetchType.LAZY] (lazy loading) had. In de query JPQL wordt het laden van de producten met het trefwoord [fetch] geforceerd. De parameter ?1 van de query wordt bij uitvoering vervangen door de waarde van de eerste parameter van de methode op regel 12, dus door de parameter [Long id];
- regels 16-17: de methode [getLongCategorieByName] retourneert de lange versie van een categorie waarnaar wordt verwezen via de naam [nom];
- regels 19-20: de methode [getShortCategoriesByName] retourneert de korte versies van categorieën waarnaar wordt verwezen via hun naam. Het veld [produits] van deze categorieën is niet null. Het bevat de verwijzing naar een proxy (een klasse die is aangemaakt door de implementatie JPA) die tot taak heeft de producten van de categorie terug te geven wanneer deze wordt aangeroepen. Het aanroepen ervan buiten de persistentiecontext JPA veroorzaakt een uitzondering (Hibernate en OpenJpa, maar niet EclipseLink). Om deze reden zullen we het veld [produits] van de korte versie van een categorie niet gebruiken;
- regels 22-23: de methode [getShortCategoriesById] retourneert de korte versies van categorieën waarnaar wordt verwezen via hun primaire sleutels [id];
- regels 25-26: de methode [getLongCategoriesById] haalt de lange versies van categorieën op die worden aangeduid met hun primaire sleutels [id];
- regels [28-29]: de methode [getLongCategoriesByName] retourneert de lange versies van categorieën die worden aangeduid met hun namen;
- regels 31-32: de methode [getAllShortCategories] retourneert de korte versies van alle categorieën;
- regels 34-35: de methode [getAllLongCategories] retourneert de lange versies van alle categorieën;
Opmerking: niet alle implementaties van JPA accepteren dezelfde syntaxis als JPQL. Zo wordt de volgende syntaxis wel geaccepteerd door Hibernate en EclipseLink, maar niet door OpenJpa:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
OpenJpa accepteert de bovenstaande alias [p] niet.
6.4.3.2. De interface [ProduitsRepository]
De interface [ProduitsRepository] beheert de toegang tot de tabel [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> {
// een product met zijn categorie
@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();
}
- regels [15-16]: de methode [getLongProduitById] retourneert de uitgebreide versie van een product dat wordt geïdentificeerd door zijn primaire sleutel [id], dus inclusief de categorie. We herinneren ons dat in de entiteit [Produit] het veld [categorie] het attribuut [fetch = FetchType.LAZY] (lazy loading) had. In de query JPQL wordt het laden van de categorie geforceerd met het trefwoord [fetch];
- regels 18-19: de methode [getLongProduitByName] retourneert de lange versie van een product dat wordt geïdentificeerd aan de hand van zijn naam;
- regels 21-22: de methode [getShortProduitsById] retourneert de korte versie van de producten die worden geïdentificeerd aan de hand van hun primaire sleutel [id]. In deze korte versie heeft het veld [categorie] niet de waarde null. Het bevat de referentie van een proxy die is gegenereerd door de implementatie JPA, die, indien aangeroepen, de categorie van het product ophaalt. Deze aanroep kan alleen plaatsvinden binnen de persistentie-context JPA. Als dit elders gebeurt, leidt dit tot een uitzondering (Hibernate en OpenJpa, maar niet EclipseLink). Daarom zullen we, zowel in de laag [DAO] als elders, het veld [categorie] van een product in de korte versie niet gebruiken. In de korte versie van het product wordt het veld [idCategorie] geïnitialiseerd. De waarde hiervan is de primaire sleutel van de categorie waartoe het product behoort. Hierdoor kan deze categorie later via de methode [DaoCategorie. getShortCategoriesById(idCategorie)] worden opgevraagd bij de laag [DAO];
- regels 24-25: de methode [getShortProduitsByName] retourneert de korte versie van de producten die aan de hand van hun naam worden geïdentificeerd;
- regels 27-28: de methode [getLongProduitsById] retourneert de lange versie van de producten die worden geïdentificeerd aan de hand van hun primaire sleutels;
- regels 30-31: de methode [getLongProduitsByName] retourneert de lange versie van de producten die worden geïdentificeerd aan de hand van hun namen;
- regels 33-34: de methode [getAllLongProduits] retourneert de lange versie van alle producten;
- regels 36-37: de methode [getAllShortProduits] retourneert de korte versie van alle producten;
Deze interfaces worden geïmplementeerd door klassen die tijdens de uitvoering van het project worden gegenereerd door de implementatie JPA. Dergelijke klassen worden [proxy]-klassen genoemd. Standaard worden de methoden van de interface [CrudRepository] binnen een transactie uitgevoerd. Doordat de interfaces [ProduitsRepository, CategoriesRepository] een uitbreiding vormen op de klasse [CrudRepository], zijn het Spring-componenten. Als zodanig kunnen ze in andere Spring-componenten worden geïnjecteerd.
6.4.4. De laag [DAO]
![]() |
![]() |
6.4.4.1. De interface [IDao<T>]
De interface [IDao<T>] is dezelfde die al is besproken in de implementatie van de laag [DAO], gemaakt met Spring JDBC (zie paragraaf 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// lijst van alle T-entiteiten
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// van specifieke entiteiten – korte versie
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);
// specifieke entiteiten – lange versie
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 van meerdere entiteiten
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// verwijdering van alle entiteiten
public void deleteAllEntities();
// verwijdering van meerdere entiteiten
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. De abstracte klasse [AbstractDao]
![]() |
De abstracte klasse [AbstractDao] is de bovenliggende klasse van de klassen die de laag [DAO] implementeren:
- de klasse [DaoProduit], die de interface [IDao<Produit>] implementeert en de toegang tot de tabel [PRODUITS] beheert;
- de klasse [DaoCategorie], die de interface [IDao<Categorie>] implementeert en de toegang tot de tabel [CATEGORIES] beheert;
De code ervan is dezelfde als beschreven in paragraaf 4.8, met het volgende kleine verschil: geen enkele methode heeft het attribuut [@Transactional], waardoor de methode binnen een transactie wordt uitgevoerd. Hier wordt gebruikgemaakt van het feit dat de [CrudRepository]-interfaces van Spring Data standaard binnen een transactie worden uitgevoerd.
6.4.4.3. De klasse [DaoCategorie]
![]() |
De klasse [DaoCategorie] implementeert de interface [IDao<Categorie>] als volgt:
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);
}
}
}
- regel 17: de annotatie [@Component] maakt van de klasse [DaoCategorie] een Spring-component;
- regel 18: de klasse [DaoCategorie] is een uitbreiding van de klasse [AbstractDao<Categorie>], waardoor deze de interface [IDao<Categorie>] implementeert;
- regels 20-24: injectie van verwijzingen naar de twee interfaces [CrudRepository] vanuit [Spring Data]. Deze injectie vindt plaats tijdens het instantiëren van de Spring-objecten, doorgaans aan het begin van de uitvoering van het Spring-project;
- alle methoden van de klasse delegeren het werk aan de methoden met dezelfde naam in de interfaces [CrudRepository];
- alle methoden die de entiteiten terugbrengen naar hun verkorte versie, geven dit aan door het type van de entiteit in te stellen op [EntityType.PROXY] (regels 29, 63, 72);
De methode [saveEntities] verdient een toelichting:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// de producten die zullen worden toegevoegd worden genoteerd
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);
}
// we maken van de gelegenheid gebruik om (indien nodig) de relatie product --> categorie te herstellen
produit.setCategorie(categorie);
}
}
}
// de categorieën en producten worden opgeslagen
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// het veld [idCategorie] van de toegevoegde producten wordt bijgewerkt
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// resultaat
return categories;
}
- regel 2: de als parameters doorgegeven categorieën zijn zowel in te voegen categorieën ([id==null]) als te wijzigen categorieën ([id!=null]);
- regel 20: de categorieën worden opgeslagen met de methode [categoriesRepository.save(entities)]. Bij het testen blijkt dat het veld [idCategorie] van de opgeslagen producten (id==null) niet is ingevuld. Om dit probleem op te lossen, worden in de regels 4-17 de producten genoteerd die zullen worden ingevoegd en zodra ze zijn opgeslagen, wordt hun veld [idCategorie] ingevuld (regels 25-27);
- regels 5-17: we doorlopen de lijst met categorieën;
- regels 8-16: voor elke categorie doorlopen we de bijbehorende productlijst. Hier doet zich een probleem voor. De methode [saveEntities] wordt zowel gebruikt om een categorie op te slaan als om deze te wijzigen. In het laatste geval kan de categorie in de verkorte versie zijn opgehaald, met dus de verwijzing naar een proxy-methode in het veld [produits]. Het gebruik hiervan met Hibernate leidt dan tot een uitzondering, omdat de gebruikte categorie zich niet meer in de persistentiecontext JPA bevindt, die is gesloten bij het einde van de transactie van de methode die de verkorte versies van de categorieën heeft opgehaald. Vervolgens wordt het veld [EntityType] van de entiteit [Categorie] op regel 8 gebruikt om te bepalen of de productlijst van de categorie al dan niet toegankelijk is;
- regel 14: het product wordt aan zijn categorie gekoppeld. Normaal gesproken zou dit al het geval moeten zijn. Maar we weten niet hoe dit product is opgebouwd en of het al aan zijn categorie is gekoppeld. Om problemen te voorkomen (om de entiteit [Produit] te beheren, moet JPA verwijzen naar de entiteit [Categorie] waaraan deze is gekoppeld), leggen we deze koppeling zelf aan.
Als we deze code vergelijken met die van de klasse [DaoProduit] uit de Spring-implementatie JDBC (zie paragraaf 4.9) kunnen we vaststellen dat de Spring Data-bibliotheek JPA het schrijven van de laag [DAO] enorm vergemakkelijkt.
6.4.4.4. De klasse [DaoProduit]
![]() |
De klasse [DaoProduit] implementeert de interface [IDao<Produit>] als volgt:
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) {
// de koppeling tussen een product en zijn categorie wordt (indien nodig) hersteld
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// de producten worden opgeslagen
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);
}
}
}
De code is vergelijkbaar met die van de klasse [DaoCategorie]:
- bij de lange versies van de categorieën blijkt uit tests dat het veld [idCategorie] van de producten niet is ingevuld. De methode [linkLongProduitsToCategories] op de regels 96-105 lost dit probleem op;
- de methode [saveEntities] in de regels 108-121 voegt nieuwe producten toe of wijzigt bestaande producten. De laag JPA vereist dat elke entiteit [Produit] gekoppeld is aan een entiteit [Categorie]. Aangezien we niet weten of de gebruiker dit heeft gedaan, doen we dit zelf in de regels 110-113. Het volstaat om de [Produit] te koppelen aan een entiteit [Categorie] waarvan de primaire sleutel gelijk is aan het veld [idCategorie] van de [Produit]. Bij het testen blijkt dat er een fout optreedt als we null invoeren voor de versie van de categorie. Daarom stellen we hier de waarde 0 in, maar je kunt elke gewenste waarde invoeren. Afgezien van de primaire sleutel is geen enkel veld van de entiteit [Categorie] nodig voor de laag JPA om een entiteit [Produit] in te voegen of te wijzigen;
6.4.5. De testlaag
![]() |
![]() |
De bovenstaande tests zijn identiek aan die van de Spring-implementatie JDBC. Raadpleeg indien nodig de volgende pagina's:
- [JUnitTestCheckArguments]: paragraaf 4.11.1;
- [JUnitTestDao]: paragraaf 4.11.2;
- [JUnitTestPushTheLimits]: paragraaf 4.11.3;
We gebruiken de volgende uitvoeringsconfiguraties:
![]() | ![]() |
![]() | ![]() |
De resultaten van de verschillende tests zijn als volgt:
![]() | ![]() |
![]() |
In [1], de test [JUnitTestPushTheLimits] met de Spring Data-implementatie JPA (Hibernate) en in [2] met de Spring-implementatie JDBC. We zien dat deze laatste beter presteert. We komen dus tot een eerste conclusie: het is aanzienlijk eenvoudiger om een [DAO]-laag te ontwikkelen met Spring Data JPA, maar deze presteert minder goed dan een Spring-implementatie JDBC.
De test [JUnitTestProxies] is een schijn-test JUnit. Deze is bedoeld om het gedrag van elke implementatie JPA ten opzichte van de proxies, dus de verkorte versies van de entiteiten, te laten zien:
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 {
// laag [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// de database wordt vóór elke test opgeschoond
log("Vidage de la base de données", 1);
// de tabel [CATEGORIES] wordt leeggemaakt en vervolgens ook de tabel [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// de tabellen worden gevuld
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);
}
// de categorie toevoegen – de producten worden vervolgens ook
// ingevoegd
daoCategorie.saveEntities(categories);
// resultaat
return categories;
}
@Test
public void getShortCategoriesByName1() {
// invullen
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() {
// invullen
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() {
// invullen
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() {
// vullen
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) {
// bericht weergeven
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);
}
}
De verkregen resultaten zijn als volgt:
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]
Hier zien we dat wanneer we het veld [Categorie.produits] van een categorie van het type PROXY en het veld [Produit.categorie] van een product van het type PROXY benaderen, er in beide gevallen (regels 7 en 17) een uitzondering van het type [org.hibernate.LazyInitializationException] optreedt.



































