Skip to content

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;

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:

Image

  • [sql] is de uit te voeren opdracht SQL;
  • [rowMapper] is een instantie van de volgende interface [RowMapper<T>]:

Image

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:

1
2
3
4
5
6
7
c1, produits11
c1, produit12
...
c1,produits1n
c2, produits21
c2, produits22
...

waarbij elke categorie [ci] een veld [produits] heeft dat een productlijst is met slechts één element [produitsij]. Wij hebben echter de volgende lijst nodig:

c1, produits1
c2, produits2

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:

Image

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:

Image

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:

Image

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]:

Image

  • 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

test
rol
durée (s)
perf01
vult de database met 2500 categorieën en 5000 producten
4,179
perf02
vult de database en wijzigt deze vervolgens
7,624
perf03
vult de database en verwijdert vervolgens alle categorieën en hun producten
3,911
perf04
vult de database en vraagt de lange versie van alle categorieën op
2,426
perf05
vult de database en verwijdert de 5000 producten één voor één op basis van hun primaire sleutels
3,507
perf06
vult de database en verwijdert de 5000 producten één voor één op basis van hun naam
3,947
perf07
vult de database en verwijdert de 5000 producten één voor één op basis van hun artikelnummers
3,633
perf08
vult de database en vraagt de korte versie van alle producten op via hun namen
4,054
perf09
vult de database aan en vraagt de lange versie van alle producten op basis van hun naam
2,643
perf10
vult de database aan en vraagt de korte versie van alle producten op via hun primaire sleutels
3,463
perf11
vult de database aan en vraagt de lange versie van alle producten op via hun primaire sleutels
2,777
perf12
vult de database en verwijdert vervolgens alle categorieën (en dus de bijbehorende producten) één voor één op basis van hun naam
3,806
perf13
vult de database en verwijdert vervolgens alle categorieën (en dus de bijbehorende producten) één voor één op basis van hun referentienummers
2,828
perf14
vult de database en vraagt de korte versie van alle categorieën op via hun namen
2,731
perf15
vult de database aan en vraagt de volledige versie van alle categorieën op basis van hun namen
2,603
perf16
vult de database aan en vraagt de korte versie van alle categorieën op via hun primaire sleutels
2,462
perf17
vult de database aan en vraagt de lange versie van alle categorieën op via hun primaire sleutels
3,287

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;