4. Inleiding tot Spring JDBC
In dit hoofdstuk gaan we de volgende architectuur bestuderen:
![]() |
Dit is dus dezelfde architectuur als eerder. We gaan twee wijzigingen aanbrengen:
- de database zal twee tabellen bevatten die via een vreemde-sleutelrelatie met elkaar zijn verbonden;
- de laag [DAO] wordt geïmplementeerd met de bibliotheek [Spring JDBC], die het beheer van de API en JDBC vereenvoudigt;
4.1. Het opzetten van de werkomgeving
Importeer met STS het project [spring-jdbc-04] dat zich in de map [<exemples>/spring-database-generic/spring-jdbc] bevindt
![]() |
Daarnaast moeten we een nieuwe database MySQL aanmaken met de client [MyManager] (zie paragraaf 3.1):
![]() |
- in [3]; de volgende voorbeelden zijn gebaseerd op een database MySQL met de naam [dbproduitscategories];
![]() |
- naar [9]: voer het wachtwoord van de gebruiker root in (dit wachtwoord is root in dit document);
![]() |
![]() |
- in [18] is de database [dbproduitscategories] leeg aangemaakt. We maken tabellen aan en vullen deze met een script SQL [19-20] ;
![]() |
- in [21], ga naar de map [<exemples>/spring-database-config/mysql/databases];
![]() |
- in [25], zorg ervoor dat u zich op de database [dbproduitscategories] bevindt en niet op de database [dbproduits] ;
- in [29] heeft het script SQL vijf tabellen aangemaakt. De tabellen [ROLES, USERS, USERS_ROLES] worden pas gebruikt wanneer we de beveiliging gaan regelen van de webservice die is gebouwd om de database [dbproduitscategories] op het web beschikbaar te maken;
4.2. De database [dbproduitscategories]
De database [dbproduitscategories] is een uitbreiding van de eerder besproken database [dbproduits]. Waar in de tabel [PRODUITS] het product een categorie had die werd aangeduid met een nummer zonder specifieke betekenis, zal dit nummer hier een vreemde sleutel zijn in de tabel [CATEGORIES].
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 externe 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;
4.3. Het Eclipse-project
![]() |
Het project [spring-jdbc-04] implementeert de volgende architectuur:
![]() |
Het project [spring-jdbc-04] 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-jdbc-generic-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-04</name>
<description>Demo project for Spring JdbcTemplate</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- opzoeken bovenliggend item uit repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuratie JDBC van SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring JdbcTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- regels 28-32: het project is gebaseerd op het project [mysql-config-jdbc], dat de laag JDBC configureert;
- regels 34-37: het artefact [spring-boot-starter-jdbc] voegt de Spring-bibliotheken JDBC toe;
Uiteindelijk zijn de afhankelijkheden als volgt:
![]() |
4.4. Spring-configuratie
![]() |
De klasse [AppConfig] die het Spring-project configureert, is als volgt:
package spring.jdbc.config;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
@EnableTransactionManagement
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
// 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;
}
// Transactiemanager
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// JdbcTemplate
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
// product toevoegen
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
ConfigJdbc.TAB_PRODUITS_ID);
}
// categorie toevoegen
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
ConfigJdbc.TAB_CATEGORIES_ID);
}
}
- regel 16: de klasse is een Spring-configuratieklasse;
- regel 17: het pakket [spring.jdbc.dao] wordt doorzocht op andere Spring-componenten dan die in de klasse [AppConfig]. Daarin bevindt zich de component die de laag [DAO] implementeert;
- regel 18: we gaan de transacties niet zelf beheren, maar dit overlaten aan Spring JDBC. Het enige wat we hoeven te doen, is de methoden die binnen een transactie moeten worden uitgevoerd, annoteren met de Spring-annotatie [@Transactional]. Regel 18 zorgt ervoor dat deze annotatie wordt verwerkt en niet wordt genegeerd. Het transactiebeheer wordt verzorgd door een van de afhankelijkheden van het Spring-project JDBC, dat wordt geïmporteerd door het bestand [pom.xml];
- regel 19: we importeren de beans die al zijn gedefinieerd in de klasse [generic.jdbc.config.ConfigJdbc] van het project [mysql-config-jdbc];
- regels 23-36: de gegevensbron [tomcat-jdbc] die in het voorbeeld [spring-jdbc-02] is geïntroduceerd;
- regels 40-42: de transactiebeheerder die gekoppeld is aan de eerder gedefinieerde gegevensbron. De bean moet absoluut [transactionManager] heten, omdat deze naam wordt gebruikt door de annotatie [@EnableTransactionManagement]. De handler [DataSourceTransactionManager] wordt geleverd door de Spring-bibliotheek JDBC (regel 12);
- regels 45-48: de bean [namedParameterJdbcTemplate] waarop de implementatie van de laag [DAO] zal berusten. Deze bean wordt geleverd door de Spring-bibliotheek JDBC (regel 10). Ook deze bean is gekoppeld aan de eerder gedefinieerde gegevensbron (regel 47);
- regels 51-55: de bean [simpleJdbcInsertProduit] (vrije naam) wordt gebruikt om een product in de tabel [PRODUITS] in te voegen en de gegenereerde primaire sleutel op te halen. De verschillende gebruikte parameters zijn als volgt:
- [dataSource]: de gegevensbron [tomcat-jdbc] uit de regels 24-36;
- [ConfigJdbc.TAB_PRODUITS]: de tabel [PRODUITS];
- [ConfigJdbc.TAB_CATEGORIES_ID]: de primaire sleutelkolom van de tabel [PRODUITS]. Let op: voor PostgreSQL moet de naam van deze kolom in kleine letters worden geschreven;
- regels 58-62: de bean [simpleJdbcInsertCategorie] wordt gebruikt om een categorie in de tabel [CATEGORIES] in te voegen en de gegenereerde primaire sleutel op te halen;
4.5. Uitzonderingen in het project
![]() |
We hebben de klassen [UncheckedException, DaoException, ShortException] al gezien in het project [spring-jdbc-03]. We voegen er een nieuwe aan toe:
package spring.jdbc.infrastructure;
public class MyIllegalArgumentException extends UncheckedException {
private static final long serialVersionUID = 1L;
// fabrikanten
public MyIllegalArgumentException() {
super();
}
public MyIllegalArgumentException(int code, Throwable e, String className) {
super(code, e, className);
}
}
- De klasse [MyIllegalArgumentException] is afgeleid van de klasse [UncheckedException] en is dus een niet-gecontroleerde klasse. Deze zal worden gebruikt om een aanroep met onjuiste argumenten van een methode uit de laag [DAO] te signaleren. We hebben deze klasse niet [IllegalArgumentException] genoemd, omdat deze uitzondering al bestaat in de klasse JDK en dit er soms toe leidde dat de compiler een onjuiste [import] genereerde;
4.6. De entiteiten van het project
![]() |
De klassen van het pakket [spring.jdbc.entities] zijn de afbeeldingen van de rijen uit de tabellen van de database [dbproduitscategories]. We laten de afbeeldingen van de tabellen [USERS, ROLES, USERS_ROLE] voorlopig buiten beschouwing.
Alle entiteiten zijn afgeleid van de bovenliggende klasse [AbstractCoreEntity]:
package spring.jdbc.entities;
public abstract class AbstractCoreEntity {
// eigenschappen
protected Long id;
protected Long version;
// fabrikanten
public AbstractCoreEntity() {
}
public AbstractCoreEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
public AbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
public void setAbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
// ------------------------------------------------------------
// herdefinitie van [equals] en [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
return id != null && other.id != null && id.equals(other.id);
}
// getters en setters
...
}
- regel 5: het veld [id] wordt gekoppeld aan de kolom [ID], de primaire sleutel van de tabellen;
- regel 6: het veld [version] wordt gekoppeld aan de kolom [VERSIONING] van de tabellen;
- regels 8-26: verschillende constructors en methoden voor het aanmaken of initialiseren van een object [AbstractCoreEntity];
- regels 35-47: de methode [equals] bepaalt dat twee objecten [AbstractCoreEntity] gelijk zijn als ze hetzelfde veld [id] hebben. Hierbij moet worden bedacht dat de objecten [AbstractCoreEntity] afbeeldingen zijn van tabelrijen waarin [id] de primaire sleutel is en waarin er dus geen twee rijen met hetzelfde [id] kunnen voorkomen;
- regels 30-33: een voorstel van [hashCode];
De klasse [Produit] is de weergave van een rij uit de tabel [PRODUITS]:
package spring.jdbc.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
// eigenschappen
private String nom;
private Long idCategorie;
private double prix;
private String description;
private Categorie categorie;
// constructors
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
super(id, version);
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// signatuur
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// getters en setters
...
}
- regel 6: de klasse [Produit] is een uitbreiding van de klasse [AbstractCoreEntity];
- regels 8-12: de velden [id, version, nom, idCategorie, prix, description] zijn de afbeeldingen van de kolommen [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] uit de tabel [PRODUITS];
- regel 12: het object van het type [Categorie] met primaire sleutel [idCategorie]. Dit veld wordt al dan niet ingevuld, afhankelijk van het geval. Wanneer het is ingevuld, spreken we van het product in de lange versie [LongProduit], anders van het product in de korte versie [ShortProduit];
- regel 5: een filter jSON. Ter herinnering: het project [mysql-config-jdbc] bevat een bibliotheek jSON. Het filter is nodig omdat het veld [categorie] al dan niet kan worden ingevuld. In dat geval verschilt de weergave jSON van het product. Om deze twee gevallen te verwerken, configureren we het filter [jsonFilterProduit] op regel 5. Met een filter jSON kunnen we dynamisch aangeven welke velden moeten worden uitgesloten van de weergave jSON. Zodra bekend is dat het veld [categorie] niet is ingevuld, wordt het uitgesloten van de weergave jSON van het product;
De klasse [Categorie] is de weergave van een rij uit de tabel [CATEGORIES]:
package spring.jdbc.entities;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
// eigenschappen
private String nom;
public List<Produit> produits;
// constructors
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
super(id, 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) {
// een product toevoegen
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// het product wordt toegevoegd
produits.add(produit);
// de categorie instellen
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// getters en setters
...
}
- regel 9: de klasse [Categorie] is een uitbreiding van de klasse [AbstractCoreEntity];
- regel 12: de velden [id, version, nom] zijn de weergaven van de kolommen [ID, VERSIONING, NOM] uit de tabel [CATEGORIES];
- regel 13: het veld [produits] bevat de lijst met producten van de categorie. Dit veld is niet altijd ingevuld. Wanneer het niet is ingevuld, spreken we van de korte versie van de categorie [ShortCategorie], anders van de lange versie van de categorie [LongCategorie];
- regels 32-44: met de methode [addProduit] kan een product aan de categorie worden toegevoegd (regel 39) en kunnen in het toegevoegde product de kenmerken van de categorie worden vastgelegd (idCategorie en categorie);
- regel 8: een filter jSON. Wanneer de bibliotheek jSON een object [Categorie] moet serialiseren/deserialiseren, moet aan de bibliotheek worden aangegeven hoe het filter met de naam [jsonFilterCategorie] moet worden afgehandeld;
4.7. De Idao<T>-interface
![]() |
![]() |
De interface [IDao] van de laag [DAO] heeft de volgende handtekening:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.AbstractCoreEntity;
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);
// van 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);
// meerdere entiteiten bijwerken
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);
}
- regel 7: hier hebben we een interface [IDao] die wordt gedefinieerd door een type T met een voorwaarde: dit type moet de klasse [AbstractCoreEntity] uitbreiden of de interface [AbstractCoreEntity] implementeren. Het sleutelwoord [extends] wordt in beide gevallen gebruikt. Hier zal T worden geïnstantieerd door ofwel het type [Produit], ofwel het type [Categorie]. Het wordt namelijk al snel duidelijk dat we hetzelfde soort bewerkingen (invoegen, wijzigen, verwijderen, selecteren) uitvoeren op de typen [Produit] en [Categorie]. Het lijkt dan ook logisch om deze methoden in een generieke interface samen te brengen;
- afhankelijk van het geval verwijzen de termen [LongEntity] en [ShortEntity] naar verschillende situaties:
- wanneer T het type [Produit] is:
- is [ShortEntity] het product zonder het ingevulde veld [Categorie categorie];
- [LongEntity] is het product waarbij het veld [Categorie categorie] is ingevuld;
- wanneer T het type [Categorie] is:
- [ShortEntity] is de categorie zonder het ingevulde veld [List<Produit> produits];
- [LongEntity] is het product waarbij het veld [List<Produit> produits] is ingevuld;
- wanneer T het type [Produit] is:
We hebben dus een interface met 19 methoden. De meeste methoden komen dubbel voor. Laten we de methode [getShortEntitiesById] als voorbeeld nemen:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- regels 1 en 3: de parameter is de lijst met primaire sleutels van de entiteiten waarvan we de verkorte versie willen. Deze lijst wordt in twee verschillende vormen weergegeven:
- regel 1: een lijst die de interface [Iterable<Long>] implementeert. Het type [List<Long>] implementeert deze interface, maar er zijn nog veel meer andere. Als we [List<Long> ids] hadden gebruikt, zou dat voor onze voorbeelden voldoende zijn geweest, maar dan zouden we de gebruiker van onze voorbeelden hebben gedwongen om conversies uit te voeren als zijn parameter niet van het exact verwachte type was;
- regel 3: helaas implementeert het type Long[] de interface [Iterable<Long>] niet. In dat geval gebruiken we de versie uit regel 3. De formele parameter [Long... ids] (3 punten) kan zowel de waarde van een array als van een reeks id’s aannemen: getShortEntitiesById(id1, id2, ...);
Dezezelfde interface IDao<T> zal worden geïmplementeerd door de volgende architectuur:
![]() |
waarbij een laag [JPA] (Java Persistence API) tussen de laag [DAO] en de driver JDBC van de SGBD wordt geplaatst. Hierdoor beschikken we over een testlaag die voor beide architecturen gemeenschappelijk is. In beide gevallen zal de laag [DAO] twee interfaces hebben:
- IDao<Product> voor toegang tot de tabel [PRODUITS];
- IDao<Categorie> voor toegang tot de tabel [CATEGORIES];
4.8. Implementatie van de interface IDao<T>
![]() |
- de interface IDao<Product> wordt geïmplementeerd door de klasse [DaoProduit] ;
- de interface IDao<Categorie> wordt geïmplementeerd door de klasse [DaoCategorie];
De klassen [DaoProduit] en [DaoCategorie] zijn beide afgeleid van de abstracte klasse [AbstractDao] met de volgende :
package spring.jdbc.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;
import spring.jdbc.entities.AbstractCoreEntity;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T extends AbstractCoreEntity> implements IDao<T> {
// toevoegingen
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// lokaal
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// geldigheid van het argument
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// ophalen in delen
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// resultaat
return entities;
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// geldigheid van het argument
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// resultaat
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(String... names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Iterable<Long> ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Long... ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(String... names) {
...
}
@Override
@Transactional
public List<T> saveEntities(Iterable<T> entities) {
...
}
@Override
@Transactional
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities) {
...
}
@Override
public void deleteEntitiesById(Iterable<Long> ids) {
...
}
@Override
public void deleteEntitiesById(Long... ids) {
...
}
@Override
public void deleteEntitiesByName(Iterable<String> names) {
...
}
@Override
public void deleteEntitiesByName(String... names) {
...
}
@Override
public void deleteEntitiesByEntity(Iterable<T> entities) {
...
}
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities) {
...
}
protected void deleteEntitiesByEntity(List<T> entities) {
...
}
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
// privé-methoden ----------------------------------------------
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
}
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
// beschermde methoden ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
- regel 15: de klasse [AbstractDao] is abstract (trefwoord abstract). Als zodanig kan deze niet worden geïnstantieerd. Deze kan alleen worden afgeleid. Deze klasse heeft verschillende functies:
- het vastleggen van de aard van de transactie waarin elke methode plaatsvindt;
- zoveel mogelijk gemeenschappelijke elementen aanbrengen in de twee implementaties van de interfaces [IDao<Produit>] en [IDao<Categorie>]. Het gaat hierbij voornamelijk om het controleren van de geldigheid van de argumenten. Er worden geen null-argumenten of lege lijsten geaccepteerd;
- het type van de parameters T... params en Iterable<T> params uniform maken tot één type: List<T> params;
- het werk delegeren aan de dochterklassen zodra het specifiek wordt voor een van de twee interfaces;
Dankzij de standaardisatie van de parameters van de verschillende methoden door de klasse [AbstractDao], hoeven de dochterklassen [DaoProduit] en [DaoCategorie] slechts 10 methoden te implementeren in plaats van 19:
// door afgeleide klassen geïmplementeerde methoden ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
Laten we enkele methoden van de klasse [AbstractDao] bekijken.
Methode [getShortEntitiesById]
Deze methode is bedoeld om de korte versie van entiteiten op te halen waarvan de primaire sleutels worden opgegeven.
// injecties
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// lokaal
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- regels 2-4: de bean [maxPreparedStatementParameters], gedefinieerd in het configuratiebestand [ConfigJdbc], wordt geïnjecteerd. Dit configuratiebestand configureert de laag JDBC van een specifieke SGBD:
// maximaal aantal parameters van een [PreparedStatement]
public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
@Bean(name = "maxPreparedStatementParameters")
public int maxPreparedStatementParameters() {
return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
- regels 1-7: definiëren de bean [maxPreparedStatementParameters] die het maximale aantal parameters vaststelt dat aan een type [PreparedStatement] kan worden doorgegeven. Deze behoefte deed zich niet voor bij de SGBD en MySQL, die 10.000 parameters accepteerden voor een type [PreparedStatement]. Tijdens tests met de SGBD en SQL-server werd er een uitzondering gegenereerd waarin werd aangegeven dat het maximale aantal parameters voor een type [PreparedStatement] 2100 was. Daarom is dit getal een configuratieparameter geworden voor de verschillende SGBD-instellingen. Het moet dus worden opgenomen in het configuratieproject [sgbd-config-jdbc] van elke SGBD;
Laten we teruggaan naar de code van de methode [getShortEntitiesById]:
// injecties
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// lokaal
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- regel 7: de naam van de klasse. Wordt gebruikt als parameter voor een van de constructors van de uitzonderingsklasse [DaoException];
- regel 10: de annotatie [@Transactional(readOnly = true)] geeft aan dat de methode binnen een alleen-lezen-transactie moet worden uitgevoerd. Men kan zich afvragen wat het nut is van een dergelijke transactie, aangezien de methode alleen leesbewerkingen uitvoert en er dus in geval van een fout niets ongedaan gemaakt hoeft te worden. De auteur van de bibliotheek [Spring Data] adviseert dit en legt uit waarom. Ik heb zijn advies opgevolgd;
De hoofdtekst van de methode is als volgt:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// geldigheid van het argument
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
...
}
- regel 5: de geldigheid van de parameter [ids] wordt gecontroleerd door de volgende methode:
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
// elementen null?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
}
// lege elementen?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T>();
}
}
// standaardresultaat
return null;
}
- regel 1: de methode [checkNullOrEmptyArgument] is een generieke methode die wordt geparametriseerd door het type <T2>. T2 is het type van de elementen die als tweede parameter aan de methode worden doorgegeven. Dit kan bijvoorbeeld [Long, String, AbstractCoreEntity] zijn;
- regel 1: de methode [checkNullOrEmptyArgument] heeft twee parameters:
- [Iterable<T2> elements]: de te testen parameter;
- [checkEmpty]: wordt op waar gezet als moet worden getest of de vorige parameter een niet-lege lijst is;
- regels 4-6: er wordt gecontroleerd of de parameter [elements] niet null is. Als dat niet het geval is, wordt er een uitzondering van het type [MyIllegalArgumentException] gegenereerd;
- regels 8-15: als de lijst leeg is en er gecontroleerd moest worden of deze niet leeg was, wordt er een uitzondering van het type [MyIllegalArgumentException] gegenereerd;
- regel 13: als de lijst leeg is en er niet gecontroleerd hoeft te worden of deze niet leeg is, dan wordt een lege lijst met elementen van het type T geretourneerd. De interface [Iterable<T2>] heeft een methode [iterator()] waarmee de elementen van de lijst die de interface implementeert, kunnen worden doorlopen. Twee methoden van deze iterator zijn nuttig:
- [itérateur].hasNext(): retourneert ‘waar’ als de lijst nog een element bevat dat kan worden verwerkt, anders ‘onwaar’;
- [iterateur].next(): retourneert het huidige element van de lijst en gaat één element verder;
- uiteindelijk,
- als het argument [T2... elements] gelijk is aan null of leeg is, wordt er een uitzondering van het type [MyIllegalArgumentException] gegenereerd;
- als het argument [T2... elements] een lege lijst is en dit toegestaan was, dan wordt een lege lijst met elementen van het type T geretourneerd;
Er bestaat een vergelijkbare methode wanneer het te testen argument van het type [T2... elements] is:
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
Laten we terugkeren naar de code van de methode [getShortEntitiesById]:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// geldigheid van het argument
List<T> entities = checkNullOrEmptyArgument(true, ids);
// in delen ophalen
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// resultaat
return entities;
}
- regel 7: als we hier terechtkomen, betekent dit dat het argument [Iterable<Long> ids] geldig is;
- regels 7-14: we zullen later zien dat de methode [getShortEntitiesById] geïmplementeerd zal worden door een type [PreparedStatement], dat als parameters de lijst met te zoeken primaire sleutels zal hebben. Bijvoorbeeld:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
: ids is een parameter waarvan de daadwerkelijke waarde een type List<Long> zal zijn. Elk element van deze lijst zal het onderwerp vormen van een parameter ? in een type [PreparedStatement]. We hebben echter gezegd dat dit type een maximaal aantal parameters accepteert, een aantal dat wordt bepaald door het veld [maxPreparedStatementParameters] van de klasse;
- regel 7: de lijst met T-entiteiten die door de methode [getShortEntitiesById] wordt geretourneerd. Deze lijst wordt opgebouwd uit stukken van [maxPreparedStatementParameters]-elementen;
- regel 9: op basis van het argument [Iterable<Long> ids] wordt een type [List<Long> listIds] aangemaakt. De klasse [Lists] is een klasse uit de Google Guava-bibliotheek die talrijke statische methoden biedt voor het bewerken van verzamelingen van objecten. De Google Guava-bibliotheek is geïmporteerd (pom.xml) door het Maven-project [mysql-config-jdbc]:
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- regel 10: het aantal T-entiteiten dat in de database moet worden gezocht;
- regels 11-13: ze worden gezocht in groepen van [taille = maxPreparedStatementParameters] elementen;
- regel 12: een berekening om te voorkomen dat het einde van de lijst wordt overschreden;
- regel 13: de T-entiteiten worden verkregen via de aanroep [getShortEntitiesById(listIds.subList(i, limit))]. Deze methode is in de klasse gedefinieerd door:
abstract protected List<T> getShortEntitiesById(List<Long> ids);
Het is dus de onderliggende klasse die de T-entiteiten uit de database gaat ophalen:
- [DaoProduit] als T van het type [Produit] is;
- [DaoCategorie] als T van het type [Categorie] is;
Het voordeel van deze bewerking door de bovenliggende klasse is tweeledig:
- de signatuur van de methode [getShortEntitiesById] in de dochterklasse is uniek: het argument ervan is van het type [List<Long> ids];
- de dochterklasse hoeft zich niet bezig te houden met het probleem van de [maxPreparedStatementParameters]-parameters van een [PreparedStatement]. Haar bovenliggende klasse heeft dat voor haar geregeld;
- regel 13: de entiteiten die door de dochterklasse worden teruggegeven, worden toegevoegd aan de lijst met entiteiten die door de bovenliggende klasse wordt geretourneerd (regel 16);
Laten we nu eens kijken naar de implementatie van de andere methode [getShortEntitiesById] van de klasse:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// geldigheid van het argument
List<T> entities = checkNullOrEmptyArgument(true, ids);
// resultaat
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
- regel 3: de aard van het argument is veranderd: Long... ids;
- regel 5: de geldigheid van dit argument wordt getest;
- regel 7: de methode [getShortEntitiesById] wordt aangeroepen, die we zojuist hebben beschreven. Ook hier maken we gebruik van de klasse [Lists] uit de bibliotheek [Google Guava]. Merk op dat we een expliciete cast naar het type [Iterable<Long>] moeten uitvoeren om de compiler te helpen de juiste methode te kiezen, omdat de methode [getShortEntitiesById] drie signaturen heeft in de klasse:
- List<T> getShortEntitiesById(Long... ids);
- List<T> getShortEntitiesById(Iterable<Long> ids);
- List<T> getShortEntitiesById(List<Long> ids), die abstract is en door de afgeleide klasse wordt geïmplementeerd;
We zullen verder geen toelichting geven op de abstracte klasse [AbstractDao], de bovenliggende klasse van de klassen [DaoProduit] en [DaoCategorie]. We onthouden alleen dat het soms interessant is om gedrag dat meerdere klassen gemeen hebben, te factoriseren in een bovenliggende klasse, al dan niet abstract. Na deze aanpassing hoeven de onderliggende klassen alleen nog de volgende methoden te implementeren:
// door de afgeleide klassen geïmplementeerde methoden ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
De code in paragraaf 4.8 toont de verschillende transactietypen die voor elke methode worden gebruikt. Laten we enkele punten opmerken:
- de methoden die de database lezen, zijn voorzien van de annotatie [@Transactional(readOnly = true)];
- methoden die de database wijzigen, zijn gemarkeerd met [@Transactional];
- de methoden [delete] zijn niet geannoteerd en vinden dus niet plaats binnen een transactie. Het idee hierachter is dat als een verwijdering mislukt, de gebruiker waarschijnlijk niet alle eerdere succesvolle verwijderingen ongedaan wil maken;
4.9. De klasse [DaoCategorie]
![]() |
![]() |
De klasse [DaoCategorie] implementeert de interface [IDao<Categorie>], die zorgt voor detoegang tot de gegevens in de tabel [CATEGORIES] van de database MySQL [dbproduitscategories]. De structuur ervan is als volgt:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
// constanten
// injecties
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
@Autowired
private IDao<Produit> daoProduit;
@Override
public List<Categorie> getAllShortEntities() {
...
}
@Override
public List<Categorie> getAllLongEntities() {
...
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
...
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
...
}
// --------------------- mappers
class ShortCategorieMapper implements RowMapper<Categorie> {
....
}
class LongCategorieMapper implements RowMapper<Categorie> {
....
}
- regel 28: de klasse [DaoCategorie] is een Spring-component en kan als zodanig in andere Spring-componenten worden geïnjecteerd;
- regel 29: de klasse [DaoCategorie] is een afgeleide van de abstracte klasse [AbstractDao<Categorie>], waardoor het een implementatie is van de interface [IDao<Categorie>];
- regels 34-37: injectie van beans die zijn gedefinieerd in de klasse [AppConfig], zoals beschreven in paragraaf 4.4;
- regels 38-39: injectie van een verwijzing naar de klasse [DaoProduit], die de interface [IDao<Produit>] implementeert, welke de toegang tot de gegevens van de tabel [PRODUITS] beheert;
- regels 41-89: implementatie van de interface [IDao<Categorie>];
- regels 95-101: twee interne klassen die de interface [RowMapper<T>] implementeren;
Laten we de methoden een voor een bekijken.
4.9.1. De methode [getAllShortEntities]
De methode [getAllShortEntities] zet alle categorieën uit de tabel [CATEGORIES] om naar hun verkorte versie:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Alle methoden zijn gebaseerd op het object [namedParameterJdbcTemplate], dat is gedefinieerd in het Spring-configuratiebestand en wordt geleverd door de Spring-bibliotheek JDBC. Dit object beschikt over talrijke methoden. De hierboven gebruikte methode is de volgende:
![]()
- [sql] is de uit te voeren opdracht SQL;
- [rowMapper] is een instantie van de volgende interface [RowMapper<T>]:

Het idee is als volgt:
- de methode [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) voert de opdracht SQL van het type [Select] uit. De methode verwerkt eventuele uitzonderingen en zorgt voor het openen en sluiten van de verbinding met SGBD. Het enige wat de methode niet kan doen, isde elementen van het [ResultSet]-type in te kapselen in de objecten die het ontvangt van het type [Categorie], omdat het de koppeling tussen de velden van het type [Categorie] en de kolommen van het [Resultset] niet kent. We zullen later zien dat deze koppeling wordt gelegd met behulp van de technologie JPA, waardoor het inkapselen van de elementen van een [ResultSet] in instanties van het type T automatisch verloopt. Voorlopig is de tweede parameter van de methode [query] een instantie van de interface [RowMapper<T>] die deze inkapseling kan uitvoeren;
Laten we teruggaan naar de code:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
De volgorde SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] is als volgt:
public final static String SELECT_ALLSHORTCATEGORIES = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c";
De query vraagt de kolommen [ID, VERSIONING, NOM] van de elementen in de tabel [CATEGORIES] op. We zullen systematisch de volgende syntaxis gebruiken:
SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...
Belangrijk is de naamgeving van de kolommen die worden verkregen via SELECT met het attribuut [as nom_colonne]. Dit is de enige manier om de gegevens tussen SGBD uitwisselbaar te maken, omdat deze allemaal een eigen manier hebben om de kolommen te benoemen die worden verkregen via een SELECT, waarbij kolommen uit verschillende tabellen dezelfde naam hebben (bijvoorbeeld ID, NOM of VERSIONING in ons geval). We nemen deze dubbelzinnigheid dan weg door zelf aan te geven welke naam deze kolommen moeten krijgen.
De interne klasse [ShortCategorieMapper] ziet er als volgt uit:
class ShortCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSIONING"), rs.getString("c_NOM"), null);
}
}
- regel 1: de klasse [ShortCategorieMapper] implementeert de interface [RowMapper<Categorie>] en moet daarom de methode [mapRow] van de regels 4-5, die tot doel heeft een regel van de [ResultSet rs] – gegenereerd door de opdracht [SELECT] – in te kapselen in een type [Categorie];
- regel 5: deze inkapseling is voltooid. Merk op dat de naam die door de methoden [rs.getType(nom)] wordt gebruikt, dezelfde is als de naam die wordt gebruikt in de attributen [as nom] van de kolommen van SELECT;
We hebben dus de lijst met categorieën in hun verkorte versie verkregen zonder uitzonderingen of verbindingen te hoeven beheren. Dat is het voordeel van de Spring-bibliotheek JDBC, die alles beheert wat kan worden gestandaardiseerd in het beheer van tabelelementen en de ontwikkelaar de taken overlaat die niet kunnen worden gestandaardiseerd.
4.9.2. De methode [getAllLongEntities]
De methode [getAllLongEntities] geeft alle categorieën uit de tabel [CATEGORIES] weer in hun lange versie:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
De volgorde SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] is als volgt:
public final static String SELECT_ALLLONGCATEGORIES = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID";
Het gaat erom de categorieën samen met hun producten weer te geven. Dit wordt bereikt door een join uit te voeren tussen de tabel [CATEGORIES] en de tabel [PRODUITS] via de vreemde sleutel [CATEGORIE_ID], diede tabel [PRODUITS] met de tabel [CATEGORIES] verbindt. Met de syntaxis [FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID] kunnen ook categorieën worden opgehaald waaraan geen producten zijn gekoppeld. In dit geval haalt de query SELECT een categorie en een product op, waarbij alle kolommen in NULL zijn opgenomen.
De klasse [LongCategorieMapper] is als volgt:
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// geval van de categorie zonder producten
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
- regel 4: de methode [mapRow] moet een object [Categorie] retourneren waarbij het veld [produits] is ingevuld, en wel op basis van een regel uit het [ResultSet]-object dat afkomstig is van de voorgaande opdracht SELECT;
Uiteindelijk is de bewerking:
[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]
levert een lijst op van het type:
waarbij elke categorie [ci] een veld [produits] heeft dat een productlijst is met slechts één element [produitsij]. Wij hebben echter de volgende lijst nodig:
waarbij elke categorie [ci] een veld [produits] zal hebben dat de lijst met producten [produiti1, produiti2, ...] bevat. Dit wordt bereikt door de verkregen lijst met categorieën door te geven aan een privémethode [filterCategories]:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
De methode [filterCategories] is als volgt:
private List<Categorie> filterCategories(List<Categorie> categories) {
if (categories.size() == 0) {
return categories;
}
// weer te geven categorieën
List<Categorie> cats = new ArrayList<Categorie>();
// de lijst met verkregen categorieën wordt doorlopen
for (Categorie categorie : categories) {
boolean trouve = false;
for (Categorie cat : cats) {
if (categorie.equals(cat)) {
cat.addProduit(categorie.getProduits().get(0));
trouve = true;
break;
}
}
// gevonden?
if (!trouve) {
cats.add(categorie);
}
}
// resultaat
return cats;
}
- regel 1: [List<Categorie> categories] is de lijst met categorieën die moeten worden gefilterd (of gegroepeerd);
- regel 6: de lijst met categorieën die naar de aanroeper moeten worden teruggestuurd;
- regels 8-21: elke categorie uit de te filteren lijst wordt verwerkt;
- regels 10-16: er wordt nagegaan of de huidige categorie [categorie] al voorkomt in de te samen te stellen lijst met categorieën [cats] (ter herinnering: twee categorieën worden als gelijk beschouwd als ze dezelfde primaire sleutel hebben, zie paragraaf 4.6);
- regels 11-14: als dat al het geval is, wordt het product dat in [categorie] is ingekapseld, toegevoegd aan de productlijst van [cat];
- regels 18-20: als de huidige categorie [categorie] nog niet voorkomt in de lijst met aan te maken categorieën [cats], dan wordt deze eraan toegevoegd met de bijbehorende productlijst die één enkel element bevat;
Laten we eens kijken naar het geval waarin de opdracht SQL Select categorieën oplevert zonder bijbehorende producten. Welke entiteit levert de klasse [LongCategorieMapper] op?
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// geval van de categorie zonder producten
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
In het geval dat de selectieopdracht SQL een categorie zonder producten heeft geretourneerd, bevatten de kolommen van het product dat samen met de categorie is geretourneerd allemaal de waarde SQL NULL. Dit geval wordt behandeld in de regels 7-9:
- regel 7: de primaire sleutel van het product wordt opgehaald als een long-geheelgetal;
- regel 9: er wordt gekeken of de gelezen waarde de waarde SQL NULL (rs.wasNull) was. Als dat niet het geval is, wordt het product toegevoegd aan de lijst in regel 6; anders wordt er niets toegevoegd en blijft de productlijst leeg.
Merk op dat er in alle gevallen een categorie wordt geretourneerd met een veld [produits] dat niet null is.
4.9.3. De methode [getShortEntitiesById]
De methode [getShortEntitiesById] is vergelijkbaar met de methode [getAllShortEntities], behalve dat deze alleen de entiteiten retourneert waarvan de primaire sleutels in een lijst zijn opgegeven:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
- regel 4 is de handtekening van de gebruikte methode [query] als volgt:

De eerste parameter is een geconfigureerde opdracht SQL [Select]. De tweede is een woordenboek waarin elke parameter aan een waarde is gekoppeld. De derde is de instantie van de klasse die een regel van het [ResultSet]-resultaat van de [Select]-opdracht inkapselen in een object van het type T;
- regel 4: de geconfigureerde opdracht SQL [Select] is als volgt:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
Deze opdracht haalt uit de tabel [CATEGORIES] de categorieën waarvan de primaire sleutels in de lijst 'ids' staan.
- regel 5: de tweede parameter van de methode [query] is hier een woordenboek dat de sleutel 'ids' (1e parameter) koppelt aan de lijst [ids] die in regel 1 als parameter is doorgegeven aan de methode [getShortEntitiesById]. De klasse [Collections] behoort tot de bibliotheek [Google Guava] waarover we het al hebben gehad. [Collections.singleMap] retourneert een woordenboek met één element;
- regel 5: de klasse die verantwoordelijk is voor het inkapselen van een regel uit de [ResultSet] – het resultaat van de methode [Select] – in een object van het type [Categorie], is de reeds besproken klasse [ShortCategorieMapper];
Dit is typisch het moment waarop de bean [maxPreparedStatementParameters] in actie komt. De parameter [:ids] van de opdracht SQL, die een lijst met primaire sleutels vertegenwoordigt, kan namelijk 1 tot enkele duizenden parameters bevatten. Er geldt een limiet voor dit aantal, die afhankelijk is van elke SGBD. Voor MySQL konden we zonder fouten 10.000 parameters doorgeven; we hebben niet getest of het aantal hoger kan. Voor SQL Server is de officiële limiet 2100. Voor Firebird was 1000 al te veel. We zijn teruggegaan naar 100. Over het algemeen hebben we de maximale limiet van dit aantal voor de verschillende SGBD-bestanden niet getest.
4.9.4. De methode [getLongEntitiesById]
De methode [getLongEntitiesById] is vergelijkbaar met de methode [getShortEntitiesById], behalve dat deze de lange versies van de categorieën weergeeft:
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
Regel 4, de query SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] is als volgt:
public final static String SELECT_LONGCATEGORIE_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.ID in (:ids)";
4.9.5. De methode [getShortEntitiesByName]
De methode [getShortEntitiesByName] is vergelijkbaar met de methode [getShortEntitiesById], behalve dat de categorieën worden opgezocht op naam in plaats van op primaire sleutel:
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
Regel 4, de volgorde SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] is als volgt:
public final static String SELECT_SHORTCATEGORIE_BYNAME = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.NOM in (:noms)";
4.9.6. De methode [getLongEntitiesByName]
De methode [getLongEntitiesByName] is vergelijkbaar met de methode [getShortEntitiesByName], behalve dat de categorieën in hun volledige vorm worden gezocht:
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(215, e, simpleClassName);
}
}
Regel 4, de volgorde SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] is als volgt:
public final static String SELECT_LONGCATEGORIE_BYNAME = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.NOM in(:noms)";
4.9.7. De methode [deleteAllEntities]
De methode [deleteAllEntities] verwijdert alle categorieën uit de tabel [CATEGORIES]:
@Override
public void deleteAllEntities() {
try {
// alle categorieën worden verwijderd en vervolgens alle bijbehorende producten
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
- regel 4: de gebruikte methode [namedParameterJdbcTemplate.update] heeft de volgende handtekening:
![]()
De eerste parameter is een SQL-opdracht die is ingesteld voor een update (INSERT, UPDATE, DELETE). De tweede parameter is het woordenboek dat waarden koppelt aan de verschillende parameters van de opdracht SQL. De methode retourneert het aantal regels dat door de opdracht SQL is bijgewerkt.
- regel 4: de opdracht SQL [ConfigJdbc.DELETE_ALLCATEGORIES] luidt als volgt:
public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";
Het gaat dus niet om een geparametriseerde opdracht. Daarom heeft de tweede parameter van de methode [update] de waarde null.
4.9.8. De methode [deleteAllEntitiesById]
De methode [deleteAllEntitiesById] verwijdert de categorieën uit de tabel [CATEGORIES] waarvan de primaire sleutels worden doorgegeven:
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYID, Collections.singletonMap("ids", ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
Regel 4, de volgorde SQL [ConfigJdbc.DELETE_CATEGORIESBYID] is als volgt:
public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";
4.9.9. De methode [deleteAllEntitiesByName]
De methode [deleteAllEntitiesByName] verwijdert de categorieën uit de tabel [CATEGORIES] waarvan de namen worden doorgegeven:
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYNAME, Collections.singletonMap("noms", names));
} catch (Exception e) {
throw new DaoException(225, e, simpleClassName);
}
}
Regel 4, de opdracht SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] luidt als volgt:
public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";
4.9.10. De methode [saveEntities]
4.9.10.1. De code
De signatuur van deze methode is als volgt:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
De methode ontvangt als parameter een lijst met categorieën. Ze voert daarop de volgende bewerkingen uit:
- als de categorie een primaire sleutel null heeft, wordt een bewerking SQL INSERT uitgevoerd; anders wordt een bewerking SQL UPDATE uitgevoerd;
- deze bewerking wordt herhaald voor elk product in de categorie;
De methode retourneert de lijst met opgeslagen of bijgewerkte categorieën. De geretourneerde lijst is een exacte weergave van de categorieën en producten die in de tabellen aanwezig zijn, op versies na: deze worden namelijk niet gewijzigd in de bijgewerkte entiteiten, ook al zijn ze in de database verhoogd.
Dit is veruit de meest complexe methode. De code ervan is als volgt:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// --------------------------------------------- categorieën
List<Categorie> insertCategories = new ArrayList<Categorie>();
List<Categorie> updateCategories = new ArrayList<Categorie>();
// de categorieën worden gescand
for (Categorie categorie : entities) {
// toevoegen of bijwerken?
if (categorie.getId() == null) {
insertCategories.add(categorie);
} else {
updateCategories.add(categorie);
}
}
// categorieën invoegen
if (insertCategories.size() > 0) {
insertCategories(insertCategories);
}
// categorieën bijwerken
if (updateCategories.size() > 0) {
updateCategories(updateCategories);
}
// --------------------------------------------- producten
// de producten in de categorieën worden bijgewerkt
List<Produit> allProduits = new ArrayList<Produit>();
for (Categorie categorie : entities) {
List<Produit> produits = categorie.getProduits();
Long idCategorie = categorie.getId();
if (produits != null) {
// wordt toegevoegd aan de lijst met alle producten
allProduits.addAll(produits);
// de producten worden één voor één gescand om ze aan hun categorie te koppelen
for (Produit produit : produits) {
// het product wordt aan de categorie gekoppeld
produit.setIdCategorie(idCategorie);
produit.setCategorie(categorie);
}
}
}
// producten invoeren / bijwerken
daoProduit.saveEntities(allProduits);
// resultaat
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(207, e, simpleClassName);
}
}
- regels 5-23: toevoegen of bijwerken van categorieën;
- regels 26-43: toevoegen of bijwerken van producten;
- regels 35-39: deze code koppelt elk product aan zijn categorie. In de voorgaande fase, waarin de categorieën werden ingevoerd, hebben deze een primaire sleutel gekregen die in het veld [idCategorie] van het product moet worden ingevuld (regel 37). Bovendien maken de regels 37-38 het mogelijk om situaties te corrigeren waarin de gebruiker niet elk product correct aan zijn categorie heeft gekoppeld. Om ervoor te zorgen dat deze koppeling correct is, moet de methode [Categorie] .add(Product p) worden gebruikt, maar niets weerhoudt een gebruiker ervan om een product rechtstreeks aan de lijst met producten van de categorie toe te voegen zonder deze methode te gebruiken, met het risico dat de velden [idCategorie, categorie] van product p onjuist worden ingevuld;
- regel 43: we delegeren het opslaan / bijwerken van de producten aan de instantie van de interface [IDao<Produit>]. Ter herinnering: deze instantie is geïnjecteerd in de klasse [DaoCategorie]:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. Invoegen van categorieën
De categorieën worden in de tabel [CATEGORIES] ingevoegd via de volgende privémethode [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// toe te voegen categorieën
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// de primaire sleutel wordt opgeslagen
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// alles is OK - de primaire sleutels worden toegewezen aan de opgeslagen categorieën
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// resultaat
return categories;
}
- regel 6: er wordt gebruikgemaakt van de bean [simpleJdbcInsertCategorie], die via de volgende regels in de klasse wordt geïnjecteerd:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
Deze bean is in de klasse [AppConfig] van het project als volgt gedefinieerd:
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_CATEGORIES_ID)
.usingColumns(ConfigJdbc.TAB_CATEGORIES_NOM);
}
- regel 5: de klasse [SimpleJdbcInsert] is een klasse uit de Spring-bibliotheek JDBC (regel 1):
- de parameter van de constructor [SimpleJdbcInsert] is de gegevensbron waarop wordt gewerkt;
- met de clausule [withTableName] kan de tabel worden aangegeven waarin een element moet worden ingevoegd, in dit geval de tabel [CATEGORIES];
- met de clausule [usingGeneratedKeyColumns] kan de kolom van de automatisch gegenereerde primaire sleutel worden gespecificeerd, in dit geval de kolom [ID];
- met de clausule [usingColumns] kan het invoegen worden beperkt tot bepaalde kolommen. Hier sluiten we de kolom [ID] uit, die automatisch wordt gegenereerd door SGBD, en de kolom [VERSIONING], die een standaardwaarde van 1 heeft;
Laten we teruggaan naar de code van de methode [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// toe te voegen categorieën
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// de primaire sleutel wordt opgeslagen
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// alles is OK - de primaire sleutels worden toegewezen aan de opgeslagen categorieën
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// resultaat
return categories;
}
- regel 6: de methode [simpleJdbcInsertCategorie.executeAndReturnKey] wordt gebruikt:
![]()
De methode verwacht als parameter een woordenboek dat de koppelingen legt tussen de kolommen van de tabel en de waarden die daarin moeten worden ingevoegd. Het resultaat is de primaire sleutel in de vorm van een type [Number]. Met de methode [Number.longValue()] kan de primaire sleutel worden verkregen in de vorm van een type [Long].
De methode [getMapForCategorie] is de volgende privémethode:
private Map<String, ?> getMapForCategorie(Categorie categorie) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
return map;
}
De sleutels van het woordenboek zijn de namen van de kolommen die moeten worden ingevuld: [NOM], en de waarden van het woordenboek zijn de waarden die in deze kolommen moeten worden ingevoerd.
- regel 8 [insertCategories]: de opgehaalde primaire sleutel wordt opgeslagen in een woordenboek. We wachten tot we zeker weten dat alle entiteiten zijn ingevoegd voordat we ze hun primaire sleutels toewijzen. In het geval van een uitzondering worden namelijk alle invoegingen ongedaan gemaakt en we willen dat de entiteiten [categories] uit regel 1 dan ook ongewijzigd blijven;
- regels 14-17: nu we zeker weten dat alles goed is verlopen, wijzen we de gegenereerde primaire sleutels toe aan de categorieën;
- regel 19: we geven de lijst met categorieën en hun primaire sleutels weer;
4.9.10.3. Categorieën bijwerken
De categorieën worden bijgewerkt met de volgende privémethode [updateCategories]:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// de categorie in de database bijwerken
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// is het gelukt?
Long idCategorie = null;
if (nbLignes == 0) {
// het is niet gelukt – we zoeken uit waarom
// de categorie wordt in de database gezocht
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// de categorie bestaat niet
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// de versie klopte niet
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
Het bijwerken van een categorie C1 in de database met een categorie C2 in het geheugen is alleen toegestaan als de categorieën C1 en C2 dezelfde versie hebben. Dit versienummer dient om te voorkomen dat de entiteit tegelijkertijd 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 treedt een uitzondering op omdat het een versie (V1) heeft die verschilt van die in de database (V1+1).
- regels 2-29: de try-blok heeft twee catch-blokken:
- de eerste, op regel 25, is bedoeld om de eventuele uitzondering van het type [DaoException] door te laten, die door de code op regel 13 wordt gegenereerd;
- de tweede, op regel 27, is bedoeld om de andere soorten uitzonderingen af te handelen;
- regel 3: alle bij te werken categorieën worden gescand;
- regel 4: de huidige categorie wordt bijgewerkt met de methode [namedParameterJdbcTemplate.update]:

- laten we de instructie analyseren:
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES, new BeanPropertySqlParameterSource(categorie));
De volgorde SQL [ConfigJdbc.UPDATE_CATEGORIES] is als volgt:
public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";
De opdracht heeft drie parameters (:id, :version, :nom) waarvan de waarden zich bevinden in de velden met dezelfde naam van het gewijzigde object [categorie]. We maken gebruik van deze eigenschap door als tweede parameter [new BeanPropertySqlParameterSource(categorie)] door te geven, wat aangeeft: "de waarden van de parameters bevinden zich in de velden met dezelfde naam van deze Java-bean";
Het resultaat van deze bewerking, wanneer deze normaal verloopt, is het aantal gewijzigde regels, dus 0 of 1.
Laten we terugkeren naar de besproken code:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// de categorie in de database is bijgewerkt
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// Is het gelukt?
Long idCategorie = null;
if (nbLignes == 0) {
// het is niet gelukt – we zoeken uit waarom
// de categorie wordt in de database gezocht
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// de categorie bestaat niet
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// de versie klopte niet
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
- regel 9: er wordt gecontroleerd of de wijziging is gelukt;
- regel 10: de wijziging is niet gelukt. Aangezien de clausule [WHERE] betrekking heeft op de kolommen [ID] en [VERSIONING], zoeken we de kolom die ervoor heeft gezorgd dat [WHERE] is mislukt;
- regels 12-18: er wordt gecontroleerd of de sleutel [id] van de categorie in de database aanwezig is. Als dat niet het geval is, wordt een [RuntimeException] gestart met een passend foutbericht;
- regels 19-22: behandelen het geval waarin de versie niet correct was;
4.10. De klasse [DaoProduit]
![]() |
![]() |
De klasse [DaoProduit] implementeert de interface [IDao<Produit>], die zorgt voor detoegang tot de gegevens van de tabel [PRODUITS] in de database MySQL [dbproduitscategories]. De structuur ervan is als volgt:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
// injecties
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
@Override
public List<Produit> getAllShortEntities() {
...
}
@Override
public List<Produit> getAllLongEntities() {
....
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
....
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
....
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
}
// --------------------- mappers
class ShortProduitMapper implements RowMapper<Produit> {
...
}
class LongProduitMapper implements RowMapper<Produit> {
...
}
De code lijkt sterk op die van de klasse [DaoCategorie]. We zullen slechts enkele methoden bekijken.
4.10.1. De methode [getShortEntitiesById]
De methode [getShortEntitiesById] genereert de verkorte versie van de producten waarvan de primaire sleutels worden doorgegeven:
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTPRODUIT_BYID,
Collections.singletonMap("ids", ids), new ShortProduitMapper());
} catch (Exception e) {
throw new DaoException(109, e, simpleClassName);
}
}
- regel 4: de opdracht SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] is als volgt:
public final static String SELECT_SHORTPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSIONING, p.NOM as p_NOM, p.CATEGORIE_ID as p_CATEGORIE_ID, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION FROM PRODUITS p WHERE p.ID in (:ids)";
- regel 4: de klasse [ShortProduitMapper], die [ResultSet] in een productlijst moet inkapselen, is als volgt:
class ShortProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSIONING"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), null);
}
}
4.10.2. De methode [getLongEntitiesByName]
De methode [getShortEntitiesById] genereert de lange versie van de producten waarvan de namen worden doorgegeven:
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
- regel 4: de opdracht SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] is als volgt:
public final static String SELECT_LONGPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p, CATEGORIES c WHERE p.ID in (:ids) AND p.CATEGORIE_ID=c.ID";
- regel 4: de klasse [LongProduitMapper], die de elementen van [ResultSet] in producten (lange versie) moet inkapselen, is als volgt:
class LongProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSION"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null));
}
}
4.10.3. De methode [saveEntities]
De methode [saveEntities] wordt zowel gebruikt om nieuwe producten toe te voegen (id==null) als om bestaande producten bij te werken (id!=null):
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
try {
// toe te voegen producten
List<Produit> insertProduits = new ArrayList<Produit>();
// te updaten producten
List<Produit> updateproduits = new ArrayList<Produit>();
// de lijst met ontvangen entiteiten wordt gescand
for (Produit produit : entities) {
Long id = produit.getId();
if (id == null) {
insertProduits.add(produit);
} else {
updateproduits.add(produit);
}
}
// toevoegingen
insertProduits(insertProduits);
// wijzigingen
updateProduits(updateproduits);
// resultaat
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
Regel 18: de toe te voegen producten worden toegevoegd via de volgende privémethode [insertProduits]:
private List<Produit> insertProduits(List<Produit> produits) {
Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
try {
// toe te voegen producten
for (Produit produit : produits) {
Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
// de primaire sleutel wordt genoteerd
mapProduits.put(newId.longValue(), produit);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// alles is OK - de primaire sleutels worden toegewezen aan de opgeslagen producten
for (Long id : mapProduits.keySet()) {
Produit produit = mapProduits.get(id);
produit.setId(id);
}
// resultaat
return produits;
}
private Map<String, ?> getMapForProduit(Produit produit) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_PRODUITS_NOM, produit.getNom());
map.put(ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, produit.getIdCategorie());
map.put(ConfigJdbc.TAB_PRODUITS_PRIX, produit.getPrix());
map.put(ConfigJdbc.TAB_PRODUITS_DESCRIPTION, produit.getDescription());
return map;
}
Deze methode is vergelijkbaar met de methode [insertCategories] die in paragraaf 4.9.10.3 is besproken.
- regel 4: er wordt gebruikgemaakt van de bean [simpleJdbcInsertProduit] die in de klasse is geïnjecteerd:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
Deze bean is gedefinieerd in de klasse [AppConfig], die het project configureert:
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource)
.withTableName(ConfigJdbc.TAB_PRODUITS)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_PRODUITS_ID)
.usingColumns(ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION,ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID);
}
- regels 3-6: de bean [simpleJdbcInsertProduit]
- is gekoppeld aan de gegevensbron van de database [dbproduitscategories] (regel 3) en aan de tabel [ConfigJdbc.TAB_PRODUITS] van deze bron (regel 4);
- de primaire sleutel van deze tabel wordt gegenereerd in kolom [ConfigJdbc.TAB_PRODUITS_ID] (regel 5);
- er worden alleen waarden toegekend aan de kolommen [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (regel 6);
De methode [updateProduits] die de producten bijwerkt (regel 20 van [saveEntities]) is als volgt:
private void updateProduits(List<Produit> updateProduits) {
try {
// de producten worden gescand
for (Produit produit : updateProduits) {
// het product wordt bijgewerkt in de database
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
new BeanPropertySqlParameterSource(produit));
// is het gelukt?
Long idProduit = null;
if (nbLignes == 0) {
// het is niet gelukt – we zoeken uit waarom
// het product wordt in de database opgezocht
idProduit = produit.getId();
List<Produit> produitsInBd = getShortEntitiesById(idProduit);
if (produitsInBd.size() == 0) {
// het product bestaat niet
throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
idProduit));
} else {
// de versie klopte niet
throw new RuntimeException(String.format(
"Erreur de mise à jour. Le produit de clé [%s] n'a pas la bonne version", idProduit));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
Deze is vergelijkbaar met de opdracht waarmee de categorieën worden bijgewerkt (zie paragraaf 4.9.10.3). Op regel 23 is de opdracht SQL [ConfigJdbc.UPDATE_PRODUITS], die wordt uitgevoerd om de producten bij te werken, als volgt:
public final static String UPDATE_PRODUITS = "UPDATE PRODUITS SET VERSIONING=VERSIONING+1, NOM=:nom, PRIX=:prix, CATEGORIE_ID=:idCategorie, DESCRIPTION=:description WHERE ID=:id AND VERSIONING=:version";
De namen van de parameters [:id,:version,:nom,:prix,:idCategorie,:description] zijn tevens de namen van de velden van de klasse [Produit], waardoor de instructie in de regels 6-7 kan worden gebruikt om het huidige product bij te werken.
4.11. De testlaag
![]() |
![]() |
De testlaag bestaat uit drie testklassen:
- [JUnitTestCheckArguments]: de tests van deze klasse roepen de verschillende methoden van de laag [DAO] aan met ongeldige argumenten en controleren of deze correct reageren;
- [JUnitTestDao]: de tests van deze klasse roepen de verschillende methoden van de laag [DAO] aan en controleren of deze doen wat er van hen verwacht wordt;
- [JUnitTestPushTheLimits] is niet bedoeld om de laag [DAO] te testen, maar om de prestaties ervan te meten;
Deze testlaag speelt een belangrijke rol in dit document. Ze is namelijk gemeenschappelijk voor alle implementaties van de interface [IDao<T>]. Er zijn er zes per SGBD (1 implementatie van JDBC, 3 implementaties van JPA, 1 Spring-implementatie MVC, 1 beveiligde Spring-implementatie MVC), dus 36 voor de zes geteste SGBD-implementaties. Met de testlaag kunnen we controleren of alle implementaties op dezelfde manier reageren.
4.11.1. De test [JUnitTestCheckArguments]
De testklasse [JUnitTestCheckArguments] bevat 48 methoden die de reactie testen van de methoden van de laag [DAO] wanneer deze met onjuiste argumenten worden aangeroepen. De structuur ervan is als volgt:
package spring.jdbc.tests;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCheckArguments {
// laag [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// lokale gegevens
private Iterable<String> names1 = null;
private Iterable<String> names2 = Lists.newArrayList(new String[0]);
private String[] names3 = null;
private String[] names4 = new String[0];
private Iterable<Long> ids1 = null;
private Iterable<Long> ids2 = Lists.newArrayList(new Long[0]);
private Long[] ids3 = null;
private Long[] ids4 = new Long[0];
private Iterable<Categorie> categories1 = null;
private Iterable<Categorie> categories2 = Lists.newArrayList(new Categorie[0]);
private Categorie[] categories3 = null;
private Categorie[] categories4 = new Categorie[0];
private Iterable<Produit> produits1 = null;
private Iterable<Produit> produits2 = Lists.newArrayList(new Produit[0]);
private Produit[] produits3 = null;
private Produit[] produits4 = new Produit[0];
...
}
- regel 19: de test JUnit wordt uitgevoerd in integratie met het Spring-framework;
- regel 18: vóór de tests worden de beans die zijn gedefinieerd in de klasse [AppConfig] van het project geïnstantieerd;
- regels 23-26: injectie van een instantie van elk van de twee interfaces van de laag [DAO];
- regels 29-44: onjuiste aanroepparameters voor de methoden van de laag [DAO];
- regel 29: een null-pointer van het type [Iterable<String>] als namenlijst;
- regel 30: een lege lijst van het type [Iterable<String>] als namenlijst;
- regel 29: een pointer null van het type String[] als naamarray;
- regel 30: een lege array van het type String[] als namenarray;
- ...
Met het veld [names1] voeren we bijvoorbeeld de volgende test uit:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- regel 1: we geven aan dat de test [getShortProduitsByName1] de uitzondering van het type [MyIllegalArgumentException] moet opleveren
Met het veld [names2] voeren we bijvoorbeeld de volgende test uit:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName2() {
daoCategorie.getLongEntitiesByName(names2);
}
Met het veld [names3] voer je bijvoorbeeld de volgende test uit:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName3() {
daoCategorie.getLongEntitiesByName(names3);
}
Met het veld [names4] voeren we bijvoorbeeld de volgende test uit:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName4() {
daoProduit.getShortEntitiesByName(names4);
}
Zo voeren we 48 tests uit om alle mogelijke gevallen te testen. We voeren de uitvoeringsconfiguratie met de naam [spring-jdbc-generic-04-JUnitTestCheckArguments] [1] uit. Het verkregen resultaat is als volgt: [2]:
![]() |
4.11.2. De test [JUnitTestDao]
De test [JUnitTestDao] roept de methoden van de laag [DAO] aan met geldige argumenten en controleert of de methoden doen wat er van hen verwacht wordt. Er zijn in totaal 74 tests die de bewerkingen voor het invoegen, selecteren, bijwerken en verwijderen van entiteiten, categorieën of producten controleren. In totaal zijn er meer dan 1000 regels code. We gaan slechts enkele van deze methoden bekijken.
4.11.2.1. Het raamwerk van de test
De klasse [JUnitTestDao] heeft de volgende opbouw:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
// Spring-context
@Autowired
private ApplicationContext context;
// laag [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// constanten
private final int NB_PRODUITS = 5;
private final int NB_CATEGORIES = 2;
// lokaal
// lokaal
private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
@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 geleegd en vervolgens ook de tabel [PRODUITS]
daoCategorie.deleteAllEntities();
// de woordenboeken worden leeggemaakt
for (Long id : mapCategories.keySet()) {
mapCategories.remove(id);
}
for (Long id : mapProduits.keySet()) {
mapProduits.remove(id);
}
}
...
}
- regels 27-28: net als bij de test [JUnitTestCheckArguments] hebben we hier te maken met een test die is geïntegreerd met Spring en geconfigureerd door de klasse [AppConfig] van het project;
- regels 32-33: injectie van de Spring-context die toegang geeft tot alle beans;
- regels 35-36: injectie van de instantie van de interface [IDao<Produit>] die door de klasse wordt getest;
- regels 37-38: injectie van de instantie van de interface [IDao<Categorie>] die door de klasse wordt getest;
- regels 41-42: wanneer een test gegevens uit de database nodig heeft, wordt er een database met [NB_CATEGORIES]-categorieën aangemaakt, met elk [NB_PRODUITS]-producten. Zo krijgen we [NB_CATEGORIES]-categorieën in de tabel [CATEGORIES] en [NB_CATEGORIES] * [NB_PRODUITS]-producten in de tabel [PRODUITS];
- regels 46-47: twee woordenboeken waarin de producten en categorieën worden opgeslagen;
- regels 49-62: de methode [clean] wordt vóór elke test uitgevoerd (regel 49). Op regel 54 wordt de tabel [CATEGORIES] geleegd. Hierbij moet worden opgemerkt dat de tabel [PRODUITS] een primaire sleutel [CATEGORIE_ID] heeft op de kolom ID van de tabel [CATEGORIES] en dat deze als volgt is gedefinieerd;
![]() |
- (vervolg)
- 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];
Dus wanneer de inhoud van de tabel [CATEGORIES] wordt verwijderd, wordt ook de inhoud van de tabel [PRODUITS] verwijderd.
- regels 56-58: het categorieënwoordenboek wordt leeggemaakt;
- regels 59-61: hetzelfde gebeurt met het productwoordenboek;
Let op: vóór elke test hebben we lege tabellen in de database en lege woordenboeken in het geheugen.
4.11.2.2. De methode [verifyClean]
De methode [verifyClean] controleert of de tabellen na de methode [clean] leeg zijn:
@Test
public void verifyClean() {
log("verifyClean", 1);
List<Categorie> categories = daoCategorie.getAllShortEntities();
Assert.assertEquals(0, categories.size());
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.2.3. De methode [fillDataBase]
Deze methode controleert of de database correct is gevuld met testgegevens:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// de database en woordenboeken vullen
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// weergave
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// enkele controles
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// de woordenboeken moeten leeg zijn
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
Deze test maakt gebruik van verschillende privémethoden:
- [fill], regel 4, die de database vult met testgegevens;
- [registerCategories], regel 4, die de woordenboeken vult met de gegevens die worden geretourneerd door de methode [fill]. Deze twee woordenboeken vertegenwoordigen de opgeslagen entiteiten;
- [showDataBase], regel 6, die de twee tabellen [CATEGORIES] en [PRODUITS] leest en de gelezen gegevens retourneert;
- [checkShortCategorie], regel 13, controleert de categorie die door [showDataBase] is gelezen. Het controleert of de korte versie van deze categorie overeenkomt met wat is opgeslagen in het categoriewaardeset;
- [checkShortProduit] regel 16 doet hetzelfde voor de producten;
- wanneer een entiteit in een woordenboek is gevonden, wordt deze uit het woordenboek verwijderd. De regels 19-20 controleren of beide woordenboeken leeg zijn. Als deze twee beweringen kloppen, betekent dit dat:
- alle waarden die door [showDataBase] zijn ingelezen, daadwerkelijk in de woordenboeken zijn gevonden;
- dat deze geen andere entiteiten bevatten dan degene die zijn ingelezen;
De privémethode [fill] is als volgt:
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);
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);
}
// toevoeging van de categorie – de producten worden vervolgens ook
// toegevoegd
categories = daoCategorie.saveEntities(categories);
// resultaat
return categories;
}
- regels 3-12: er wordt een lijst van [nbCategories] categorieën opgebouwd met voor elke categorie [nbProduits] producten;
- regel 15: deze lijst met categorieën wordt opgeslagen. We hebben gezien dat de methode [daoCategorie.saveEntities] ook de producten van de categorieën opslaat wanneer deze producten bevatten;
- regel 17: de opgeslagen lijst met categorieën wordt geretourneerd. De opgeslagen entiteiten (categorieën en producten) hebben nu een primaire sleutel in hun veld [id];
De privémethode [registerCategories] voegt deze entiteiten toe aan beide woordenboeken:
private void registerCategories(List<Categorie> categories) {
// woordenboeken
for (Categorie categorie : categories) {
mapCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
mapProduits.put(produit.getId(), produit);
}
}
}
Elk woordenboek heeft als toegangssleutel de primaire sleutel van de entiteiten.
Zodra dit is gebeurd, wordt de eerder gevulde database gelezen en weergegeven door de volgende privémethode [showDataBase]:
private Object[] showDataBase() throws BeansException, JsonProcessingException {
// lijst met categorieën
log("Liste des catégories", 2);
List<Categorie> categories = daoCategorie.getAllShortEntities();
affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
// lijst met producten
log("Liste des produits", 2);
List<Produit> produits = daoProduit.getAllShortEntities();
affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
// resultaat
return new Object[] { categories, produits };
}
- regels 4 en 8: de korte versies van de categorieën en producten worden opgehaald;
- regel 11: er wordt een array geretourneerd die de twee opgehaalde entiteitenlijsten bevat;
- regels 5 en 9: de lijsten met entiteiten worden weergegeven met de volgende privémethode [affiche]:
// weergave van een lijst met elementen van het type T
private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, mapper);
}
}
// weergave van een element van het type T
private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
System.out.println(mapper.writeValueAsString(element));
}
De entiteiten worden weergegeven door een mapper jSON (regel 10). Deze mapper is de tweede parameter van de methode [affiche], regel 2. De Spring-context definieert vier mappers jSON in het bestand [ConfigJdbc] van de Maven-afhankelijkheid [mysql-config-jdbc]:
// filters jSON -------------------------------------
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
- deze mappers jSON (regels 7-9, 16-18, 26-28, 35-37) hebben een attribuut
[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)]
waardoor ze bij elk verzoek aan de Spring-context als bean worden geïnstantieerd. Dit is nieuw. Alle Spring-beans die we tot nu toe hebben gezien, waren singletons: er werd slechts één exemplaar van aangemaakt en dat exemplaar werd telkens geretourneerd wanneer er een verwijzing naar de Spring-context werd aangevraagd. Waarom deze verandering? In feite configureren de vier beans [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] de enige mapper jSON (dit is wel degelijk een singleton) die in de regels 2-5 is gedefinieerd. Deze moet bij elke aanroep van een van de vier voorgaande beans opnieuw worden geconfigureerd en niet slechts één keer bij het initialiseren van de context. Als we hadden besloten om vier verschillende mappers jSON te gebruiken, één voor elk van de vier beans, dan hadden deze singletons kunnen zijn. Dat was heel goed mogelijk geweest. We zouden dan in de regels 10, 19, 29 en 38 het volgende hebben geschreven:
ObjectMapper jsonMapper = new ObjectMapper();
- De vier JSON-mappers dienen om de filters jSON van de entiteiten [Produit] en [Categorie] te configureren. We hebben namelijk (zie paragrafen 4.6 en 4.6) het volgende geschreven:
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
en
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
De weergave jSON van de entiteit [Categorie] wordt gecontroleerd door het filter jSON [jsonFilterCategorie] en die van deentiteit [produit] door het filter jSON [jsonFilterProduit]. De vier mappers jSON van de Spring-context configureren deze twee filters als volgt:
- de mapper [jsonMapperShortCategorie] configureert het filter jSON [jsonFilterCategorie] voor een verkorte versie van de categorie: het veld [produits] wordt niet opgenomen in de weergave jSON van de categorie;
- de mapper [jsonMapperLongCategorie] configureert het filter jSON [jsonFilterCategorie] voor een lange versie van de categorie: het veld [produits] wordt opgenomen in de weergave jSON van de categorie;
- de mapper [jsonMapperShortProduit] configureert het filter jSON [jsonFilterProduit] voor een korte versie van het product: het veld [categorie] wordt niet opgenomen in de weergave jSON van het product;
- de mapper [jsonMapperLongProduit] configureert het filter jSON [jsonFilterProduit] voor een lange versie van het product: het veld [categorie] wordt opgenomen in de weergave jSON van het product;
We zijn klaar met de privémethode [showDataBase]. Laten we teruggaan naar de testcode [fillDataBase]:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// basis en woordenboeken vullen
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// weergave
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// enkele controles
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// de woordenboeken moeten leeg zijn
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
- regels 6-8: we halen de verkorte versies op van de producten en categorieën die uit de database zijn gelezen;
- regels 10-11: eerste controles;
- regels 12-14: elke categorie die door de methode [showDataBase] wordt opgehaald, wordt gecontroleerd door de volgende privé-methode [checkShortCategorie]:
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
// het veld [produits] kan niet op een draagbare manier worden getest met de implementaties van jPA
}
- regel 1: [Categorie actual] is de categorie die uit de database is opgehaald en die identiek moet zijn aan de categorie in het woordenboek [mapCategories];
- regel 2: de primaire sleutel van de uitgelezen categorie wordt opgehaald;
- regel 3: de categorie die met deze primaire sleutel in het categorieënwoordenboek is opgeslagen, wordt opgehaald;
- regel 4: de sleutel wordt uit het woordenboek verwijderd om er zeker van te zijn dat geen andere gelezen categorie dezelfde sleutel gebruikt;
- regel 5: er wordt gecontroleerd of beide categorieën dezelfde naam hebben;
De verkorte versie van de producten die via de methode [showDataBase] zijn opgehaald, wordt gecontroleerd door de volgende eigen methode [checkShortProduit]:
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(actual.getIdCategorie(), expected.getIdCategorie());
// het veld [categorie] kan niet op een draagbare manier worden getest met de implementaties van jPA
}
- regel 1: [Produit actual] is het verkorte product dat uit de database is gelezen;
- regels 2-3: uit het woordenboek met opgeslagen producten wordt het product met dezelfde primaire sleutel opgehaald;
- regel 4: de gevonden vermelding wordt uit het woordenboek verwijderd;
- regels 5-8: er wordt gecontroleerd of beide producten dezelfde veldwaarden hebben;
4.11.2.4. De methode [getLongCategoriesByName3]
Deze test is als volgt:
@Test
public void getLongCategoriesByName3() {
// basisinvulling
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// test
log("getLongCategoriesByName3", 1);
List<Categorie> categories2 = daoCategorie.getLongEntitiesByName("categorie[0]", "categorie[1]");
Assert.assertEquals(2, categories2.size());
registerCategories(Lists.newArrayList(categories.get(0), categories.get(1)));
for (Categorie categorie : categories) {
checkLongCategorie(categorie);
}
Assert.assertEquals(0, mapCategories.size());
}
- regel 4: de database wordt gevuld en de lijst met opgeslagen categorieën en producten wordt opgehaald;
- regel 7: we testen de methode [daoCategorie.getLongEntitiesByName(Iterable<String> names)] van de laag [DAO]. We vragen een lijst op van twee producten, aangeduid met hun volledige namen;
- regel 8: we controleren of de door [daoCategorie.getLongEntitiesByName(Iterable<String> names)] geretourneerde lijst inderdaad twee elementen bevat;
- regel 9: de twee elementen die op regel 4 zijn opgeslagen, worden in het categoriedictionary geplaatst;
- regels 10-12: er wordt gecontroleerd of de twee gelezen elementen inderdaad dezelfde zijn als die welke zijn opgeslagen;
- regel 13: er wordt gecontroleerd of het categorieënwoordenboek leeg is, wat betekent dat alle ingelezen categorieën in het woordenboek zijn gevonden en dat het geen waarden bevat die niet zijn ingelezen;
Regel 11: de methode [checkLongCategorie] controleert de lange versie van een categorie:
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
- op regel 6 wordt gecontroleerd of het veld [produits] van de categorie niet null is. Het ophalen van een categorie in het lange formaat levert deze namelijk altijd op met een veld [produits] dat niet null is. Als de categorie geen producten bevat, dan is het veld [produits] een lege maar bestaande lijst;
4.11.2.5. De methode [updateDataBase1]
@Test
public void updateDataBase1() {
// invullen
fill(NB_CATEGORIES, NB_PRODUITS);
// test
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = daoCategorie.getLongEntitiesByName("categorie[1]").get(0);
List<Produit> produits = categorie1.getProduits();
Map<Produit, Long> versions = new HashMap<Produit, Long>();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
versions.put(produit, produit.getVersion());
}
daoProduit.saveEntities(produits);
// proeflezen
List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
.getProduits();
Assert.assertEquals(produits.size(), produitsInBd.size());
// controles
for (Produit produit2 : produitsInBd) {
Produit produit = findProduitByName(produit2.getNom(), produits);
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
Assert.assertEquals(produit2.getVersion().longValue(), versions.get(produit) + 1);
}
}
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
De methode [updateDataBase1] verhoogt de prijs van de producten in de categorie met de naam categorie[1] met 10% en controleert twee dingen:
- of de basisprijs daadwerkelijk is gewijzigd;
- dat de versie van het bijgewerkte product met 1 is verhoogd;
De code doet het volgende:
- regel 4: de database aanvullen;
- regel 7: de categorie met de naam 'categorie[1]' wordt uit de database opgehaald;
- regels 8-13: de prijs van al deze producten wordt met 10% verhoogd (regel 11). Daarnaast wordt er een woordenboek aangemaakt waarin een product aan zijn versie wordt gekoppeld (regels 9 en 12);
- regel 14: de methode [daoProduit.saveEntities] wordt aangeroepen. Deze methode zorgt voor de update van de producten;
- regel 16: de producten uit de categorie met de naam 'categorie[1]' worden uit de database opgehaald;
- regels 20-24: voor alle producten in deze categorie wordt gecontroleerd of de prijs daadwerkelijk is gewijzigd (regel 22) en of de versie met 1 is verhoogd (regel 23);
4.11.2.6. De methode [deleteProduitsByProduit1]
De methode [deleteProduitsByProduit1] verwijdert producten uit de tabel [PRODUITS]:
@Test
public void deleteProduitsByProduit1() {
// invullen
fill(NB_CATEGORIES, NB_PRODUITS);
// verwijdering
daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
// controle
List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
Assert.assertEquals(0, produits.size());
}
- regel 6: er worden twee producten verwijderd;
- regels 8-9: er wordt gecontroleerd of ze niet meer in de database staan;
4.11.2.7. De methode [getLongProduitsById3]
@Test
public void getLongProduitsById3() {
// invullen
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// test
log("getLongProduitsById3", 1);
List<Produit> produits = daoProduit.getLongEntitiesByName("produit[0,3]", "produit[1,4]");
Assert.assertEquals(2, produits.size());
registerProduits(Lists.newArrayList(categories.get(0).getProduits().get(3), categories.get(1).getProduits().get(4)));
produits = daoProduit.getLongEntitiesById(produits.get(0).getId(), produits.get(1).getId());
for (Produit produit : produits) {
checkLongProduit(produit);
}
Assert.assertEquals(0, mapProduits.size());
}
- regel 4: de database wordt gevuld en de lijst met opgeslagen categorieën wordt opgehaald;
- regel 7: de volledige versie van twee producten, geïdentificeerd aan de hand van hun naam, wordt uit de database opgehaald;
- regel 9: de producten [produit[0,3], produit[1,4]] die in de lijst met categorieën van regel 4 voorkomen, worden in het productwoordenboek geplaatst;
- regel 10: deze twee producten worden in de database opgezocht aan de hand van hun primaire sleutels;
- regels 11-14: er wordt gecontroleerd of de gelezen gegevens identiek zijn aan de gegevens die in het productregister zijn opgeslagen;
De privémethode [checkLongProduit] is als volgt:
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertNotNull(actual.getCategorie());
}
4.11.2.8. Conclusion
We laten het hierbij. Er zijn momenteel 74 tests en er zouden er nog meer kunnen worden toegevoegd, aangezien ik waarschijnlijk enkele te testen scenario’s ben vergeten. Hoewel deze tests niet uitputtend zijn, hebben ze toch talrijke fouten aan het licht gebracht, meestal randgevallen die bij het oorspronkelijk schrijven van de laag [DAO] niet waren voorzien. Een fase van uitgebreide tests is onmisbaar voor elk project.
Om de test uit te voeren, kan de geïmporteerde uitvoeringsconfiguratie met de naam [spring-jdbc-generic-04.JUnitTestDao] worden gebruikt.
![]() | ![]() |
4.11.3. De test [JUnitTestPushTheLimits]
De test [JUnitTestPushTheLimits] is een prestatietest. We maken gebruik van het feit dat de tests JUnit hun uitvoeringstijd weergeven om de prestaties van de laag [DAO] te meten. Deze worden vervolgens vergeleken met die van de JPA-implementaties van de [DAO]-laag.
4.11.3.1. Squelette
Het skelet van de klasse [JUnitTestPushTheLimits] is als volgt:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestPushTheLimits {
// laag [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// constanten
private final int NB_CATEGORIES = 2500;
private final int NB_PRODUITS = 2;
// lokaal
private Map<Long, Categorie> hCategories;
private Map<Long, Produit> hProduits;
@Before
public void clean() {
// de tabel leegmaken [CATEGORIES]
daoCategorie.deleteAllEntities();
// woordenboeken
hCategories = new HashMap<Long, Categorie>();
hProduits = new HashMap<Long, Produit>();
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// de tabellen vullen
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, 0L, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, 0L, String.format("produit[%d,%d]", i, j), 0L,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// categorie toevoegen – de producten worden vervolgens ook automatisch ingevoegd
categories = daoCategorie.saveEntities(categories);
// woordenboeken
for (Categorie categorie : categories) {
hCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
hProduits.put(produit.getId(), produit);
}
}
// resultaat
return categories;
}
....
// -------------------- privé-methoden
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
Assert.assertNotNull(actual.getCategorie());
}
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
boolean erreur = false;
try {
actual.getCategorie().getNom();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
boolean erreur = false;
try {
actual.getProduits().size();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
}
Hier zien we het skelet van de klasse [JUnitTestDao]. We zijn al met al deze methoden bekend. De test werkt met een database van 2500 categorieën met elk 2 producten (regels 32-33). De tabel [CATEGORIES] zal dus 2500 rijen bevatten en de tabel [PRODUITS] 5000 rijen. We hadden meer rijen kunnen toevoegen, maar de test duurt nu al bijna een minuut. We hebben daarom gekozen voor waarden die draaglijk zijn voor de gebruiker die op het einde van de test wacht.
Er zijn in totaal 18 tests. Deze worden uitgevoerd met de uitvoerconfiguratie [1]. De uitvoertijden worden weergegeven in [2]:
![]() |
4.11.3.2. doNothing [0,114]
De methode [doNothing] doet niets. Deze methode wordt gebruikt om de duur te meten van de methode [clean], die vóór elke test wordt uitgevoerd en de database leegmaakt. Hierboven is te zien dat de duur van deze bewerking in vergelijking met de andere bewerkingen gering is.
@Test
public void doNothing() {
// opschonen
}
4.11.3.3. perf01 [4,179]
De test [perf01] dient om de tijd te meten die nodig is om de database te vullen:
@Test
public void perf01() {
// invoegen
fill(NB_CATEGORIES, NB_PRODUITS);
}
4.11.3.4. perf02 [7,624]
De methode [perf02]:
- vult de database;
- wijzigt vervolgens de naam van alle categorieën en de prijs van alle producten.
@Test
public void perf02() {
// update
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
for (Categorie categorie : categories) {
categorie.setNom(categorie.getNom() + "*");
for (Produit produit : categorie.getProduits()) {
produit.setPrix(produit.getPrix() * 1.1);
}
}
// update
daoCategorie.saveEntities(categories);
}
4.11.3.5. perf03[3,911]
De methode [perf03]:
- vult de database
- en verwijdert vervolgens alle categorieën één voor één. De producten worden ook verwijderd vanwege de cascade die bestaat tussen de tabel [CATEGORIES] en de tabel [PRODUITS].
Het is hier opmerkelijk dat deze bewerking minder lang duurt dan de bewerking [perf01] [4,179 s], die minder bewerkingen uitvoert.
@Test
public void perf03() {
// categorieën verwijderen en de bijbehorende producten
daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}
Als we de code van de methode [daoCategorie.deleteEntitiesByEntity] bekijken, zien we dat er een [PreparedStatement] met 2500 parameters (het aantal categorieën) zal worden uitgevoerd. Hier komt de bean [maxPreparedStatementParameters] in beeld, die de opdracht SQL opsplitst in meerdere [PreparedStatement]-opdrachten met een aantal parameters dat door de specifieke gebruikte SGBD kan worden verwerkt.
4.11.3.6. perf04[2,426]
De methode [perf04]:
- vult de database;
- vraagt vervolgens de lange versie van alle categorieën op;
@Test
public void perf04() {
// selecteren
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
ids.add(categorie.getId());
}
daoCategorie.getLongEntitiesById(ids);
}
4.11.3.7. perf05 [3,507]
De methode [perf05]:
- vult de database;
- verwijdert vervolgens de 5000 producten op basis van hun primaire sleutels (we hebben dus mogelijk een [PreparedStatement] met 5000 parameters);
- controleert of de producttabel daarna leeg is;
@Test
public void perf05() {
// producten verwijderen
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
for (Produit p : categorie.getProduits()) {
ids.add(p.getId());
}
}
daoProduit.deleteEntitiesById(ids);
// controle
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.3.8. Résultats
We gaan niet alle afzonderlijke tests verder toelichten. We geven alleen aan wat ze doen en hoe lang ze duren. Deze tijden zijn alleen interessant als ze onderling worden vergeleken. De waarden zijn namelijk afhankelijk van de gebruikte testomgeving (hardware en softwareconfiguratie). Maar als ze in dezelfde omgeving zijn gemeten, kunnen ze wel worden vergeleken.
Totale duur van de test: 59,995 seconden
rol | ||
vult de database met 2500 categorieën en 5000 producten | ||
vult de database en wijzigt deze vervolgens | ||
vult de database en verwijdert vervolgens alle categorieën en hun producten | ||
vult de database en vraagt de lange versie van alle categorieën op | ||
vult de database en verwijdert de 5000 producten één voor één op basis van hun primaire sleutels | ||
vult de database en verwijdert de 5000 producten één voor één op basis van hun naam | ||
vult de database en verwijdert de 5000 producten één voor één op basis van hun artikelnummers | ||
vult de database en vraagt de korte versie van alle producten op via hun namen | ||
vult de database aan en vraagt de lange versie van alle producten op basis van hun naam | ||
vult de database aan en vraagt de korte versie van alle producten op via hun primaire sleutels | ||
vult de database aan en vraagt de lange versie van alle producten op via hun primaire sleutels | ||
vult de database en verwijdert vervolgens alle categorieën (en dus de bijbehorende producten) één voor één op basis van hun naam | ||
vult de database en verwijdert vervolgens alle categorieën (en dus de bijbehorende producten) één voor één op basis van hun referentienummers | ||
vult de database en vraagt de korte versie van alle categorieën op via hun namen | ||
vult de database aan en vraagt de volledige versie van alle categorieën op basis van hun namen | ||
vult de database aan en vraagt de korte versie van alle categorieën op via hun primaire sleutels | ||
vult de database aan en vraagt de lange versie van alle categorieën op via hun primaire sleutels |
Deze resultaten zijn soms verrassend:
- het was sneller om de lange versie van de producten (perf09) op te halen dan de korte versie (perf08), terwijl de lange versie een join tussen twee tabellen vereist;
- de duur van de eerste vulling (perf01) is aanzienlijk langer dan die van alle andere vullingen die daarna volgen;
- het opvragen van de korte versie van de producten via hun namen (perf08) duurt langer dan het opvragen via de primaire sleutels (perf10). Dat lijkt vrij logisch. Maar voor de lange versies is het omgekeerde het geval (perf09, perf11);
We zullen daarom niet verder ingaan op deze resultaten. Ze zullen ons echter van pas komen om deze oplossing [Spring JDBC] te vergelijken met de oplossingen:
- [Spring JDBC] van de vijf andere SGBD;
- [Spring JPA] die hierna volgen;





























