Skip to content

4. Einführung in Spring JDBC

In diesem Kapitel werden wir die folgende Architektur untersuchen:

Dies ist dieselbe Architektur wie zuvor. Wir werden zwei Änderungen vornehmen:

  • Die Datenbank wird zwei Tabellen enthalten, die durch eine Fremdschlüsselbeziehung verknüpft sind;
  • die [DAO]-Schicht wird mithilfe der [Spring JDBC]-Bibliothek implementiert, was die Verwaltung der JDBC-API vereinfacht;

4.1. Einrichten der Entwicklungsumgebung

Importieren Sie mit STS das Projekt [spring-jdbc-04] aus dem Ordner [<examples>/spring-database-generic/spring-jdbc]

Außerdem müssen wir mit dem [MyManager]-Client eine neue MySQL-Datenbank anlegen (siehe Abschnitt 3.1):

  • In [3] verwenden die folgenden Beispiele eine MySQL-Datenbank namens [dbproduitscategories];
  • In [9] geben Sie das Passwort des Root-Benutzers ein (in diesem Dokument lautet dieses Passwort „root“);
  • In [18] wurde die Datenbank [dbproduitscategories] leer angelegt. Wir erstellen Tabellen und füllen sie mit einem SQL-Skript [19-20];
  • Wechseln Sie in [21] in den Ordner [<examples>/spring-database-config/mysql/databases];
  • Stellen Sie in [25] sicher, dass Sie sich in der Datenbank [dbproduitscategories] und nicht in der Datenbank [dbproduits] befinden;
  • In [29] hat das SQL-Skript fünf Tabellen erstellt. Die Tabellen [ROLES, USERS, USERS_ROLES] werden erst verwendet, wenn wir uns mit der Sicherheit des Webdienstes befassen, der erstellt wurde, um die Datenbank [dbproduitscategories] im Web verfügbar zu machen;

4.2. Die Datenbank [dbproduitscategories]

Die Datenbank [dbproduitscategories] ist eine Erweiterung der zuvor besprochenen Datenbank [dbproduits]. Während in der Tabelle [PRODUITS] das Produkt eine Kategorie hatte, die durch eine Zahl identifiziert wurde, die keine besondere Bedeutung hatte, ist diese Zahl hier ein Fremdschlüssel in der Tabelle [CATEGORIES].

Die Tabelle [PRODUCTS] sieht wie folgt aus:

  • [ID]: der automatisch inkrementierte Primärschlüssel der Tabelle [PRODUCTS];
  • [NAME]: der eindeutige Name des Produkts [4];
  • [PRICE]: der Preis des Produkts;
  • [DESCRIPTION]: die Produktbeschreibung;
  • [VERSIONING] ist die Versionsnummer des Produkts. Die Anfangsversion ist 1 [3]. Bei jeder Änderung des Produkts wird die Versionsnummer durch den Code, der die Tabelle verwaltet, erhöht;
  • [CATEGORY_ID]: der Fremdschlüssel in der Tabelle [CATEGORIES] zur Identifizierung der Kategorie, zu der das Produkt gehört;
  • in [1-3] der Fremdschlüssel [CATEGORIE_ID] der Tabelle [PRODUITS]. Er verweist auf die Spalte [ID] der Tabelle [CATEGORIES] [4-5];
  • Wenn eine Kategorie gelöscht wird, werden auch alle damit verknüpften Produkte gelöscht [6]. Dieser Punkt ist wichtig zu beachten, da er bei der Erstellung der [DAO]-Schicht verwendet wird, die die Datenbank [dbproduitscategories] nutzt;

Die Tabelle [CATEGORIES] sieht wie folgt aus:

  • [ID]: automatisch inkrementierter Primärschlüssel;
  • [VERSIONING]: Versionsnummer der Kategorie;
  • [NAME]: Eindeutiger Name der Kategorie;

4.3. Das Eclipse-Projekt

  

Das [spring-jdbc-04]-Projekt implementiert die folgende Architektur:

Das [spring-jdbc-04]-Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei konfiguriert wird:

  

<?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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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>
  • Zeilen 28–32: Das Projekt stützt sich auf das Projekt [mysql-config-jdbc], das die JDBC-Schicht konfiguriert;
  • Zeilen 34–37: Das Artefakt [spring-boot-starter-jdbc] stellt die Spring-JDBC-Bibliotheken bereit;

Letztendlich lauten die Abhängigkeiten wie folgt:

  

4.4. Spring-Konfiguration

  

Die [AppConfig]-Klasse, die das Spring-Projekt konfiguriert, sieht wie folgt aus:


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 {
 
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
 
    // JdbcTemplate
    @Bean
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
        return new NamedParameterJdbcTemplate(dataSource);
    }
 
    // product insertion
    @Bean
    public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
                ConfigJdbc.TAB_PRODUITS_ID);
    }
 
    // insertion category
    @Bean
    public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
                ConfigJdbc.TAB_CATEGORIES_ID);
    }
 
}
  • Zeile 16: Die Klasse ist eine Spring-Konfigurationsklasse;
  • Zeile 17: Das Paket [spring.jdbc.dao] wird nach Spring-Komponenten durchsucht, die nicht in der Klasse [AppConfig] vorhanden sind. Hier finden wir die Komponente, die die [DAO]-Schicht implementiert;
  • Zeile 18: Wir werden Transaktionen nicht selbst verwalten, sondern dies Spring JDBC überlassen. Das Einzige, was zu tun ist, besteht darin, die Methoden, die innerhalb einer Transaktion ausgeführt werden müssen, mit der Spring-Annotation [@Transactional] zu versehen. Zeile 18 stellt sicher, dass diese Annotation verarbeitet und nicht ignoriert wird. Die Transaktionsverwaltung wird von einer der Spring-JDBC-Projektabhängigkeiten übernommen, die über die Datei [pom.xml] importiert werden;
  • Zeile 19: Wir importieren die bereits in der Klasse [generic.jdbc.config.ConfigJdbc] definierten Beans aus dem Projekt [mysql-config-jdbc];
  • Zeilen 23–36: die im Beispiel [spring-jdbc-02] vorgestellte Datenquelle [tomcat-jdbc];
  • Zeilen 40–42: der Transaktionsmanager, der mit der zuvor definierten Datenquelle verknüpft ist. Der Bean muss den Namen [transactionManager] tragen, da dies der Name ist, der von der Annotation [@EnableTransactionManagement] verwendet wird. Der [DataSourceTransactionManager] wird von der Spring-JDBC-Bibliothek bereitgestellt (Zeile 12);
  • Zeilen 45–48: die Bean [namedParameterJdbcTemplate], auf der die Implementierung der [DAO]-Schicht basieren wird. Diese Bean wird von der Spring-JDBC-Bibliothek bereitgestellt (Zeile 10). Diese Bean ist zudem mit der zuvor definierten Datenquelle verknüpft (Zeile 47);
  • Zeilen 51–55: Die Bean [simpleJdbcInsertProduit] (beliebiger Name) wird verwendet, um ein Produkt in die Tabelle [PRODUITS] einzufügen und den generierten Primärschlüssel abzurufen. Die verschiedenen verwendeten Parameter lauten wie folgt:
    • [dataSource]: die Datenquelle [tomcat-jdbc] aus den Zeilen 24–36;
    • [ConfigJdbc.TAB_PRODUITS]: die Tabelle [PRODUITS];
    • [ConfigJdbc.TAB_CATEGORIES_ID]: die Primärschlüsselspalte der Tabelle [PRODUCTS]. Beachten Sie, dass bei PostgreSQL der Name dieser Spalte in Kleinbuchstaben geschrieben sein muss;
  • Zeilen 58–62: Die Bean [simpleJdbcInsertCategorie] wird verwendet, um eine Kategorie in die Tabelle [CATEGORIES] einzufügen und den generierten Primärschlüssel abzurufen;

4.5. Projekt-Ausnahmen

  

Wir haben die Klassen [UncheckedException, DaoException, ShortException] bereits im Projekt [spring-jdbc-03] kennengelernt. Wir fügen eine neue hinzu:


package spring.jdbc.infrastructure;
 
public class MyIllegalArgumentException extends UncheckedException {
 
    private static final long serialVersionUID = 1L;
 
    // manufacturers
    public MyIllegalArgumentException() {
        super();
    }
 
    public MyIllegalArgumentException(int code, Throwable e, String className) {
        super(code, e, className);
    }

}
  • Die Klasse [MyIllegalArgumentException] leitet sich von der Klasse [UncheckedException] ab und ist daher eine ungeprüfte Klasse. Sie wird verwendet, um einen Aufruf mit falschen Argumenten an eine Methode in der [DAO]-Schicht zu signalisieren. Wir haben sie nicht [IllegalArgumentException] genannt, da diese Ausnahme bereits im JDK existiert und dies manchmal dazu führte, dass der Compiler einen falschen [import] generierte;

4.6. Projektentitäten

  

Die Klassen im Paket [spring.jdbc.entities] repräsentieren die Zeilen in den Datenbanktabellen [dbproduitscategories]. Vorerst lassen wir die Tabellen [USERS, ROLES, USERS_ROLE] außer Acht.

Alle Entitäten erben von der übergeordneten Klasse [AbstractCoreEntity]:


package spring.jdbc.entities;
 
public abstract class AbstractCoreEntity {
    // properties
    protected Long id;
    protected Long version;
 
    // manufacturers
    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;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [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 and setters
...
}
  • Zeile 5: Das Feld [id] wird der Spalte [ID] zugeordnet, dem Primärschlüssel der Tabellen;
  • Zeile 6: Das Feld [version] wird der Spalte [VERSIONING] der Tabellen zugeordnet;
  • Zeilen 8–26: verschiedene Konstruktoren und Methoden zum Erstellen oder Initialisieren eines [AbstractCoreEntity]-Objekts;
  • Zeilen 35–47: Die Methode [equals] legt fest, dass zwei [AbstractCoreEntity]-Objekte gleich sind, wenn sie dasselbe [id]-Feld haben. Dabei ist zu beachten, dass [AbstractCoreEntity]-Objekte Darstellungen von Tabellenzeilen sind, bei denen [id] der Primärschlüssel ist und es daher keine zwei Zeilen mit derselben [id] geben kann;
  • Zeilen 30–33: ein Vorschlag für [hashCode];

Die Klasse [Product] stellt eine Zeile in der Tabelle [PRODUCTS] dar:


package spring.jdbc.entities;
 
import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
    // properties
    private String nom;
    private Long idCategorie;
    private double prix;
    private String description;
    private Categorie categorie;
 
    // manufacturers
    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;
    }
 
    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
                description, idCategorie);
    }
 
    // getters and setters
...
}
  • Zeile 6: Die Klasse [Product] erweitert die Klasse [AbstractCoreEntity];
  • Zeilen 8–12: Die Felder [id, version, name, categoryId, price, description] entsprechen den Spalten [ID, VERSIONING, NAME, CATEGORY_ID, PRICE, DESCRIPTION] in der Tabelle [PRODUCTS];
  • Zeile 12: das Objekt vom Typ [Category] mit dem Primärschlüssel [categoryId]. Dieses Feld kann je nach Fall ausgefüllt sein oder auch nicht. Wenn es ausgefüllt ist, beziehen wir uns auf ein Langform-Produkt [LongProduct]; andernfalls auf ein Kurzform-Produkt [ShortProduct];
  • Zeile 5: ein JSON-Filter. Beachten Sie, dass das Projekt [mysql-config-jdbc] eine JSON-Bibliothek enthält. Der Filter ist notwendig, da das Feld [category] ausgefüllt sein kann oder auch nicht. In diesem Fall unterscheidet sich die JSON-Darstellung des Produkts. Um diese beiden Fälle zu behandeln, konfigurieren wir in Zeile 5 den Filter [jsonFilterProduct]. Ein JSON-Filter ermöglicht es uns, dynamisch festzulegen, welche Felder aus der JSON-Darstellung ausgeschlossen werden sollen. Wenn wir wissen, dass das Feld [category] nicht ausgefüllt wurde, schließen wir es aus der JSON-Darstellung des Produkts aus;

Die Klasse [Category] repräsentiert eine Zeile in der Tabelle [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 {
 
    // properties
    private String nom;
    public List<Produit> produits;
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(Long id, Long version, String nom, List<Produit> produits) {
        super(id, version);
        this.nom = nom;
        this.produits = produits;
    }
 
    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
    }
 
    // methods
    public void addProduit(Produit produit) {
        // add a product
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // we add the product
            produits.add(produit);
            // set your category
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }
 
    // getters and setters
...
}
  • Zeile 9: Die Klasse [Category] erweitert die Klasse [AbstractCoreEntity];
  • Zeile 12: Die Felder [id, version, name] entsprechen den Spalten [ID, VERSIONING, NAME] in der Tabelle [CATEGORIES];
  • Zeile 13: Das Feld [products] stellt die Liste der Produkte in der Kategorie dar. Dieses Feld ist nicht immer ausgefüllt. Ist dies nicht der Fall, beziehen wir uns auf eine Kurzform-Kategorie [ShortCategorie]; andernfalls auf eine Langform-Kategorie [LongCategorie];
  • Zeilen 32–44: Mit der Methode [addProduct] können Sie ein Produkt zur Kategorie hinzufügen (Zeile 39) und die Eigenschaften der Kategorie (categoryID und category) im hinzugefügten Produkt festlegen;
  • Zeile 8: Ein JSON-Filter. Wenn die JSON-Bibliothek ein [Category]-Objekt serialisieren/deserialisieren muss, müssen wir ihr mitteilen, wie sie mit dem Filter namens [jsonFilterCategory] umgehen soll;

4.7. Die Schnittstelle Idao<T>

  

Die [IDao]-Schnittstelle der [DAO]-Schicht hat die folgende Signatur:


package spring.jdbc.dao;
 
import java.util.List;
 
import spring.jdbc.entities.AbstractCoreEntity;
 
public interface IDao<T extends AbstractCoreEntity> {
 
    // list of all T entities
    public List<T> getAllShortEntities();
 
    public List<T> getAllLongEntities();
 
    // special entities - short version
    public List<T> getShortEntitiesById(Iterable<Long> ids);
 
    public List<T> getShortEntitiesById(Long... ids);
 
    public List<T> getShortEntitiesByName(Iterable<String> names);
 
    public List<T> getShortEntitiesByName(String... names);
 
    // special entities - long version
    public List<T> getLongEntitiesById(Iterable<Long> ids);
 
    public List<T> getLongEntitiesById(Long... ids);
 
    public List<T> getLongEntitiesByName(Iterable<String> names);
 
    public List<T> getLongEntitiesByName(String... names);
 
    // update of several entities
    public List<T> saveEntities(Iterable<T> entities);
 
    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities();
 
    // deletion of multiple entities
    public void deleteEntitiesById(Iterable<Long> ids);
 
    public void deleteEntitiesById(Long... ids);
 
    public void deleteEntitiesByName(Iterable<String> names);
 
    public void deleteEntitiesByName(String... names);
 
    public void deleteEntitiesByEntity(Iterable<T> entities);
 
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
  • Zeile 7: Hier haben wir eine [IDao]-Schnittstelle, die durch einen Typ T parametrisiert ist, mit der Bedingung: Dieser Typ muss die Klasse [AbstractCoreEntity] erweitern oder die Schnittstelle [AbstractCoreEntity] implementieren. Das Schlüsselwort [extends] wird für beide Fälle verwendet. Hier wird T entweder durch den Typ [Product] oder durch den Typ [Category] instanziiert. Tatsächlich wird schnell deutlich, dass wir die gleichen Arten von Operationen (Einfügen, Ändern, Löschen, Auswählen) an den Typen [Product] und [Category] durchführen. Es ist daher sinnvoll, diese Methoden in einer generischen Schnittstelle zu gruppieren;
  • je nach Kontext beziehen sich die Begriffe [LongEntity] und [ShortEntity] auf unterschiedliche Situationen:
    • wenn T der Typ [Product] ist:
      • ist [ShortEntity] das Produkt, bei dem das Feld [Category] nicht ausgefüllt ist;
      • ist [LongEntity] das Produkt mit ausgefülltem Feld [Category];
    • wenn T der Typ [Category] ist:
      • ist [ShortEntity] die Kategorie, bei der das Feld [List<Product> products] nicht ausgefüllt ist;
      • [LongEntity] ist das Produkt, bei dem das Feld [List<Product> products] ausgefüllt ist;

Wir haben also eine Schnittstelle mit 19 Methoden. Die meisten Methoden sind Duplikate. Nehmen wir das Beispiel der Methode [getShortEntitiesById]:


    public List<T> getShortEntitiesById(Iterable<Long> ids);
 
    public List<T> getShortEntitiesById(Long... ids);
  • Zeilen 1 und 3: Der Parameter ist die Liste der Primärschlüssel der Entitäten, für die wir die Kurzform wünschen. Diese Liste wird in zwei verschiedenen Formen dargestellt:
    • Zeile 1: eine Liste, die die Schnittstelle [Iterable<Long>] implementiert. Der Typ [List<Long>] implementiert diese Schnittstelle, aber es gibt noch viele andere. Hätten wir [List<Long> ids] geschrieben, wäre das für unsere Beispiele ausreichend gewesen, hätte den Benutzer unserer Beispiele jedoch gezwungen, Konvertierungen durchzuführen, wenn sein Parameter nicht genau dem erwarteten Typ entsprochen hätte;
    • Zeile 3: Leider implementiert der Typ `Long[]` die Schnittstelle `Iterable<Long>` nicht. In diesem Fall verwenden wir die Version aus Zeile 3. Der formale Parameter [Long... ids] (3 Punkte) kann Werte entweder aus einem Array oder einer Folge von IDs akzeptieren: getShortEntitiesById(id1, id2, ...);

Dieselbe IDao<T>-Schnittstelle wird durch die folgende Architektur implementiert:

wobei eine [JPA]-Schicht (Java Persistence API) zwischen die [DAO]-Schicht und den JDBC-Treiber des DBMS eingefügt wird. Dies ermöglicht uns eine gemeinsame Testschicht für beide Architekturen. In beiden Fällen stellt die [DAO]-Schicht zwei Schnittstellen bereit:

  • IDao<Product> für den Zugriff auf die [PRODUCTS]-Tabelle;
  • IDao<Category> für den Zugriff auf die Tabelle [CATEGORIES];

4.8. Implementierung der IDao<T>-Schnittstelle

  
  • Die Schnittstelle IDao<Product> wird von der Klasse [DaoProduct] implementiert;
  • Die Schnittstelle IDao<Category> wird von der Klasse [DaoCategory] implementiert;

Die Klassen [DaoProduct] und [DaoCategory] erben beide von der folgenden abstrakten Klasse [ AbstractDao]:


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> {
 
    // injections
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;
 
    // local
    protected String simpleClassName = getClass().getSimpleName();
 
    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // argument validity
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // obtaining by tranches
        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)));
        }
        // result
        return entities;
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Long... ids) {
        // argument validity
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // result
        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();
 
    // méthodes privées ----------------------------------------------
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
    }
 
    @SuppressWarnings("unchecked")
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
    ...
    }
 
    // méthodes protégées ----------------------------------------------
    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);
 
}
  • Zeile 15: Die Klasse [AbstractDao] ist abstrakt (Schlüsselwort `abstract`). Als solche kann sie nicht instanziiert werden. Sie kann nur als Basisklasse dienen. Diese Klasse hat mehrere Aufgaben:
    • die Art der Transaktion zu definieren, in der jede Methode ausgeführt wird;
    • um möglichst viele allgemeine Aufgaben für beide Implementierungen der Schnittstellen [IDao<Product>] und [IDao<Category>] zu übernehmen. Dies betrifft in erster Linie die Validierung der Argumente. Null-Argumente und leere Listen werden nicht akzeptiert;
    • Vereinheitlichen Sie die Typen der Parameter `T... params` und `Iterable<T> params` zu einem einzigen Typ: `List<T> params`;
    • Delegieren Sie die Arbeit an die Unterklassen, sobald sie für eine der beiden Schnittstellen spezifisch wird;

Dank der Standardisierung der Parameter der verschiedenen Methoden durch die Klasse [AbstractDao] müssen die Unterklassen [DaoProduit] und [DaoCategorie] nur noch 10 statt 19 Methoden implementieren:


    // methods implemented by child classes ----------------------------------------------
    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();

Sehen wir uns einige Methoden der Klasse [AbstractDao] an.

Methode [getShortEntitiesById]

Diese Methode ruft die Kurzversion von Entitäten ab, für die Primärschlüssel angegeben sind.


    // injections
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;
 
    // local
    protected String simpleClassName = getClass().getSimpleName();
 
    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
    ...
}
  • Zeilen 2–4: Wir injizieren die im Konfigurationsdatei [ConfigJdbc] definierte Bean [maxPreparedStatementParameters], die die JDBC-Schicht für ein bestimmtes DBMS konfiguriert:

    // max number of parameters of a [PreparedStatement]
    public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
 
    @Bean(name = "maxPreparedStatementParameters")
    public int maxPreparedStatementParameters() {
        return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
  • Zeilen 1–7: Definieren Sie die Bean [maxPreparedStatementParameters], die die maximale Anzahl von Parametern festlegt, die an ein [PreparedStatement] übergeben werden können. Diese Anforderung ergab sich nicht beim MySQL-DBMS, das 10.000 Parameter für ein [PreparedStatement] akzeptierte. Bei Tests mit dem SQL Server-DBMS wurde eine Ausnahme ausgelöst, die darauf hinwies, dass die maximale Anzahl von Parametern für ein [PreparedStatement] 2.100 betrug. Daher ist diese Zahl zu einem Konfigurationsparameter für die verschiedenen DBMS geworden. Sie muss daher für jedes DBMS im Konfigurationsprojekt [sgbd-config-jdbc] hinterlegt werden;

Kehren wir zum Code für die Methode [getShortEntitiesById] zurück:


    // injections
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;
 
    // local
    protected String simpleClassName = getClass().getSimpleName();
 
    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
    ...
}
  • Zeile 7: Der Klassenname. Wird als Parameter für einen der Konstruktoren der Ausnahmeklasse [DaoException] verwendet;
  • Zeile 10: Die Annotation [@Transactional(readOnly = true)] gibt an, dass die Methode innerhalb einer schreibgeschützten Transaktion ausgeführt werden muss. Man könnte sich fragen, welchen Nutzen eine solche Transaktion hat, da die Methode nur Lesevorgänge durchführt und es daher im Falle eines Fehlers nichts zurückzusetzen gibt. Der Autor der [Spring Data]-Bibliothek empfiehlt dies und erklärt, warum. Ich bin seinem Rat gefolgt;

Der Methodenkörper lautet wie folgt:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // validité de l'argument
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
...
}
  • Zeile 5: Die Gültigkeit des Parameters [ids] wird mit der folgenden Methode überprüft:

    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
        // elements null ?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
        }
        // elements vide ?
        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>();
            }
        }
        // résultat par défaut
        return null;
}
  • Zeile 1: Die Methode [checkNullOrEmptyArgument] ist eine generische Methode, die durch den Typ <T2> parametrisiert ist. T2 ist der Typ der Elemente, die als zweiter Parameter an die Methode übergeben werden. Dies kann [Long, String, AbstractCoreEntity] sein;
  • Zeile 1: Die Methode [checkNullOrEmptyArgument] nimmt zwei Parameter entgegen:
    • [Iterable<T2> elements]: der zu prüfende Parameter;
    • [checkEmpty]: wird auf true gesetzt, wenn überprüft werden soll, ob der vorherige Parameter eine nicht leere Liste ist;
  • Zeilen 4–6: Wir prüfen, ob der Parameter [elements] null ist. Ist dies der Fall, wird eine [MyIllegalArgumentException] ausgelöst;
  • Zeilen 8–15: Wenn die Liste leer ist und wir überprüfen sollten, ob sie nicht leer ist, lösen wir eine [MyIllegalArgumentException] aus;
  • Zeile 13: Wenn die Liste leer ist und wir nicht überprüfen sollten, ob sie nicht leer ist, geben wir eine leere Liste von Elementen vom Typ T zurück. Die Schnittstelle [Iterable<T2>] verfügt über eine Methode [iterator()], die es ermöglicht, die Elemente der Liste, die die Schnittstelle implementiert, zu durchlaufen. Zwei Methoden dieses Iterators sind nützlich:
    • [iterator].hasNext(): gibt true zurück, wenn die Liste noch ein zu verarbeitendes Element enthält, andernfalls false;
    • [iterator].next(): gibt das aktuelle Element der Liste zurück und schreitet um ein Element vor;
  • Schließlich
    • wenn das Argument [T2... elements] null oder leer ist, wird eine [MyIllegalArgumentException] ausgelöst;
    • wenn das Argument [T2... elements] eine leere Liste ist und dies zulässig war, wird eine leere Liste von Elementen vom Typ T zurückgegeben;

Eine ähnliche Methode existiert, wenn das zu prüfende Argument vom Typ [T2... elements] ist:


@SuppressWarnings("unchecked")
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
    ...
    }

Kehren wir zum Code für die Methode [getShortEntitiesById] zurück:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // argument validity
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        // obtaining by tranches
        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)));
        }
        // result
        return entities;
}
  • Zeile 7: Wenn wir diesen Punkt erreichen, bedeutet dies, dass das Argument [Iterable<Long> ids] gültig ist;
  • Zeilen 7–14: Wir werden später sehen, dass die Methode [getShortEntitiesById] durch einen Typ [PreparedStatement] implementiert wird, der als Parameter die Liste der zu suchenden Primärschlüssel verwendet. Zum Beispiel:

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 ist ein Parameter, dessen tatsächlicher Wert vom Typ List<Long> ist. Jedes Element dieser Liste wird als Parameter ? in einem [PreparedStatement] übergeben. Wir haben jedoch festgelegt, dass dieser Typ eine maximale Anzahl von Parametern akzeptiert, die durch das Feld [maxPreparedStatementParameters] der Klasse festgelegt wird;

  • Zeile 7: Die Liste der T-Entitäten, die von der Methode [getShortEntitiesById] zurückgegeben wird. Diese Liste wird in Blöcken von [maxPreparedStatementParameters] Elementen erstellt;
  • Zeile 9: Aus dem Argument [Iterable<Long> ids] erstellen wir einen Typ [List<Long> listIds]. Die Klasse [Lists] ist eine Klasse aus der Google Guava-Bibliothek, die zahlreiche statische Methoden zur Bearbeitung von Objektsammlungen bietet. Die Google Guava-Bibliothek wurde vom Maven-Projekt [mysql-config-jdbc] importiert (pom.xml):

        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
</dependency>
  • Zeile 10: die Anzahl der T-Entitäten, nach denen in der Datenbank gesucht werden soll;
  • Zeilen 11–13: Sie werden in Gruppen von [size = maxPreparedStatementParameters] Elementen gesucht;
  • Zeile 12: eine Berechnung, um zu verhindern, dass das Ende der Liste [listIds] überschritten wird;
  • Zeile 13: Die T-Entitäten werden durch Aufruf von [getShortEntitiesById(listIds.subList(i, limit))] abgerufen. Diese Methode ist in der Klasse wie folgt definiert:

abstract protected List<T> getShortEntitiesById(List<Long> ids);

Es ist daher die Unterklasse, die die T-Entitäten aus der Datenbank abruft:

  • [DaoProduct], wenn T vom Typ [Product] ist;
  • [DaoCategory], wenn T vom Typ [Category] ist;

Dieser Ansatz in der übergeordneten Klasse hat zwei Vorteile:

  • Die Signatur der Methode [getShortEntitiesById] in der Unterklasse ist eindeutig: Ihr Argument ist vom Typ [List<Long> ids];
  • die Unterklasse muss sich nicht mit dem Problem der [maxPreparedStatementParameters]-Parameter eines [PreparedStatement] befassen. Die übergeordnete Klasse hat dies bereits für sie übernommen;
  • Zeile 13: Die von der Unterklasse zurückgegebenen Entitäten werden der Liste der Entitäten hinzugefügt, die von der Oberklasse zurückgegeben werden (Zeile 16);

Betrachten wir nun die Implementierung der anderen Methode der Klasse, [getShortEntitiesById]:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Long... ids) {
        // validité de l'argument
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        // résultat
        return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
  • Zeile 3: Der Typ des Arguments hat sich geändert: Long... ids;
  • Zeile 5: Die Gültigkeit dieses Arguments wird geprüft;
  • Zeile 7: Wir rufen die soeben beschriebene Methode [getShortEntitiesById] auf. Auch hier verwenden wir die Klasse [Lists] aus der Bibliothek [Google Guava]. Beachten Sie, dass wir eine explizite Typumwandlung in den Typ [Iterable<Long>] vornehmen müssen, um dem Compiler bei der Auswahl der richtigen Methode zu helfen, da die Methode [getShortEntitiesById] in der Klasse drei Signaturen hat:
    • List<T> getShortEntitiesById(Long... ids);
    • List<T> getShortEntitiesById(Iterable<Long> ids);
    • List<T> getShortEntitiesById(List<Long> ids), die abstrakt ist und von der Unterklasse implementiert wird;

Wir werden nicht weiter auf die abstrakte Klasse [AbstractDao] eingehen, die die Oberklasse der Klassen [DaoProduit] und [DaoCategorie] ist. Wir möchten lediglich anmerken, dass es manchmal nützlich ist, Verhaltensweisen, die mehreren Klassen gemeinsam sind, in einer Oberklasse zu bündeln, unabhängig davon, ob diese abstrakt ist oder nicht. Nach dieser Arbeit müssen die Unterklassen nur noch die folgenden Methoden implementieren:


    // methods implemented by child classes ----------------------------------------------
    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();

Der Code in Abschnitt 4.8 zeigt die verschiedenen Arten von Transaktionen, die für jede Methode verwendet werden. Beachten Sie folgende Punkte:

  • Methoden, die die Datenbank lesen, sind mit [@Transactional(readOnly = true)] annotiert;
  • Methoden, die die Datenbank ändern, sind mit [@Transactional] annotiert;
  • [delete]-Methoden sind nicht mit einer Annotation versehen und werden daher nicht innerhalb einer Transaktion ausgeführt. Dahinter steht der Gedanke, dass der Benutzer bei einem Fehlschlag des Löschvorgangs wahrscheinlich nicht alle zuvor erfolgreich durchgeführten Vorgänge rückgängig machen möchte;

4.9. Die Klasse [DaoCategorie]

  

Die Klasse [DaoCategorie] implementiert die Schnittstelle [IDao<Categorie>], die Zugriff auf Daten in der Tabelle [CATEGORIES] der MySQL-Datenbank [dbproduitscategories] bietet. Ihr Grundgerüst sieht wie folgt aus:


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> {
 
    // constants
 
    // injections
    @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> {
....
}
  • Zeile 28: Die Klasse [DaoCategorie] ist eine Spring-Komponente und kann als solche in andere Spring-Komponenten injiziert werden;
  • Zeile 29: Die Klasse [DaoCategorie] erweitert die abstrakte Klasse [AbstractDao<Categorie>] und ist somit eine Implementierung der Schnittstelle [IDao<Categorie>];
  • Zeilen 34–37: Injektion von Beans, die in der in Abschnitt 4.4 beschriebenen Klasse [AppConfig] definiert sind;
  • Zeilen 38–39: Injektion einer Referenz auf die Klasse [DaoProduit], die die Schnittstelle [IDao<Produit>] implementiert, welche den Zugriff auf Daten in der Tabelle [PRODUITS] verwaltet;
  • Zeilen 41–89: Implementierung der Schnittstelle [IDao<Category>];
  • Zeilen 95–101: zwei interne Klassen, die die Schnittstelle [RowMapper<T>] implementieren;

Betrachten wir die Methoden nacheinander.

4.9.1. Die Methode [getAllShortEntities]

Die Methode [getAllShortEntities] gibt alle Kategorien aus der Tabelle [CATEGORIES] in ihrer Kurzform zurück:


    @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 basieren auf dem Objekt [namedParameterJdbcTemplate], das in der Spring-Konfigurationsdatei definiert ist und von der Spring-JDBC-Bibliothek bereitgestellt wird. Es verfügt über zahlreiche Methoden. Die oben verwendete lautet wie folgt:

Image

  • [sql] ist die auszuführende SQL-Anweisung;
  • [rowMapper] ist eine Instanz der folgenden [RowMapper<T>]-Schnittstelle:

Image

Das Prinzip ist wie folgt:

  • Die Methode [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) führt die [Select]-SQL-Anweisung aus. Sie behandelt alle Ausnahmen und übernimmt das Öffnen und Schließen der Verbindung zum DBMS. Das Einzige, was sie nicht tun kann, ist, die Elemente des [ResultSet] – die Objekte, die sie erhält – in einen Typ [Category] zu kapseln, da sie die Zuordnung zwischen den Feldern des Typs [Category] und den Spalten des [ResultSet] nicht kennt. Wir werden später sehen, dass diese Zuordnung mithilfe der JPA-Technologie erstellt wird, die die Elemente eines [ResultSet] automatisch in Instanzen des Typs T kapseln wird. Vorerst ist der zweite Parameter der [query]-Methode eine Instanz der [RowMapper<T>]-Schnittstelle, die diese Kapselung durchführen kann;

Kehren wir zum Code zurück:


    @Override
    public List<Categorie> getAllShortEntities() {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
        } catch (Exception e) {
            throw new DaoException(202, e, simpleClassName);
        }
}

Die SQL-Anweisung [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] lautet wie folgt:


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";

Die Abfrage ruft die Spalten [ID, VERSIONING, NOM] aus der Tabelle [CATEGORIES] ab. Wir werden durchgehend die folgende Syntax verwenden:


SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...

Wichtig ist die Benennung der von der SELECT-Anweisung zurückgegebenen Spalten mithilfe des Attributs [as column_name]. Nur so kann die Portabilität zwischen verschiedenen DBMS gewährleistet werden, da jedes DBMS seine eigene proprietäre Art der Benennung von Spalten hat, die von einer SELECT-Anweisung zurückgegeben werden, in der Spalten aus verschiedenen Tabellen denselben Namen haben (z. B. ID, NAME oder VERSIONING in unserem Fall). Wir beseitigen diese Mehrdeutigkeit, indem wir die Namen angeben, die diese Spalten haben sollen.

Die interne Klasse [ShortCategorieMapper] sieht wie folgt aus:


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);
    }
}
  • Zeile 1: Die Klasse [ShortCategorieMapper] implementiert die Schnittstelle [RowMapper<Categorie>] und muss daher die Methode [mapRow] in den Zeilen 4–5 implementieren, deren Aufgabe es ist, eine Zeile aus dem durch die [SELECT]-Anweisung erzeugten [ResultSet rs] in einen Typ [Categorie] zu kapseln;
  • Zeile 5: Diese Kapselung wird durchgeführt. Beachten Sie, dass der von den [rs.getType(name)]-Methoden verwendete Name der Name ist, der in den [as name]-Attributen der SELECT-Spalten verwendet wird;

Wir haben somit die Liste der Kategorien in ihrer Kurzform erhalten, ohne Ausnahmen behandeln oder die Verbindung verwalten zu müssen. Dies ist der Vorteil der Spring-JDBC-Bibliothek, die alles übernimmt, was bei der Verwaltung von Tabellenelementen abstrahiert werden kann, und es dem Entwickler überlässt, sich um das zu kümmern, was nicht abstrahiert werden kann.

4.9.2. Die Methode [getAllLongEntities]

Die Methode [getAllLongEntities] gibt alle Kategorien aus der Tabelle [CATEGORIES] in ihrer Langform zurück:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
                    new LongCategorieMapper()));
        } catch (Exception e) {
            throw new DaoException(223, e, simpleClassName);
        }
}

Die SQL-Anweisung [ConfigJdbc.SELECT_ALLLONGCATEGORIES] lautet wie folgt:


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";    

Das Ziel ist es, die Kategorien zusammen mit den zugehörigen Produkten abzurufen. Dies wird erreicht, indem die Tabelle [CATEGORIES] mit der Tabelle [PRODUCTS] verknüpft wird, wobei der Fremdschlüssel [CATEGORY_ID] aus der Tabelle [PRODUCTS] zur Tabelle [CATEGORIES] verwendet wird. Die Syntax [FROM PRODUCTS p RIGHT JOIN CATEGORIES c ON p.CATEGORY_ID=c.ID] ruft auch Kategorien ab, denen keine Produkte zugeordnet sind. In diesem Fall gibt die SELECT-Abfrage eine Kategorie und ein Produkt zurück, bei denen alle Spalten auf NULL gesetzt sind.

Die Klasse [LongCategorieMapper] sieht wie folgt aus:


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");
        // cas de la catégorie sans produits
        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;
    }
}
  • Zeile 4: Die Methode [mapRow] muss ein [Category]-Objekt zurückgeben, dessen Feld [products] ausgefüllt ist, basierend auf einer Zeile aus dem [ResultSet], das von der vorherigen SELECT-Anweisung zurückgegeben wurde;

Letztendlich lautet die Operation:


[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]

eine Liste des Typs:

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

wobei jede Kategorie [ci] ein Feld [products] enthält, das eine Liste von Produkten ist, die jeweils ein einzelnes Element [productsij] enthält. Nun benötigen wir die folgende Liste:

c1, produits1
c2, produits2

wobei jede Kategorie [ci] ein Feld [products] enthält, das die Liste der Produkte [producti1, producti2, ...] darstellt. Dies wird erreicht, indem die Liste der Kategorien an eine private Methode [filterCategories] übergeben wird:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
                    new LongCategorieMapper()));
        } catch (Exception e) {
            throw new DaoException(223, e, simpleClassName);
        }
}

Die Methode [filterCategories] lautet wie folgt:


    private List<Categorie> filterCategories(List<Categorie> categories) {
        if (categories.size() == 0) {
            return categories;
        }
        // catégories à rendre
        List<Categorie> cats = new ArrayList<Categorie>();
        // on parcourt la liste des catégories obtenues
        for (Categorie categorie : categories) {
            boolean trouve = false;
            for (Categorie cat : cats) {
                if (categorie.equals(cat)) {
                    cat.addProduit(categorie.getProduits().get(0));
                    trouve = true;
                    break;
                }
            }
            // trouvé ?
            if (!trouve) {
                cats.add(categorie);
            }
        }
        // résultat
        return cats;
}
  • Zeile 1: [List<Category> categories] ist die Liste der zu filternden (oder zu gruppierenden) Kategorien;
  • Zeile 6: die Liste der Kategorien, die an den Aufrufer zurückgegeben werden sollen;
  • Zeilen 8–21: Jede Kategorie in der zu filternden Liste wird verarbeitet;
  • Zeilen 10–16: Es wird geprüft, ob die aktuelle Kategorie [category] bereits in der zu erstellenden Liste der Kategorien [cats] vorhanden ist (beachte, dass zwei Kategorien als gleich angesehen werden, wenn sie denselben Primärschlüssel haben, siehe Abschnitt 4.6);
  • Zeilen 11–14: Ist dies bereits der Fall, wird das in [categorie] enthaltene Produkt zur Liste der Produkte in [cat] hinzugefügt;
  • Zeilen 18–20: Ist die aktuelle Kategorie [categorie] noch nicht in der zu erstellenden Liste der Kategorien [cats] vorhanden, wird sie zusammen mit ihrer Produktliste, die ein einzelnes Element enthält, hinzugefügt;

Betrachten wir den Fall, in dem die SQL-SELECT-Anweisung Kategorien ohne zugehörige Produkte zurückgibt. Welche Entität gibt die Klasse [LongCategorieMapper] zurück?


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");
        // cas de la catégorie sans produits
        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;
    }
}

Wenn die SQL-SELECT-Anweisung eine Kategorie ohne Produkte zurückgibt, enthalten alle mit der Kategorie zurückgegebenen Produktspalten den SQL-NULL-Wert. Dieser Fall wird in den Zeilen 7–9 behandelt:

  • Zeile 7: Abrufen des Primärschlüssels des Produkts als Long-Integer;
  • Zeile 9: Wir prüfen, ob der gelesene Wert SQL NULL war (rs.wasNull). Wenn nicht, fügen wir das Produkt in Zeile 6 zur Liste hinzu; andernfalls wird nichts hinzugefügt und die Produktliste bleibt leer.

Beachten Sie, dass wir in allen Fällen eine Kategorie mit einem [products]-Feld zurückgeben, das nicht null ist.

4.9.3. Die Methode [getShortEntitiesById]

Die Methode [getShortEntitiesById] ähnelt der Methode [getAllShortEntities], gibt jedoch nur die Entitäten zurück, deren Primärschlüssel in einer Liste angegeben sind:


    @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);
        }
}
  • Zeile 4: Die Signatur der verwendeten [query]-Methode lautet wie folgt:

Image

Der erste Parameter ist eine parametrisierte SQL-[Select]-Anweisung. Der zweite ist ein Wörterbuch, das jeden Parameter einem Wert zuordnet. Der dritte ist die Instanz der Klasse, die eine Zeile aus dem [ResultSet], das aus der [Select]-Anweisung resultiert, in ein Objekt vom Typ T kapseln;

  • Zeile 4: Die parametrisierte SQL-[Select]-Anweisung lautet wie folgt:

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)";

Diese Abfrage ruft aus der Tabelle [CATEGORIES] die Kategorien ab, deren Primärschlüssel in der Liste :ids enthalten sind.

  • Zeile 5: Der zweite Parameter der [query]-Methode ist hier ein Wörterbuch, das den Schlüssel 'ids' (erster Parameter) mit der Liste [ids] verknüpft, die in Zeile 1 als Parameter an die [getShortEntitiesById]-Methode übergeben wurde. Die Klasse [Collections] gehört zur Bibliothek [Google Guava], die wir bereits besprochen haben. [Collections.singleMap] gibt ein Wörterbuch mit einem einzigen Element zurück;
  • Zeile 5: Die Klasse, die dafür zuständig ist, eine Zeile aus dem durch [Select] erzeugten [ResultSet] in ein Objekt vom Typ [Category] zu kapseln, ist die Klasse [ShortCategoryMapper], die wir bereits betrachtet haben;

An dieser Stelle kommt in der Regel die Bean [maxPreparedStatementParameters] ins Spiel. Tatsächlich kann der Parameter [:ids] der SQL-Anweisung, der eine Liste von Primärschlüsseln darstellt, zwischen 1 und mehreren tausend Parametern enthalten. Diese Anzahl unterliegt einer Begrenzung, die vom jeweiligen DBMS abhängt. Bei MySQL konnten wir 10.000 Parameter fehlerfrei übergeben und haben darüber hinaus nicht getestet. Für SQL Server liegt die offizielle Grenze bei 2.100. Bei Firebird waren bereits 1.000 zu viel. Wir haben die Anzahl auf 100 reduziert. Generell haben wir die maximale Grenze dieser Anzahl für die verschiedenen DBMS nicht getestet.

4.9.4. Die Methode [getLongEntitiesById]

Die Methode [getLongEntitiesById] entspricht der Methode [getShortEntitiesById], gibt jedoch die Langversionen der Kategorien zurück:


    @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);
        }
}

Zeile 4, die SQL-Abfrage [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] lautet wie folgt:


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. Die Methode [getShortEntitiesByName]

Die Methode [getShortEntitiesByName] ähnelt der Methode [getShortEntitiesById], mit dem Unterschied, dass Kategorien anhand ihrer Namen statt anhand ihrer Primärschlüssel abgerufen werden:


    @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);
        }
}

Zeile 4, die SQL-Anweisung [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] lautet wie folgt:


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. Die Methode [getLongEntitiesByName]

Die Methode [getLongEntitiesByName] ähnelt der Methode [getShortEntitiesByName], mit dem Unterschied, dass die Kategorien in ihrer Langform abgerufen werden:


    @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);
        }
}

Zeile 4, die SQL-Anweisung [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] lautet wie folgt:


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. Die Methode [deleteAllEntities]

Die Methode [deleteAllEntities] löscht alle Kategorien aus der Tabelle [CATEGORIES]:


    @Override
    public void deleteAllEntities() {
        try {
            // on supprime toutes les catégories et par cascade tous les produits
            namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
        } catch (Exception e) {
            throw new DaoException(208, e, simpleClassName);
        }
}
  • Zeile 4: Die verwendete Methode [namedParameterJdbcTemplate.update] hat folgende Signatur:

Image

Der erste Parameter ist eine parametrisierte SQL-Update-Anweisung (INSERT, UPDATE, DELETE). Der zweite Parameter ist das Wörterbuch, das Werte den verschiedenen Parametern der SQL-Anweisung zuordnet. Die Methode gibt die Anzahl der durch die SQL-Anweisung aktualisierten Zeilen zurück.

  • Zeile 4: Die SQL-Anweisung [ConfigJdbc.DELETE_ALLCATEGORIES] lautet wie folgt:

public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";

Es handelt sich also nicht um eine parametrisierte Abfrage. Deshalb hat der zweite Parameter der [update]-Methode den Wert null.

4.9.8. Die Methode [deleteAllEntitiesById]

Die Methode [deleteAllEntitiesById] löscht die Kategorien aus der Tabelle [CATEGORIES], für die die Primärschlüssel übergeben werden:


    @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);
        }
}

Zeile 4, die SQL-Anweisung [ConfigJdbc.DELETE_CATEGORIESBYID] lautet wie folgt:


public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";

4.9.9. Die Methode [deleteAllEntitiesByName]

Die Methode [deleteAllEntitiesByName] löscht die Kategorien aus der Tabelle [CATEGORIES], deren Namen übergeben werden:


    @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);
        }
}

Zeile 4, die SQL-Anweisung [ConfigJdbc.DELETE_CATEGORIESBYNAME] lautet wie folgt:


public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";

4.9.10. Die Methode [saveEntities]

4.9.10.1. Der Code

Die Signatur dieser Methode lautet wie folgt:


    @Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {

Die Methode nimmt eine Liste von Kategorien als Parameter entgegen. Sie führt folgende Operationen an ihnen durch:

  • Wenn die Kategorie einen Null-Primärschlüssel hat, wird eine SQL-INSERT-Operation durchgeführt; andernfalls wird eine SQL-UPDATE-Operation durchgeführt;
  • diese Operation wird für jedes Produkt in der Kategorie wiederholt;

Die Methode gibt die Liste der gespeicherten oder aktualisierten Kategorien zurück. Die zurückgegebene Liste ist eine exakte Darstellung der in den Tabellen vorhandenen Kategorien und Produkte, abgesehen von den Versionsnummern: Diese werden in den aktualisierten Entitäten nicht tatsächlich geändert, auch wenn sie in der Datenbank erhöht wurden.

Dies ist bei weitem die komplexeste Methode. Ihr Code lautet wie folgt:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // --------------------------------------------- categories
            List<Categorie> insertCategories = new ArrayList<Categorie>();
            List<Categorie> updateCategories = new ArrayList<Categorie>();
            // on scanne les catégories
            for (Categorie categorie : entities) {
                // insert or update ?
                if (categorie.getId() == null) {
                    insertCategories.add(categorie);
                } else {
                    updateCategories.add(categorie);
                }
            }
            // insertions catégories
            if (insertCategories.size() > 0) {
                insertCategories(insertCategories);
            }
            // updates categories
            if (updateCategories.size() > 0) {
                updateCategories(updateCategories);
            }
 
            // --------------------------------------------- produits
            // on met à jour les produits des catégories
            List<Produit> allProduits = new ArrayList<Produit>();
            for (Categorie categorie : entities) {
                List<Produit> produits = categorie.getProduits();
                Long idCategorie = categorie.getId();
                if (produits != null) {
                    // on l'ajoute à la liste de tous les produits
                    allProduits.addAll(produits);
                    // on scanne les produits un à un pour les relier à leur catégorie
                    for (Produit produit : produits) {
                        // on relie le produit à sa catégorie
                        produit.setIdCategorie(idCategorie);
                        produit.setCategorie(categorie);
                    }
                }
            }
            // insert / update des produits
            daoProduit.saveEntities(allProduits);
            // résultat
            return entities;
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(207, e, simpleClassName);
        }
    }
  • Zeilen 5–23: Kategorien einfügen oder aktualisieren;
  • Zeilen 26–43: Einfügen oder Aktualisieren von Produkten;
  • Zeilen 35–39: Dieser Code verknüpft jedes Produkt mit seiner Kategorie. In der vorherigen Phase des Einfügens von Kategorien wurde ihnen ein Primärschlüssel zugewiesen, der in das Feld [idCategorie] des Produkts eingefügt werden muss (Zeile 37). Zudem ermöglichen die Zeilen 37–38 die Korrektur von Situationen, in denen der Aufrufer nicht jedes Produkt korrekt mit seiner Kategorie verknüpft hat. Um sicherzustellen, dass diese Beziehung korrekt ist, muss die Methode [Category].add(Product p) verwendet werden; nichts hindert einen Benutzer jedoch daran, ein Produkt direkt zur Produktliste der Kategorie hinzuzufügen, ohne diese Methode zu verwenden, wobei das Risiko besteht, dass die Felder [idCategory, category] des Produkts p falsch ausgefüllt werden;
  • Zeile 43: Wir delegieren die Aufgabe des Speicherns/Aktualisierens der Produkte an die Instanz der Schnittstelle [IDao<Product>]. Erinnern Sie sich daran, dass diese Instanz in die Klasse [DaoCategory] injiziert wurde:

    @Autowired
    private IDao<Produit> daoProduit;

4.9.10.2. Einfügen von Kategorien

Kategorien werden mithilfe der folgenden privaten Methode [insertCategories] in die Tabelle [CATEGORIES] eingefügt:


private List<Categorie> insertCategories(List<Categorie> categories) {
        Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
        try {
            // catégories à ajouter
            for (Categorie categorie : categories) {
                Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
                // on mémorise la clé primaire
                mapCategories.put(newId.longValue(), categorie);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // tout est OK - on affecte les clés primaires aux catégories persistées
        for(Long id : mapCategories.keySet()){
            Categorie categorie=mapCategories.get(id);
            categorie.setId(id);
        }        
        // résultat
        return categories;
    }
  • Zeile 6: Wir verwenden die Bean [simpleJdbcInsertCategorie], die durch die folgenden Zeilen in die Klasse injiziert wird:

    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertCategorie;

Diese Bean ist in der [AppConfig]-Klasse des Projekts wie folgt definiert:


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);
}
  • Zeile 5: Die Klasse [SimpleJdbcInsert] ist eine Klasse aus der Spring-JDBC-Bibliothek (Zeile 1):
    • Der Konstruktorparameter [SimpleJdbcInsert] ist die Datenquelle, auf der die Operation ausgeführt wird;
    • die Klausel [withTableName] gibt die Tabelle an, in die ein Element eingefügt werden soll, in diesem Fall die Tabelle [CATEGORIES];
    • die Klausel [usingGeneratedKeyColumns] gibt die automatisch generierte Primärschlüsselspalte an, in diesem Fall die Spalte [ID];
    • die Klausel [usingColumns] beschränkt das Einfügen auf bestimmte Spalten. Hier schließen wir die Spalte [ID] aus, die vom DBMS automatisch generiert wird, sowie die Spalte [VERSIONING], deren Standardwert 1 ist;

Kehren wir zum Code für die Methode [insertCategories] zurück:


private List<Categorie> insertCategories(List<Categorie> categories) {
        Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
        try {
            // catégories à ajouter
            for (Categorie categorie : categories) {
                Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
                // on mémorise la clé primaire
                mapCategories.put(newId.longValue(), categorie);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // tout est OK - on affecte les clés primaires aux catégories persistées
        for(Long id : mapCategories.keySet()){
            Categorie categorie=mapCategories.get(id);
            categorie.setId(id);
        }        
        // résultat
        return categories;
}
  • Zeile 6: Die Methode [simpleJdbcInsertCategorie.executeAndReturnKey] wird verwendet:

Image

Die Methode erwartet als Parameter ein Dictionary, das Tabellenspalten den Werten zuordnet, die in diese eingefügt werden sollen. Sie gibt den Primärschlüssel als Typ [Number] zurück. Die Methode [Number.longValue()] wird verwendet, um den Primärschlüssel als Typ [Long] zu erhalten.

Die Methode [getMapForCategorie] ist die folgende private 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;
}

Die Schlüssel des Wörterbuchs sind die Namen der zu füllenden Spalten [NAME], und die Werte des Wörterbuchs sind die Werte, die in diese Spalten eingefügt werden sollen.

  • Zeile 8 [insertCategories]: Der abgerufene Primärschlüssel wird in einem Wörterbuch gespeichert. Wir warten, bis wir sicher sind, dass alle Entitäten eingefügt wurden, bevor wir ihnen ihre Primärschlüssel zuweisen. Denn im Falle einer Ausnahme werden alle Einfügungen rückgängig gemacht, und wir möchten, dass die [categories]-Entitäten aus Zeile 1 ebenfalls unverändert bleiben;
  • Zeilen 14–17: Da wir nun sicher sind, dass alles gut gelaufen ist, weisen wir den Kategorien die generierten Primärschlüssel zu;
  • Zeile 19: Wir geben die Liste der Kategorien mit ihren Primärschlüsseln zurück;

4.9.10.3. Aktualisieren von Kategorien

Kategorien werden mithilfe der folgenden privaten Methode [updateCategories] aktualisiert:


    private void updateCategories(List<Categorie> categories) {
        try {
            for (Categorie categorie : categories) {
                // basic category update
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
                        new BeanPropertySqlParameterSource(categorie));
                // did we succeed?
                Long idCategorie = null;
                if (nbLignes == 0) {
                    // we didn't succeed - we're trying to find out why
                    // search for the basic category
                    idCategorie = categorie.getId();
                    List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
                    if (categoriesInBd.size() == 0) {
                        // category does not exist
                        throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
                                idCategorie));
                    } else {
                        // the version was no good
                        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);
        }
}

Das Aktualisieren einer C1-Kategorie in der Datenbank mit einer C2-Kategorie im Speicher ist nur zulässig, wenn die Kategorien C1 und C2 dieselbe Version haben. Diese Versionsnummer wird verwendet, um gleichzeitige Aktualisierungen der Entität durch zwei verschiedene Benutzer zu verhindern: Zwei Benutzer, U1 und U2, lesen die Entität E mit einer Versionsnummer gleich V1. U1 ändert E und speichert diese Änderung in der Datenbank: Die Versionsnummer ändert sich dann zu V1+1. U2 ändert seinerseits E und speichert diese Änderung in der Datenbank: Es wird eine Ausnahme ausgelöst, da sich die Version (V1) von der in der Datenbank (V1+1) unterscheidet.

  • Zeilen 2–29: Der `try`-Block enthält zwei `catch`-Blöcke:
    • Der erste, in Zeile 25, dient dazu, jede [DaoException]-Ausnahme, die vom Code in Zeile 13 ausgelöst wird, durchzulassen;
    • der zweite in Zeile 27 dient dazu, andere Ausnahmetypen zu behandeln;
  • Zeile 3: Wir durchsuchen alle zu aktualisierenden Kategorien;
  • Zeile 4: Wir aktualisieren die aktuelle Kategorie mit der Methode [namedParameterJdbcTemplate.update]:

Image

  • Analysieren wir die Anweisung:

            int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,                         new BeanPropertySqlParameterSource(categorie));

Die SQL-Anweisung [ConfigJdbc.UPDATE_CATEGORIES] lautet wie folgt:


public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";

Die Anweisung hat drei Parameter (:id, :version, :nom), deren Werte in den gleichnamigen Feldern des geänderten [categorie]-Objekts stehen. Wir nutzen diese Funktion, indem wir [new BeanPropertySqlParameterSource(categorie)] als zweiten Parameter übergeben, was angibt, dass „die Parameterwerte in den gleichnamigen Feldern dieses Java-Beans stehen“;

Das von dieser Operation zurückgegebene Ergebnis ist bei normaler Ausführung die Anzahl der geänderten Zeilen, d. h. 0 oder 1.

Kehren wir nun zu dem Code zurück, den wir gerade untersuchen:


private void updateCategories(List<Categorie> categories) {
        try {
            for (Categorie categorie : categories) {
                // basic category update
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
                        new BeanPropertySqlParameterSource(categorie));
                // did we succeed?
                Long idCategorie = null;
                if (nbLignes == 0) {
                    // we didn't succeed - we're trying to find out why
                    // search for the basic category
                    idCategorie = categorie.getId();
                    List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
                    if (categoriesInBd.size() == 0) {
                        // category does not exist
                        throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
                                idCategorie));
                    } else {
                        // the version was no good
                        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);
        }
}
  • Zeile 9: Prüfen, ob die Aktualisierung erfolgreich war;
  • Zeile 10: Die Aktualisierung ist fehlgeschlagen. Da die [WHERE]-Klausel die Spalten [ID] und [VERSIONING] betrifft, suchen wir nach der Spalte, die den Fehler in der [WHERE]-Klausel verursacht hat;
  • Zeilen 12–18: Wir überprüfen, ob der [id]-Schlüssel der Kategorie in der Datenbank vorhanden ist. Ist dies nicht der Fall, lösen wir eine [RuntimeException] mit einer entsprechenden Fehlermeldung aus;
  • Zeilen 19–22: Behandeln Sie den Fall, in dem die Version falsch war;

4.10. Die Klasse [DaoProduit]

  

Die Klasse [DaoProduit] implementiert die Schnittstelle [IDao<Produit>], die Zugriff auf Daten in der Tabelle [PRODUITS] der MySQL-Datenbank [dbproduitscategories] bietet. Ihr Grundgerüst sieht wie folgt aus:


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> {
 
    // injections
    @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> {
...
}

Der Code ist dem der Klasse [DaoCategory] sehr ähnlich. Wir werden nur einige Methoden näher betrachten.

4.10.1. Die Methode [getShortEntitiesById]

Die Methode [getShortEntitiesById] gibt die Kurzversion der Produkte zurück, deren Primärschlüssel übergeben wurden:


    @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);
        }
}
  • Zeile 4: Die SQL-Select-Anweisung [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] lautet wie folgt:

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)";
  • Zeile 4: Die Klasse [ShortProductMapper], die für die Kapselung des [ResultSet] in eine Liste von Produkten zuständig ist, sieht wie folgt aus:

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. Die Methode [getLongEntitiesByName]

Die Methode [getShortEntitiesById] gibt die Langform der Produkte zurück, deren Namen übergeben wurden:


    @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);
        }
}
  • Zeile 4: Die SQL-SELECT-Anweisung [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] lautet wie folgt:

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";
  • Zeile 4: Die Klasse [LongProductMapper], die für die Kapselung der Elemente des [ResultSet] in Produkte (Langversion) zuständig ist, lautet wie folgt:

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. Die Methode [saveEntities]

Die Methode [saveEntities] wird sowohl zum Einfügen neuer Produkte (id==null) als auch zum Aktualisieren bestehender Produkte (id!=null) verwendet:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        try {
            // produits à insérer
            List<Produit> insertProduits = new ArrayList<Produit>();
            // produits à mettre à jour
            List<Produit> updateproduits = new ArrayList<Produit>();
            // on scanne la liste des entités reçues
            for (Produit produit : entities) {
                Long id = produit.getId();
                if (id == null) {
                    insertProduits.add(produit);
                } else {
                    updateproduits.add(produit);
                }
            }
            // ajouts
            insertProduits(insertProduits);
            // modifications
            updateProduits(updateproduits);
            // résultat
            return entities;
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(103, e, simpleClassName);
        }
}

Zeile 18: Die einzufügenden Produkte werden mithilfe der folgenden privaten Methode [insertProducts] hinzugefügt:


private List<Produit> insertProduits(List<Produit> produits) {
        Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
        try {
            // produits à ajouter
            for (Produit produit : produits) {
                Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
                // on note la clé primaire
                mapProduits.put(newId.longValue(), produit);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // tout est OK - on affecte les clés primaires aux produits persistés
        for (Long id : mapProduits.keySet()) {
            Produit produit = mapProduits.get(id);
            produit.setId(id);
        }
        // résultat
        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;
    }

Diese Methode entspricht der in Abschnitt 4.9.10.3 beschriebenen Methode [insertCategories].

  • Zeile 4: Wir verwenden die Bean [simpleJdbcInsertProduit], die in die Klasse injiziert wurde:

    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertProduit;

Diese Bean wurde in der Klasse [AppConfig] definiert, die das Projekt konfiguriert:


    @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);
}
  • Zeilen 3–6: Die Bean [simpleJdbcInsertProduct]
    • ist mit der Datenbank [dbproduitscategories] (Zeile 3) und mit der Tabelle [ConfigJdbc.TAB_PRODUITS] in dieser Datenbank (Zeile 4) verknüpft;
    • Der Primärschlüssel für diese Tabelle wird in der Spalte [ConfigJdbc.TAB_PRODUITS_ID] generiert (Zeile 5);
    • Werte werden nur den Spalten [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] zugewiesen (Zeile 6);

Die Methode [updateProducts], die die Produkte aktualisiert (Zeile 20 von [saveEntities]), lautet wie folgt:


private void updateProduits(List<Produit> updateProduits) {
        try {
            // we scan products
            for (Produit produit : updateProduits) {
                // basic product update
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
                        new BeanPropertySqlParameterSource(produit));
                // did we succeed?
                Long idProduit = null;
                if (nbLignes == 0) {
                    // we didn't succeed - we're trying to find out why
                    // we search for the basic product
                    idProduit = produit.getId();
                    List<Produit> produitsInBd = getShortEntitiesById(idProduit);
                    if (produitsInBd.size() == 0) {
                        // the product does not exist
                        throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
                                idProduit));
                    } else {
                        // the version was no good
                        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);
        }
    }

Es ähnelt dem Code, der Kategorien aktualisiert (siehe Abschnitt 4.9.10.3). In Zeile 23 lautet die SQL-Anweisung [ConfigJdbc.UPDATE_PRODUITS], die zur Aktualisierung der Produkte ausgeführt wird, wie folgt:


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";

Die Parameternamen [:id,:version,:nom,:prix,:idCategorie,:description] sind gleichzeitig die Feldnamen in der Klasse [Product], wodurch die Anweisung in den Zeilen 6–7 zur Aktualisierung des aktuellen Produkts verwendet werden kann.

4.11. Die Testschicht

  

Die Testschicht besteht aus drei Testklassen:

  • [JUnitTestCheckArguments]: Die Tests in dieser Klasse rufen die verschiedenen Methoden der [DAO]-Schicht mit ungültigen Argumenten auf und überprüfen, ob sie korrekt reagieren;
  • [JUnitTestDao]: Die Tests in dieser Klasse rufen die verschiedenen Methoden der [DAO]-Schicht auf und überprüfen, ob sie das tun, was erwartet wird;
  • [JUnitTestPushTheLimits] dient nicht dazu, die [DAO]-Schicht zu testen, sondern ihre Leistung zu messen;

Diese Testschicht spielt in diesem Dokument eine wichtige Rolle. Sie ist tatsächlich allen Implementierungen der [IDao<T>]-Schnittstelle gemeinsam. Es gibt sechs pro DBMS (1 JDBC-Implementierung, 3 JPA-Implementierungen, 1 Spring MVC-Implementierung, 1 sichere Spring MVC-Implementierung), also insgesamt 36 für die sechs getesteten DBMS. Die Testschicht ermöglicht es uns, zu überprüfen, ob sich alle Implementierungen gleich verhalten.

4.11.1. Der [JUnitTestCheckArguments]-Test

Die Testklasse [JUnitTestCheckArguments] verfügt über 48 Methoden, die testen, wie die Methoden der [DAO]-Schicht reagieren, wenn sie mit falschen Argumenten aufgerufen werden. Ihr Grundgerüst sieht wie folgt aus:


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 {
 
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    // local data
    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];
 
    ...
 
}
  • Zeile 19: Der JUnit-Test wird in Integration mit dem Spring-Framework durchgeführt;
  • Zeile 18: Vor den Tests werden die in der [AppConfig]-Klasse des Projekts definierten Beans instanziiert;
  • Zeilen 23–26: Injektion einer Instanz jeder der beiden Schnittstellen in der [DAO]-Schicht;
  • Zeilen 29–44: Falsche Aufrufparameter für die Methoden der [DAO]-Schicht;
  • Zeile 29: Ein Null-Zeiger vom Typ [Iterable<String>] als Liste der Namen;
  • Zeile 30: eine leere Liste vom Typ [Iterable<String>] als Liste der Namen;
  • Zeile 29: ein Null-Zeiger vom Typ String[] als Array von Namen;
  • Zeile 30: ein leeres Array vom Typ String[] als Liste von Namen;
  • ...

Mit dem Feld [names1] führen wir beispielsweise den folgenden Test durch:


    @Test(expected = MyIllegalArgumentException.class)
    public void getShortProduitsByName1() {
        daoProduit.getShortEntitiesByName(names1);
}
  • Zeile 1: Wir legen fest, dass der Test [getShortProduitsByName1] eine [MyIllegalArgumentException] auslösen muss

Mit dem Feld [names2] führen wir beispielsweise den folgenden Test durch:


    @Test(expected = MyIllegalArgumentException.class)
    public void getLongCategoriesByName2() {
        daoCategorie.getLongEntitiesByName(names2);
}

Mit dem Feld [names3] führen wir beispielsweise den folgenden Test durch:


    @Test(expected = MyIllegalArgumentException.class)
    public void getLongCategoriesByName3() {
        daoCategorie.getLongEntitiesByName(names3);
}

Mit dem Feld [names4] führen wir beispielsweise den folgenden Test durch:


    @Test(expected = MyIllegalArgumentException.class)
    public void getShortProduitsByName4() {
        daoProduit.getShortEntitiesByName(names4);
}

Wir führen somit 48 Tests durch, um alle möglichen Fälle abzudecken. Wir führen die Testkonfiguration mit dem Namen [spring-jdbc-generic-04-JUnitTestCheckArguments] [1] aus. Das Ergebnis lautet wie folgt [2]:

4.11.2. Der [JUnitTestDao]-Test

Der [JUnitTestDao]-Test ruft die Methoden der [DAO]-Schicht mit gültigen Argumenten auf und überprüft, ob die Methoden das tun, was von ihnen erwartet wird. Es gibt insgesamt 74 Tests, die die Operationen zum Einfügen, Auswählen, Aktualisieren und Löschen von Entitäten, Kategorien oder Produkten überprüfen. Insgesamt umfasst der Code über 1.000 Zeilen. Wir werden nur einige dieser Methoden näher betrachten.

4.11.2.1. Das Testgerüst

Die Klasse [JUnitTestDao] hat das folgende Gerüst:


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;
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    // constants
    private final int NB_PRODUITS = 5;
    private final int NB_CATEGORIES = 2;
 
    // local
    // local
    private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
    private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // we empty table [CATEGORIES] and cascade table [PRODUITS]
        daoCategorie.deleteAllEntities();
        // emptying dictionaries
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }
...
}
  • Zeilen 27–28: Wie beim [JUnitTestCheckArguments]-Test handelt es sich hierbei um einen Test, der in Spring integriert und über die [AppConfig]-Klasse des Projekts konfiguriert wird;
  • Zeilen 32–33: Injektion des Spring-Kontexts, der Zugriff auf alle seine Beans bietet;
  • Zeilen 35–36: Injektion der Instanz der Schnittstelle [IDao<Product>], die von der Klasse getestet wird;
  • Zeilen 37–38: Injektion der Instanz der Schnittstelle [IDao<Category>], die von der Klasse getestet wird;
  • Zeilen 41–42: Wenn ein Test Datenbankdaten benötigt, wird eine Datenbank mit [NB_CATEGORIES] Kategorien generiert, die jeweils [NB_PRODUITS] Produkte enthalten. Wir haben somit [NB_CATEGORIES] Kategorien in der Tabelle [CATEGORIES] und [NB_CATEGORIES] * [NB_PRODUITS] Produkte in der Tabelle [PRODUITS];
  • Zeilen 46–47: zwei Wörterbücher, in denen wir die Produkte und Kategorien speichern;
  • Zeilen 49–62: Die Methode [clean] wird vor jedem Test ausgeführt (Zeile 49). In Zeile 54 wird die Tabelle [CATEGORIES] gelöscht. Es ist wichtig zu beachten, dass die Tabelle [PRODUCTS] einen Primärschlüssel [CATEGORY_ID] auf der Spalte ID der Tabelle [CATEGORIES] hat und dass dieser wie folgt definiert ist;
  • (Fortsetzung)
    • in [1-3] den Fremdschlüssel [CATEGORIE_ID] der Tabelle [PRODUITS]. Er verweist auf die Spalte [ID] der Tabelle [CATEGORIES] [4-5];
    • wenn eine Kategorie gelöscht wird, werden auch alle damit verknüpften Produkte gelöscht [6]. Dieser Punkt ist wichtig zu beachten, da er bei der Erstellung der [DAO]-Schicht verwendet wird, die die Datenbank [dbproduitscategories] nutzt;

Wenn also der Inhalt der Tabelle [CATEGORIES] gelöscht wird, wird auch der Inhalt der Tabelle [PRODUCTS] gelöscht.

  • Zeilen 56–58: Wir löschen das Kategorielexikon;
  • Zeilen 59–61: Wir verfahren ebenso mit dem Produktverzeichnis;

Beachten Sie, dass wir vor jedem Test mit leeren Tabellen in der Datenbank und leeren Verzeichnissen im Speicher beginnen.

4.11.2.2. Die Methode [verifyClean]

Die Methode [verifyClean] überprüft, ob die Tabellen nach der Methode [clean] leer sind:


    @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. Die Methode [fillDataBase]

Diese Methode überprüft, ob die Datenbank ordnungsgemäß mit Testdaten gefüllt wurde:


    @Test
    public void fillDataBase() throws BeansException, JsonProcessingException {
        // remplissage base et dictionnaires
        registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
        // affichage
        Object[] data = showDataBase();
        List<Categorie> categories = (List<Categorie>) data[0];
        List<Produit> produits = (List<Produit>) data[1];
        // quelques vérifications
        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);
        }
        // les dictionnaires doivent avoir été épuisés
        Assert.assertEquals(0, mapCategories.size());
        Assert.assertEquals(0, mapProduits.size());
}

Dieser Test verwendet mehrere private Methoden:

  • [fill] in Zeile 4, die die Datenbank mit Testdaten füllt;
  • [registerCategories] in Zeile 4, die die Dictionaries mit den von der Methode [fill] zurückgegebenen Daten füllt. Diese beiden Dictionaries repräsentieren die persistenten Entitäten;
  • [showDataBase] in Zeile 6, die die beiden Tabellen [CATEGORIES] und [PRODUCTS] liest und die gelesenen Daten zurückgibt;
  • [checkShortCategorie] in Zeile 13 überprüft die von [showDataBase] gelesene Kategorie. Es wird überprüft, ob die Kurzbezeichnung dieser Kategorie mit der im Kategorie-Wörterbuch gespeicherten übereinstimmt;
  • [checkShortProduct] in Zeile 16 macht dasselbe für Produkte;
  • Wenn eine Entität in einem Wörterbuch gefunden wird, wird sie aus dem Wörterbuch entfernt. Die Zeilen 19–20 überprüfen, ob beide Wörterbücher leer sind. Wenn beide dieser Aussagen wahr sind, bedeutet dies, dass:
    • alle von [showDataBase] gelesenen Werte tatsächlich in den Wörterbüchern gefunden wurden;
    • die Wörterbücher keine anderen Entitäten enthalten als die, die gelesen wurden;

Die private Methode [fill] lautet wie folgt:


    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // on remplit les tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < nbCategories; i++) {
            Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
            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);
        }
        // ajout de la catégorie - par cascade les produits vont eux aussi être
        // insérés
        categories = daoCategorie.saveEntities(categories);
        // résultat
        return categories;
}
  • Zeilen 3–12: Wir erstellen eine Liste mit [nbCategories] Kategorien, die jeweils [nbProduits] Produkte enthalten;
  • Zeile 15: Diese Liste von Kategorien wird gespeichert. Wir haben gesehen, dass die Methode [daoCategorie.saveEntities] auch die mit den Kategorien verbundenen Produkte speichert, sofern vorhanden;
  • Zeile 17: Die gespeicherte Liste der Kategorien wird zurückgegeben. Die gespeicherten Entitäten (Kategorien und Produkte) haben nun einen Primärschlüssel in ihrem Feld [id];

Die private Methode [registerCategories] fügt diese Entitäten zu beiden Wörterbüchern hinzu:


    private void registerCategories(List<Categorie> categories) {
        // dictionaries
        for (Categorie categorie : categories) {
            mapCategories.put(categorie.getId(), categorie);
            for (Produit produit : categorie.getProduits()) {
                mapProduits.put(produit.getId(), produit);
            }
        }
}

Jedes Wörterbuch verwendet den Primärschlüssel der Entitäten als Zugriffsschlüssel.

Sobald dies geschehen ist, wird die zuvor gefüllte Datenbank von der folgenden privaten Methode [showDataBase] gelesen und angezeigt:


    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // liste des catégories
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities();
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities();
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // résultat
        return new Object[] { categories, produits };
}
  • Zeilen 4 und 8: Rufen die Kurzversionen der Kategorien und Produkte ab;
  • Zeile 11: gibt ein Array zurück, das die beiden Listen der abgerufenen Entitäten enthält;
  • Zeilen 5 und 9: Die Listen der Entitäten werden mithilfe der folgenden privaten Methode [display] angezeigt:

    // display a list of elements of type T
    private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, mapper);
        }
}
 
    // display of a T-type element
    private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
        System.out.println(mapper.writeValueAsString(element));
}

Entitäten werden mithilfe eines JSON-Mappers angezeigt (Zeile 10). Dieser Mapper ist der zweite Parameter der Methode [display] in Zeile 2. Der Spring-Kontext definiert vier JSON-Mapper in der Datei [ConfigJdbc] der Maven-Abhängigkeit [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;
    }
  • Diese JSON-Mapper (Zeilen 7–9, 16–18, 26–28, 35–37) haben ein Attribut

[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)] 

, wodurch sie zu Beans werden, die bei jeder Anfrage an den Spring-Kontext instanziiert werden. Das ist neu. Alle bisher gesehenen Spring-Beans waren Singletons: Es wurde eine einzige Instanz erstellt, und diese Instanz wurde jedes Mal zurückgegeben, wenn eine Referenz darauf vom Spring-Kontext angefordert wurde. Warum diese Änderung? Tatsächlich konfigurieren die vier Beans [jsonMapperShortCategory, jsonMapperLongCategory, jsonMapperShortProduct, jsonMapperLongProduct] den einzigen JSON-Mapper (der tatsächlich ein Singleton ist), der in den Zeilen 2–5 definiert ist. Dieser muss jedes Mal neu konfiguriert werden, wenn eine der vier vorangehenden Beans aufgerufen wird, und nicht nur einmal während der Kontextinitialisierung. Hätten wir uns für vier verschiedene JSON-Mapper entschieden – einen für jede der vier Beans –, dann hätten diese Singletons sein können. Das wäre durchaus möglich gewesen. Wir hätten dann die Zeilen 10, 19, 29, 38 geschrieben:


ObjectMapper jsonMapper = new ObjectMapper();
  • Die vier JSON-Mapper dienen dazu, die JSON-Filter für die Entitäten [Product] und [Category] zu konfigurieren. Wir haben tatsächlich Folgendes geschrieben (siehe Abschnitte 4.6 und 4.6):

@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
 

und


@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
 

Die JSON-Darstellung der Entität [Category] wird durch den JSON-Filter [jsonFilterCategory] gesteuert, die der Entität [Product] durch den JSON-Filter [jsonFilterProduct]. Die vier JSON-Mapper im Spring-Kontext konfigurieren diese beiden Filter wie folgt:

  • Der Mapper [jsonMapperShortCategory] konfiguriert den JSON-Filter [jsonFilterCategory] für eine Kurzversion der Kategorie: Das Feld [products] wird nicht in die JSON-Darstellung der Kategorie aufgenommen;
  • Der Mapper [jsonMapperLongCategory] konfiguriert den JSON-Filter [jsonFilterCategory] für eine Langversion der Kategorie: Das Feld [products] wird in die JSON-Darstellung der Kategorie aufgenommen;
  • Der Mapper [jsonMapperShortProduct] konfiguriert den JSON-Filter [jsonFilterProduct] für eine Kurzversion des Produkts: Das Feld [category] wird nicht in die JSON-Darstellung des Produkts aufgenommen;
  • Der Mapper [jsonMapperLongProduit] konfiguriert den JSON-Filter [jsonFilterProduit] für eine Langversion des Produkts: Das Feld [categorie] wird in die JSON-Darstellung des Produkts aufgenommen;

Wir sind mit der privaten Methode [showDataBase] fertig. Kehren wir zum Testcode [fillDataBase] zurück:


    @Test
    public void fillDataBase() throws BeansException, JsonProcessingException {
        // remplissage base et dictionnaires
        registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
        // affichage
        Object[] data = showDataBase();
        List<Categorie> categories = (List<Categorie>) data[0];
        List<Produit> produits = (List<Produit>) data[1];
        // quelques vérifications
        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);
        }
        // les dictionnaires doivent avoir été épuisés
        Assert.assertEquals(0, mapCategories.size());
        Assert.assertEquals(0, mapProduits.size());
}
  • Zeilen 6–8: Wir rufen die Kurzversionen der aus der Datenbank gelesenen Produkte und Kategorien ab;
  • Zeilen 10–11: erste Überprüfungen;
  • Zeilen 12–14: Jede von der Methode [showDataBase] zurückgegebene Kategorie wird durch die folgende private Methode [checkShortCategory] überprüft:

    private void checkShortCategorie(Categorie actual) {
        Long id = actual.getId();
        Categorie expected = mapCategories.get(actual.getId());
        mapCategories.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        // the [products] field cannot be tested in a portable way with jPA implementations
}
  • Zeile 1: [Category actual] ist die aus der Datenbank gelesene Kategorie und muss mit der Kategorie im Wörterbuch [mapCategories] identisch sein;
  • Zeile 2: Wir rufen den Primärschlüssel der gelesenen Kategorie ab;
  • Zeile 3: Wir rufen die Kategorie ab, die mit diesem Primärschlüssel im Kategoriewörterbuch gespeichert ist;
  • Zeile 4: Der Schlüssel wird aus dem Wörterbuch entfernt, um sicherzustellen, dass eine andere abgerufene Kategorie nicht denselben Schlüssel verwendet;
  • Zeile 5: Wir überprüfen, ob die beiden Kategorien denselben Namen haben;

Die Kurzversion der von der Methode [showDataBase] zurückgegebenen Produkte wird durch die folgende private Methode [checkShortProduct] überprüft:


    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());
        // the [category] field cannot be tested in a portable way with jPA implementations
}
  • Zeile 1: [Actual Product] ist das aus der Datenbank gelesene Kurzprodukt;
  • Zeilen 2–3: Wir rufen das Produkt mit demselben Primärschlüssel aus dem Verzeichnis der persistierten Produkte ab;
  • Zeile 4: Wir löschen den im Verzeichnis gefundenen Eintrag;
  • Zeilen 5–8: Wir überprüfen, ob die beiden Produkte dieselben Feldwerte haben;

4.11.2.4. Die Methode [getLongCategoriesByName3]

Dieser Test läuft wie folgt ab:


    @Test
    public void getLongCategoriesByName3() {
        // remplissage base
        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());
}
  • Zeile 4: Wir füllen die Datenbank und rufen die Liste der gespeicherten Kategorien und Produkte ab;
  • Zeile 7: Wir testen die Methode [daoCategorie.getLongEntitiesByName(Iterable<String> names)] aus der [DAO]-Schicht. Wir fordern eine Liste mit zwei Produkten an, die durch ihre vollständigen Namen identifiziert werden;
  • Zeile 8: Wir überprüfen, ob die von [daoCategorie.getLongEntitiesByName(Iterable<String> names)] zurückgegebene Liste tatsächlich zwei Elemente enthält;
  • Zeile 9: Die beiden in Zeile 4 persistierten Elemente werden dem Kategorie-Dictionary hinzugefügt;
  • Zeilen 10–12: Wir überprüfen, ob die beiden gelesenen Elemente tatsächlich diejenigen sind, die gespeichert wurden;
  • Zeile 13: Wir überprüfen, ob das Kategoriewörterbuch leer ist, was bedeutet, dass alle gelesenen Kategorien im Wörterbuch gefunden wurden und dass das Wörterbuch keine Werte enthält, die nicht gelesen wurden;

Zeile 11: Die Methode [checkLongCategory] überprüft die Langform einer Kategorie:


    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());
}
  • Zeile 6 überprüft, ob das Feld [products] der Kategorie nicht null ist. Dies liegt daran, dass das Auslesen einer Kategorie im Long-Format diese immer mit einem nicht-null [products]-Feld zurückgibt. Wenn die Kategorie keine Produkte enthält, ist das Feld [products] eine leere, aber vorhandene Liste;

4.11.2.5. Die Methode [updateDataBase1]


@Test
    public void updateDataBase1() {
        // remplissage
        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);
        // relecture
        List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
                .getProduits();
        Assert.assertEquals(produits.size(), produitsInBd.size());
        // vérifications
        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;
    }

Die Methode [updateDataBase1] erhöht die Preise der Produkte in der Kategorie namens categorie[1] um 10 % und überprüft zwei Dinge:

  • dass sich der Grundpreis tatsächlich geändert hat;
  • dass die Version des aktualisierten Produkts um 1 erhöht wurde;

Der Code führt Folgendes aus:

  • Zeile 4: füllt die Datenbank;
  • Zeile 7: ruft die Kategorie mit dem Namen „categorie[1]“ aus der Datenbank ab;
  • Zeilen 8–13: erhöht den Preis aller Produkte um 10 % (Zeile 11). Außerdem wird ein Wörterbuch erstellt, das ein Produkt mit seiner Version verknüpft (Zeilen 9 und 12);
  • Zeile 14: Die Methode [daoProduit.saveEntities] wird aufgerufen. Sie aktualisiert die Produkte;
  • Zeile 16: Die Produkte der Kategorie „category[1]“ werden aus der Datenbank abgerufen;
  • Zeilen 20–24: Für alle Produkte in dieser Kategorie wird überprüft, ob der Preis aktualisiert wurde (Zeile 22) und ob die Version um 1 erhöht wurde (Zeile 23);

4.11.2.6. Die Methode [deleteProductsByProduct1]

Die Methode [deleteProductsByProduct1] löscht Produkte aus der Tabelle [PRODUCTS]:


    @Test
    public void deleteProduitsByProduit1() {
        // filling
        fill(NB_CATEGORIES, NB_PRODUITS);
        // delete
        daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
        // check
        List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
        Assert.assertEquals(0, produits.size());
}
  • Zeile 6: Wir löschen zwei Produkte;
  • Zeilen 8–9: Wir überprüfen, ob sie nicht mehr in der Datenbank vorhanden sind;

4.11.2.7. Die Methode [getLongProductsById3]


    @Test
    public void getLongProduitsById3() {
        // remplissage
        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());
}
  • Zeile 4: Die Datenbank füllen und die Liste der gespeicherten Kategorien abrufen;
  • Zeile 7: Rufen Sie die Langform von zwei Produkten, die anhand ihrer Namen identifiziert werden, aus der Datenbank ab;
  • Zeile 9: Die Produkte [product[0,3], product[1,4]], die in der Liste der Kategorien aus Zeile 4 enthalten sind, werden dem Produktwörterbuch hinzugefügt;
  • Zeile 10: Dieselben beiden Produkte werden anhand ihrer Primärschlüssel in der Datenbank gesucht;
  • Zeilen 11–14: Wir überprüfen, ob die abgerufenen Daten mit den im Verzeichnis gespeicherten Daten übereinstimmen;

Die private Methode [checkLongProduct] lautet wie folgt:


    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. Fazit

Wir machen hier Schluss. Bislang gibt es 74 Tests, und wir könnten noch weitere hinzufügen, da ich wahrscheinlich einige Testfälle vergessen habe. Auch wenn sie nicht vollständig sind, haben diese Tests zahlreiche Fehler aufgedeckt – meist Randfälle, die bei der ursprünglichen Erstellung der [DAO]-Schicht nicht vorhergesehen wurden. Eine umfassende Testphase ist für jedes Projekt unerlässlich.

Um den Test auszuführen, können wir die importierte Ausführungskonfiguration mit dem Namen [spring-jdbc-generic-04.JUnitTestDao] verwenden.

4.11.3. Der Test [JUnitTestPushTheLimits]

Der Test [JUnitTestPushTheLimits] ist ein Leistungstest. Wir nutzen die Tatsache, dass JUnit-Tests ihre Ausführungszeit anzeigen, um die Leistung der [DAO]-Schicht zu messen. Diese Ergebnisse werden dann mit denen der JPA-Implementierungen der [DAO]-Schicht verglichen.

4.11.3.1. Grundgerüst

Das Gerüst der Klasse [JUnitTestPushTheLimits] sieht wie folgt aus:


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 {
 
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    // constants
    private final int NB_CATEGORIES = 2500;
    private final int NB_PRODUITS = 2;
 
    // local
    private Map<Long, Categorie> hCategories;
    private Map<Long, Produit> hProduits;
 
    @Before
    public void clean() {
        // empty table [CATEGORIES]
        daoCategorie.deleteAllEntities();
        // dictionaries
        hCategories = new HashMap<Long, Categorie>();
        hProduits = new HashMap<Long, Produit>();
    }
 
    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // fill the tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < nbCategories; i++) {
            Categorie categorie = new Categorie(null, 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);
        }
        // add the category - the products will be cascaded in as well
        categories = daoCategorie.saveEntities(categories);
        // dictionaries
        for (Categorie categorie : categories) {
            hCategories.put(categorie.getId(), categorie);
            for (Produit produit : categorie.getProduits()) {
                hProduits.put(produit.getId(), produit);
            }
        }
        // result
        return categories;
    }
 
....
 
    // -------------------- private methods
    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 sehen wir das Grundgerüst der Klasse [JUnitTestDao]. Wir sind bereits auf alle diese Methoden gestoßen. Der Test arbeitet mit einer Datenbank, die 2.500 Kategorien enthält, von denen jede 2 Produkte umfasst (Zeilen 32–33). Die Tabelle [CATEGORIES] wird daher 2.500 Zeilen und die Tabelle [PRODUCTS] 5.000 Zeilen umfassen. Wir hätten mehr Zeilen einfügen können, aber die Ausführung des Tests dauert bereits fast eine Minute. Wir haben uns daher für Werte entschieden, die für den Benutzer, der auf den Abschluss des Tests wartet, akzeptabel sind.

Insgesamt gibt es 18 Tests. Sie werden unter Verwendung der Ausführungskonfiguration [1] durchgeführt. Die Ausführungszeiten sind in [2] aufgeführt:

4.11.3.2. doNothing [0,114]

Die Methode [doNothing] führt keine Aktion aus. Sie dient dazu, die Dauer der Methode [clean] zu messen, die vor jedem Test ausgeführt wird und die Datenbank bereinigt. Oben sehen wir, dass die Dauer dieses Vorgangs im Vergleich zu den anderen vernachlässigbar ist.


    @Test
    public void doNothing() {
        // clean
}

4.11.3.3. perf01 [4.179]

Der Test [perf01] dient zur Messung der Datenbank-Füllzeit:


    @Test
    public void perf01() {
        // insert
        fill(NB_CATEGORIES, NB_PRODUITS);
}

4.11.3.4. perf02 [7,624]

Die Methode [perf02]:

  • füllt die Datenbank;
  • ändert anschließend die Namen aller Kategorien und die Preise aller Produkte.

    @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);
            }
        }
        // mise à jour
        daoCategorie.saveEntities(categories);
}

4.11.3.5. perf03[3,911]

Die Methode [perf03]:

  • füllt die Datenbank
  • und löscht anschließend nacheinander alle Kategorien. Aufgrund der Kaskadenbeziehung zwischen der Tabelle [CATEGORIES] und der Tabelle [PRODUCTS] werden auch die Produkte gelöscht.

Es mag hier überraschen, dass dieser Vorgang weniger Zeit [3,911 s] in Anspruch nimmt als der Vorgang [perf01] [4,179 s], der weniger leistet.


    @Test
    public void perf03() {
        // delete categories and cascade products
        daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}

Wenn wir uns den Code für die Methode [daoCategorie.deleteEntitiesByEntity] ansehen, stellen wir fest, dass ein [PreparedStatement] mit 2.500 Parametern (der Anzahl der Kategorien) ausgeführt wird. Hier kommt die Bean [maxPreparedStatementParameters] ins Spiel; sie teilt die SQL-Anweisung in mehrere [PreparedStatement]-Objekte auf, von denen jedes eine Anzahl von Parametern enthält, die das jeweilige DBMS verarbeiten kann.

4.11.3.6. perf04[2.426]

Die Methode [perf04]:

  • füllt die Datenbank;
  • ruft anschließend die vollständigen Details aller Kategorien ab;

    @Test
    public void perf04() {
        // select
        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]

Die Methode [perf05]:

  • füllt die Datenbank;
  • löscht anschließend die 5.000 Produkte anhand ihrer Primärschlüssel (sodass wir potenziell ein [PreparedStatement] mit 5.000 Parametern haben);
  • überprüft, ob die Produkttabelle nun leer ist;

    @Test
    public void perf05() {
        // delete products
        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);
        // check
        List<Produit> produits = daoProduit.getAllShortEntities();
        Assert.assertEquals(0, produits.size());
}

4.11.3.8. Ergebnisse

Wir werden die verschiedenen Tests nicht weiter vorstellen. Wir werden lediglich angeben, was sie tun und wie lange sie dauern. Diese Laufzeiten sind nur im Vergleich zueinander aussagekräftig. Ihre Werte hängen von der verwendeten Testumgebung (Hardware- und Softwarekonfiguration) ab. Wenn sie jedoch in derselben Umgebung ermittelt werden, können sie verglichen werden.

Gesamtdauer der Tests: 59,995 Sekunden

Test
Rolle
Dauer (s)
perf01
füllt die Datenbank mit 2.500 Kategorien und 5.000 Produkten
4,179
perf02
Füllt die Datenbank und ändert sie anschließend
7.624
perf03
füllt die Datenbank und löscht anschließend alle Kategorien und deren Produkte
3.911
perf04
füllt die Datenbank und fordert die Langfassung aller Kategorien an
2.426
perf05
füllt die Datenbank und löscht die 5.000 Produkte nacheinander anhand ihrer Primärschlüssel
3.507
perf06
füllt die Datenbank und löscht die 5.000 Produkte nacheinander anhand ihrer Namen
3.947
perf07
füllt die Datenbank und löscht die 5.000 Produkte nacheinander anhand ihrer Artikelnummern
3.633
perf08
füllt die Datenbank und ruft die Kurzbezeichnungen aller Produkte anhand ihrer Namen ab
4.054
perf09
füllt die Datenbank und ruft die Langversion aller Produkte nach Namen ab
2.643
perf10
füllt die Datenbank und ruft die Kurzversion aller Produkte anhand ihrer Primärschlüssel ab
3.463
perf11
füllt die Datenbank und ruft die Langbeschreibung aller Produkte anhand ihrer Primärschlüssel ab
2.777
perf12
füllt die Datenbank und löscht anschließend alle Kategorien (und damit die zugehörigen Produkte) nacheinander anhand ihrer Namen
3.806
perf13
füllt die Datenbank und löscht anschließend alle Kategorien (und die zugehörigen Produkte) nacheinander anhand ihrer Artikelnummern
2.828
perf14
füllt die Datenbank und ruft die Kurzform aller Kategorien anhand ihrer Namen ab
2.731
perf15
füllt die Datenbank und fordert die Langform aller Kategorien nach Namen an
2.603
perf16
füllt die Datenbank und ruft die Kurzversion aller Kategorien anhand ihrer Primärschlüssel ab
2.462
perf17
füllt die Datenbank und ruft die Langform aller Kategorien über deren Primärschlüssel ab
3.287

Diese Ergebnisse sind manchmal überraschend:

  • das Abrufen der Langversion der Produkte (perf09) war schneller als das der Kurzversion (perf08), obwohl die Langversion eine Verknüpfung zwischen zwei Tabellen beinhaltet;
  • die Dauer des ersten Füllvorgangs (perf01) deutlich über der aller nachfolgenden Füllvorgänge liegt;
  • das Abrufen der Kurzversion der Produkte über ihre Namen (perf08) dauert länger als das Abrufen über die Primärschlüssel (perf10). Das erscheint durchaus logisch. Bei den Langversionen ist jedoch das Gegenteil der Fall (perf09, perf11);

Wir werden daher nicht näher auf diese Ergebnisse eingehen. Sie werden jedoch nützlich sein, um diese [Spring JDBC]-Lösung mit der:

  • [Spring JDBC] für die fünf anderen DBMS;
  • [Spring JPA], das noch folgt;