Skip to content

6. Spring Data JPA Hibernate

6.1. Einführung

Wir werden die vom Projekt [spring-jdbc-04] verwaltete Datenbank [dbproduitscategories] übernehmen und die beiden in diesem Projekt definierten Schnittstellen [IDao<Categorie>, IDao<Produit>] implementieren. Dies ermöglicht uns mehrere Dinge:

  • den Implementierungscode zu vergleichen;
  • die gleiche Testschicht zu verwenden;
  • die Leistung der beiden Implementierungen zu vergleichen;
  • Die Schicht [JDBC] wird durch das in Abschnitt 3.3 behandelte Projekt [mysql-config-jdbc] implementiert;

Wir wenden uns nun den anderen Schichten zu.

6.2. Einrichtung der Arbeitsumgebung

Importieren Sie mit STS das Projekt [mysl-config-jpa-hibernate] [1], das sich im Ordner [<exemples>/spring-database-config/mysql/eclipse] [2] befindet:

Dieses Projekt konfiguriert die Ebene „[Spring JPA Hibernate]“ des Projekts. Jede Implementierung „JPA“ verfügt über ein eigenes Konfigurationsprojekt.

Importieren Sie anschließend das Projekt [spring-jpa-generic] [1], das sich im Ordner [<exemples>/spring-database-generic/spring-jpa] [2] befindet:

Anschließend setzen Sie die Maven-Umgebung (Alt-F5) aller in [Package Explorer] enthaltenen Projekte zurück:

 

Führen Sie anschließend zur Überprüfung der Arbeitsumgebung die Ausführungskonfiguration mit dem Namen [spring-jpa-generic-JUnitTestDao-hibernate] aus:

Diese Konfiguration führt den Test [JUnitTestDao] aus. Dieser Test muss erfolgreich sein:

  

6.3. Das Konfigurationsprojekt der Schicht JPA

  

Dieses Projekt dient dazu, die Schicht JPA der folgenden Architektur zu konfigurieren:

6.3.1. Maven-Konfiguration

Das Projekt ist ein Maven-Projekt und wird durch die folgende Datei [pom.xml] konfiguriert:


<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration mysql openjpa</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Variable Abhängigkeiten ********************************************** -->
        <!-- JPA-Anbieter -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- konstante Abhängigkeiten ********************************************** -->
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Spring Context -->
        <!-- geerbte Konfiguration JDBC -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 5–7: das von diesem Projekt generierte Maven-Artefakt. Die Konfigurationsprojekte der anderen Implementierungen JPA (Eclipselink und OpenJpa) verwenden dasselbe Artefakt. Das bedeutet, dass zu einem bestimmten Zeitpunkt nur eines dieser Projekte aktiv sein kann. Es ist daher zu vermeiden, dass alle in [Package Explorer] vorhanden sind. Es ist nur eines erforderlich;
  • Zeilen 10–14: das übergeordnete Maven-Projekt, das die Version der meisten für das Projekt erforderlichen Abhängigkeiten festlegt;
  • Zeilen 19–22: die Hibernate-Bibliothek;
  • Zeilen 25–28: die Spring-Data-Bibliothek;
  • Zeilen 32–34: Das Konfigurationsprojekt der Schicht JPA basiert auf dem Konfigurationsprojekt der Schicht JDBC, das unter anderem den verwendeten Treiber JDBC für SGBD sowie die Koordinaten der zu verwendenden Datenbank definiert;
  • Zeilen 35–39: Der Konfigurationsentwurf der Ebene JDBC enthält die Bibliothek [Spring JDBC], die hier durch die Bibliothek [Spring Data JPA] ersetzt wird. Daher wird empfohlen, sie nicht in die Projektabhängigkeiten aufzunehmen. Bleibt sie dennoch erhalten, verursacht dies jedoch keine Fehler;

Letztendlich lauten die Projektabhängigkeiten wie folgt:

  

6.3.2. Spring-Konfiguration

 

Die Klasse [ConfigJpa] konfiguriert das Spring-Projekt:


package generic.jpa.config;

import javax.persistence.EntityManagerFactory;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {

    // der Provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        return hibernateJpaVendorAdapter;
    }

    // Entitätspakete JPA
    public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };

    // Datenquelle
    @Bean
    public DataSource dataSource() {
        // Datenquelle TomcatJdbc
        DataSource dataSource = new DataSource();
        // Zugriffskonfiguration JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // anfänglich offene Verbindungen
        dataSource.setInitialSize(5);
        // Ergebnis
        return dataSource;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Transaktionsmanager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • Zeile 18: Die Klasse ist eine Spring-Konfigurationsklasse;
  • Zeile 19: Hier werden die Beans importiert, die durch die Konfigurationsklasse [ConfigJdbc] definiert sind, mit der das Spring-Projekt [mysql-config-jdbc] konfiguriert wurde. Dabei handelt es sich um die Filter jSON;
  • Zeilen 23–30: Definieren die verwendete Implementierung JPA, in diesem Fall die Hibernate-Implementierung (Zeile 25);
  • Zeile 26: Hier kann festgelegt werden, ob die von der Hibernate-Implementierung ausgeführten Operationen SQL angezeigt werden sollen oder nicht;
  • Zeile 27: Man gibt Hibernate den verbundenen SGBD an. Diese Konfiguration ist wichtig. Sie ermöglicht es Hibernate, den Dialekt SQL von SGBD MySQL zu verwenden, einschließlich seiner proprietären Aspekte. Außerdem erhält Hibernate dadurch Informationen über die Typen SQL und die Objekte von SGBD, die es verwenden kann. Es ist diese Fähigkeit der Implementierung JPA, sich an ein bestimmtes SGBD anzupassen, die ihr eine hohe Portabilität zwischen SGBD verleiht;
  • Zeile 28: Hibernate kann die Tabellen der Zieldatenbank aus den gefundenen Entitäten JPA generieren oder auch nicht. Diese Generierung findet nur statt, wenn die Tabellen nicht vorhanden sind. Sind sie bereits vorhanden, wird nichts unternommen. Wir werden diese Fähigkeit zur Tabellengenerierung nutzen, wenn wir erläutern, wie die Skripte SQL zur Generierung der verschiedenen in diesem Dokument verwendeten Datenbanken erstellt wurden;
  • Zeile 33: das Paket, in dem sich die Entitäten JPA der Datenbank [dbproduitscategories] befinden;
  • Zeilen 36–49: Die Datenquelle [tomcat-jdbc], die mit der Datenbank [dbproduitscategories] verknüpft ist;
  • Zeilen 52–60: Die Bean mit dem Namen [entityManagerFactory] (sie muss so heißen) ist die Bean, die das Objekt [EntityManager] erstellt, welches den Persistenzkontext JPA verwaltet. Alle Operationen von JPA laufen über dieses Objekt. Durch die Verwendung von [Spring Data JPA] werden wir dieses Objekt selbst nie direkt nutzen. Wir müssen es jedoch konfigurieren. Es benötigt folgende Informationen:
    • die verwendete Implementierung JPA (Zeile 55);
    • die verwendete Datenquelle (Zeile 57);
    • die Entitäten JPA dieser Quelle (Zeile 56);
  • Zeile 58: Initialisiert das EntityManager mit diesen Informationen;
  • Zeile 59: gibt das Singleton [entityManagerFactory] zurück;
  • Zeilen 63–68: definieren den Transaktionsmanager. Er muss den Namen [transactionManager] tragen;
  • Zeile 65: Ein Transaktionsmanager JPA wird angelegt;
  • Zeile 66: Er wird über die Bean [entityManagerFactory] (Zeilen 53 und 57) mit der Datenquelle aus Zeile 37 verbunden;

Nur die Bean aus den Zeilen 23–30 ist von der verwendeten Implementierung JPA abhängig. Die anderen Beans stützen sich anschließend auf diese.

6.3.3. Die Entitäten der Schicht [JPA]

  

Die Ziel-Datenbank ist die Datenbank [dbproduitscategories] mit ihren beiden Tabellen [CATEGORIES] und [PRODUITS]. Wir haben gesehen, dass sie außerdem drei weitere Tabellen namens [USERS, ROLES, USERS_ROLES] enthält, die zur Absicherung des Webdienstes dienen, der im Web bereitgestellt wird. Diese Tabellen lassen wir vorerst außer Acht. Zur Erinnerung noch einmal die Struktur der Tabellen [CATEGORIES] und [PRODUITS]:

Die Tabelle [PRODUITS] sieht wie folgt aus:

  • [ID]: der autoinkrementierte Primärschlüssel der Tabelle [2];
  • [NOM]: der eindeutige Name des Produkts [4];
  • [PRIX]: der Preis des Produkts;
  • [DESCRIPTION]: die Produktbeschreibung;
  • [VERSIONING] ist die Versionsnummer des Produkts. Die ursprüngliche Version lautet 1 [3]. Bei jeder Änderung des Produkts wird die Versionsnummer durch den Code, der die Tabelle auswertet, erhöht;
  • [CATEGORIE_ID]: Der Fremdschlüssel in der Tabelle [CATEGORIES], der die Kategorie angibt, zu der das Produkt gehört;
  • in [1-3], der Fremdschlüssel [CATEGORIE_ID] der Tabelle [PRODUITS]. Sie bezieht sich auf die Spalte [ID] der Tabelle [CATEGORIES] [4-5];
  • wenn eine Kategorie gelöscht wird, werden alle damit verknüpften Produkte ebenfalls gelöscht ([6]). Dieser Punkt ist wichtig zu beachten, da er beim Aufbau der Ebene [DAO] verwendet wird, die auf der Datenbank [dbproduitscategories] basiert;

Die Tabelle [CATEGORIES] der Kategorien sieht wie folgt aus:

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

Wir werden nun die Entitäten JPA, [Produit] und [Categorie] aus den Tabellen [PRODUITS] und [CATEGORIES] beschreiben.

  

6.3.3.1. Die Schnittstelle [AbstractCoreEntity]

Die Schnittstelle [AbstractCoreEntity] wird durch die Entitäten JPA, [Categorie] und [Produit] implementiert:


package generic.jpa.entities.dbproduitscategories;

public interface AbstractCoreEntity {

    // Getter und Setter der Felder [id], [version], [entityType]
    public Long getId();

    public void setId(Long id);

    public Long getVersion();

    public void setVersion(Long version);

    public enum EntityType {
        PROXY, POJO
    }

    public EntityType getEntityType();

    public void setEntityType(EntityType entityType);

}

Diese Schnittstelle, die von den beiden Entitäten JPA implementiert wird, dient lediglich dazu, die Methoden zum Lesen und Schreiben der Felder [id], [version] und [entityType] dieser Entitäten aufzulisten. Die Funktion des Feldes [entityType] wird zu einem späteren Zeitpunkt erläutert;

6.3.3.2. Die Entität JPA [Produit]

Die Klasse [Produit] ist die Entität JPA, die einer Zeile der Tabelle [PRODUITS] zugeordnet ist:

 

package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
    // Eigenschaften
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;

    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType = EntityType.POJO;

    @Transient
    @JsonIgnore
    protected String simpleClassName = getClass().getSimpleName();

    // Eigenschaften
    @Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
    private String nom;

    @Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
    private Long idCategorie;

    @Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
    private double prix;

    @Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
    private String description;

    // Kategorie
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
    private Categorie categorie;

    // Hersteller
    public Produit() {

    }

    public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
            Categorie categorie) {
        this.id = id;
        this.version = version;
        this.nom = nom;
        this.idCategorie = idCategorie;
        this.prix = prix;
        this.description = description;
        this.categorie = categorie;
    }

    // Signatur
    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);
    }

    // ------------------------------------------------------------
    // Neudefinition von [equals] und [hashcode]
    @Override
    public int hashCode() {
        Long id = getId();
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractCoreEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractCoreEntity other = (AbstractCoreEntity) entity;
        Long id = getId();
        Long otherId = other.getId();
        return id != null && otherId != null && id.equals(otherId);
    }

    // Getter und Setter
...
    public void setCategorie(Categorie categorie) {
        // Typ der Entität
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1005, new RuntimeException(
                    "On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
        }
        this.categorie = categorie;
    }

}
  • Zeile 21: Die Anmerkung [@Entity] macht die Klasse [Produit] zu einer Entität, die von der Schicht [JPA] verwaltet wird. Man kann auch [@Entity(name="MonProduit")] schreiben, wodurch die Entität den Namen [MonProduit] erhält. Fehlt diese Angabe, lautet der Name der Entität der Name der Klasse, in diesem Fall [Produit]. Diese Benennung wird notwendig, wenn sich unter den Entitäten zwei Klassen aus unterschiedlichen Paketen befinden, die denselben Namen tragen;
  • Zeile 22: Die Anmerkung [@Table(name = "PRODUITS")] gibt an, dass die Klasse [Produit] das Objektbild einer Zeile der Tabelle [PRODUITS] in der Datenbank ist;
  • Zeile 23: Der Name des Filters jSON, der auf die Entität angewendet werden soll. Wir werden sehen, dass die Eigenschaft [categorie] in Zeile 58 nicht immer verfügbar ist. Sie muss daher aus der Darstellung jSON des Objekts ausgeschlossen werden. Dazu benötigen wir einen Filter. In einem Filter mit dem Namen [jsonFilterCategorie] legen wir also fest, ob die Eigenschaft [categorie] berücksichtigt werden soll oder nicht;
  • Zeile 26: Die Anmerkung [@Id] legt fest, dass das mit der Anmerkung versehene Feld das Feld ist, das dem Primärschlüssel der Tabelle aus Zeile 19 zugeordnet ist;
  • Zeile 27: Die Annotation [@GeneratedValue(strategy = GenerationType.IDENTITY)] legt den Modus für die automatische Generierung des Primärschlüssels in der Tabelle [PRODUITS] fest. Dies wird durch das Attribut [strategy] bestimmt. Es gibt verschiedene Modi:

Image

Die Strategie [IDENTITY] ist nicht für alle SGBD verfügbar. Von den sechs getesteten SGBD war sie für die SGBD und [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5] verfügbar. Für die beiden anderen [Oracle Express 11g Release 2, Firebird 2.5.4] musste die Strategie [SEQUENCE] verwendet werden. Für die Portabilität zwischen Implementierungen JPA darf nicht die Strategie [AUTO] verwendet werden, da diese die Wahl der Strategie zur Generierung des Primärschlüssels dem Ermessen der jeweiligen Implementierung JPA überlässt. Bei MySQL 5 und der Strategie [AUTO] gilt somit:

  • wählt Hibernate die Strategie [IDENTITY] mit dem Modus [AUTO_INCREMENT] für den Primärschlüssel;
  • wählt „EclipseLink“ die Strategie „[TABLE]“, die standardmäßig eine Tabelle namens „[SEQUENCE]“ erstellt, die abgefragt werden muss, um die Primärschlüssel zu erhalten.

Letztendlich ist die von diesen beiden Implementierungen JPA verwaltete Datenbankstruktur nicht identisch. Wenn sie von Hibernate generiert wurde, kann sie nicht von EclipseLink verwendet werden und umgekehrt.

  • Zeile 28: Die Annotation [@Column(name="ID"] legt den Namen der Spalte in der Tabelle [PRODUITS] fest, die dem Feld [id] zugeordnet werden soll;
  • Zeile 29: Für den Primärschlüssel wird der Typ [Long] anstelle von [long] verwendet. Denn die Primärschlüssel [null] haben für JPA eine besondere Bedeutung. Daher wird hier lieber ein Objekttyp als ein einfacher Typ verwendet;
  • Zeile 31: Die Anmerkung [@Version] gibt an, dass das Feld [version] einer Versionsspalte zugeordnet ist. Die Implementierung JPA erhöht diese Versionsnummer jedes Mal, wenn die Entität geändert wird. Diese Nummer dient dazu, die gleichzeitige Aktualisierung der Entität durch zwei verschiedene Benutzer zu verhindern: Zwei Benutzer, U1 und U2, lesen die Entität E mit einer Versionsnummer von V1. U1 ändert E und speichert diese Änderung in der Datenbank: Die Versionsnummer ändert sich daraufhin zu V1+1. U2 ändert seinerseits E und speichert diese Änderung in der Datenbank: Es wird eine Ausnahme ausgelöst, da seine Versionsnummer (V1) von der in der Datenbank gespeicherten (V1+1) abweicht;
  • Zeile 36: Der Typ der Entität. Es gibt zwei davon: POJO und PROXY. Standardmäßig ist die erzeugte Instanz ein POJO (Plain Old Java Object). In bestimmten Fällen sind die aus der Datenbank abgerufenen Instanzen vom Typ [Produit] vom Typ [PROXY]. Dies ist der Fall, wenn die Eigenschaft [Categorie categorie] in Zeile 58 aufgrund des Attributs [fetch = FetchType.LAZY] in Zeile 56 nicht mit einer Kategorie initialisiert wurde. In diesem Fall unterscheiden sich die zu testenden Implementierungen JPA:
    • [Hibernate, OpenJPA]: Der Zugriff auf die Kategorie eines Produkts vom Typ [PROXY] löst eine Ausnahme aus. Hibernate bezeichnet eine Instanz vom Typ JPA, die im Modus [LAZY] abgerufen wurde, als „Proxy“. Aus diesem Grund habe ich diesen Begriff verwendet, um diese Art von Entität zu bezeichnen;
    • [EclipseLink]: Der Zugriff auf die Kategorie eines Produkts vom Typ [PROXY] führt dazu, dass in der Datenbank nach dieser Kategorie gesucht wird, und es tritt keine Ausnahme auf;

Da ich eine von der verwendeten Implementierung JPA unabhängige Testebene haben wollte, wollte ich den Typ jeder Entität kennen: POJO oder PROXY. Deshalb habe ich das Feld [entityType] zu den Entitäten JPA hinzugefügt;

  • Zeile 35: Die Anmerkung [@Transient] besagt, dass die Implementierung JPA dieses Feld ignorieren muss. Tatsächlich existiert es in den Tabellen von SGBD nicht;
  • Zeile 40: Die Klasse [Produit] löst eine Ausnahme vom Typ [ProxyException] aus, die den Namen der Klasse benötigt;
  • Zeile 38: Wie zuvor wird angegeben, dass die Implementierung JPA dieses Feld ignorieren muss;
  • Zeile 39: Die Annotation [@JsonIgnore] gibt an, dass der Serialisierer/Deserialisierer jSON einer Instanz [Produit] dieses Feld ignorieren soll;
  • Zeile 43: Die Anmerkung [@Column] ordnet das Feld [nom] der Spalte [NOM] der Tabelle [PRODUITS] zu. Wenn das Feld denselben Namen wie die zugehörige Spalte trägt (Groß-/Kleinschreibung spielt keine Rolle), kann die Annotation [@Column] weggelassen werden. Dies wäre hier der Fall. Die Attribute [unique = true, length = 30, nullable = false] werden nur verwendet, wenn die Implementierung JPA die Tabelle [CATEGORIES] aus der Entität [Produit] generieren muss. Sie werden durch die Attribute SQL und [UNIQUE, VARCHAR(30), NOT NULL] umgesetzt, wodurch die Spalte [NOM] höchstens 30 Zeichen lang ist, in der Tabelle eindeutig ist und nicht den Wert NULL annehmen darf;
  • Zeilen 46–47: Das Feld [idCategorie] ist mit der Spalte [CATEGORIE_ID] verknüpft. Auf seine Attribute werden wir etwas später noch zurückkommen;
  • Zeilen 49–50: Das Feld [prix] ist der Spalte [PRIX] zugeordnet;
  • Zeilen 52–53: Das Feld [description] ist der Spalte [DESCRIPTION] zugeordnet;
  • Zeilen 56–58: die Produktkategorie;
  • Zeile 56: Die Anmerkung [@ManyToOne] gibt an, dass die Spalte der Anmerkung in Zeile 57 ([@JoinColumn(name = "CATEGORIE_ID")]) ein Fremdschlüssel der Tabelle [PRODUITS] derEntität [Produit] auf die Tabelle [CATEGORIES], die der Entität in Zeile 58 zugeordnet ist. Diese Anmerkung muss eine Entität JPA kennzeichnen. Somit muss die Klasse in Zeile 58 eine Entität JPA sein;
  • Zeile 56: Die Annotation [fetch = FetchType.LAZY] sorgt dafür, dass beim Abrufen eines Produkts aus der Tabelle [PRODUITS] dessen Kategorie (Zeile 58) nicht sofort abgerufen wird (Lazy Loading). Sie wird dann beim ersten Aufruf der Methode [getCategorie] abgerufen. Zu diesem Zweck erweitert erweitert die Schicht JPA die ursprüngliche Methode [getCategorie] (die lediglich das Feld categorie zurückgibt) durch einen Aufruf von SGBD, um die Kategorie abzurufen – eine Technik, die als „Proxying“ bezeichnet wird. Die Implementierungen von JPA unterscheiden sich, wie oben erwähnt, in der Umsetzung dieses Merkmals. Dieses Attribut ist nicht zwingend. Die verwendete Implementierung JPA darf es ignorieren. Da die Eigenschaft [categorie] vorhanden sein kann oder auch nicht, haben wir den Filter jSON in Zeile 23 eingeführt. Die Verknüpfungsspalte [CATEGORIE_ID] der Tabelle [PRODUITS] wird beim Einfügen oder Aktualisieren des Produkts automatisch aktualisiert. Sie erhält den Wert von [categorie.getId()], wobei [categorie] das Feld in Zeile 58 ist. Die Spezifikation JPA schreibt vor, dass diese Verknüpfungsspalte nicht auf andere Weise aktualisiert werden darf. Daher schreibt sie die Attribute [insertable = false, updatable = false] aus Zeile 46 vor, die bewirken, dass die Spalte [CATEGORIE_ID] (also die Verknüpfungsspalte), die dem Feld [idCategorie] zugeordnet ist, nicht durch das Feld [idCategorie] geändert werden kann. Lediglich die Übertragung der Spalte [CATEGORIE_ID] in das Feld [idCategorie] ist möglich;
  • Zeilen 91–104: Die Gleichheit zwischen den Entitäten [Produit] wird als Gleichheit ihrer Primärschlüssel [id] definiert;
  • Zeilen 108–115: Um unsere Testschicht portabel zu gestalten, werden wir die Entitäten [PROXY] der drei Implementierungen JPA und [Hibernate, EclipseLink, OpenJpa] einheitlich behandeln. Für einen Typ [Produit] vom Typ [PROXY] wird die Änderung des Werts des Feldes [categorie] untersagt. Die Klasse [ProxyException] lautet wie folgt:
  

package generic.jpa.infrastructure;

import generic.jdbc.infrastructure.UncheckedException;

public class ProxyException extends UncheckedException {

    private static final long serialVersionUID = 7278276670314994574L;

    public ProxyException() {
    }

    public ProxyException(int code, Throwable e, String simpleClassName) {
        super(code, e, simpleClassName);
    }

}

Zum Abschluss der Betrachtung dieser Entität ist anzumerken, dass die Annotationen und ihre Attribute in zwei ganz unterschiedlichen Fällen verwendet werden:

  • zum Anlegen der Datenbanktabellen;
  • um sie zu verarbeiten. In diesem Fall erwartet die Implementierung JPA, dass die Tabellen so vorliegen, wie sie sie selbst generiert hätte. Man kann daher der oben genannten Entität [Produit] nicht beliebige Tabellen vom Typ [PRODUITS] zuordnen. Diese muss mindestens (sie kann auch weitere haben) die Merkmale der Tabelle [PRODUITS] aufweisen, die sie selbst generiert hätte. Wenn man mit JPA arbeitet, ist es am besten, von einer leeren Datenbank auszugehen, in der man JPA die Tabellen generieren lässt. Auf diese Generierung werden wir etwas später eingehen. Das für SGBD und MySQL bereitgestellte Skript SQL wurde auf der Grundlage der von JPA generierten Tabellen erstellt.

Alle Attribute der Entität [Produit] werden für die Generierung der Tabelle [PRODUITS] verwendet. Sobald dies geschehen ist, werden Generierungsattribute wie [unique = true, length = 30, nullable = false] bei der Auswertung der Tabellen nicht mehr verwendet.

6.3.3.3. Die Entität JPA [Categorie]

Die Klasse [Categorie] ist eine Entität JPA, die einer Zeile der Tabelle [CATEGORIES] zugeordnet ist:

Ihr Code lautet wie folgt:


package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;

    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType = EntityType.POJO;

    @Transient
    @JsonIgnore
    protected String simpleClassName = getClass().getSimpleName();

    // Eigenschaften
    @Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
    private String nom;

    // zugehörige Produkte
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    private List<Produit> produits;

    // Konstruktoren
    public Categorie() {

    }

    public Categorie(Long id, Long version, String nom, List<Produit> produits) {
        this.id = id;
        this.version = version;
        this.nom = nom;
        this.produits = produits;
    }

    // Signatur
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
    }

    // Methoden
    public void addProduit(Produit produit) {
        // Entitätstyp
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1004, new RuntimeException(
                    "On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
        }
        // Hinzufügen eines Produkts
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // Das Produkt wird hinzugefügt
            produits.add(produit);
            // Kategorie festlegen
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }

    // ------------------------------------------------------------
    // Neudefinition von [equals] und [hashcode]
    @Override
    public int hashCode() {
        Long id = getId();
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractCoreEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractCoreEntity other = (AbstractCoreEntity) entity;
        Long id = getId();
        Long otherId = other.getId();
        return id != null && otherId != null && id.equals(otherId);
    }

    // Getter und Setter
...
}
  • Zeile 24: Die Klasse ist eine Entität JPA;
  • Zeile 25: verknüpft mit der Tabelle [CATEGORIES];
  • Zeile 26: Die Darstellung jSON der Entität [Categorie] wird durch den Filter mit dem Namen [jsonFilterCategorie] gesteuert. Dieser muss vor jeder Anforderung der Darstellung jSON der Entität konfiguriert werden. Der Filter [jsonFilterCategorie] wird verwendet, um das Feld [produits] in Zeile 40 aus der Darstellung jSON der Entität [Categorie] auszuschließen oder nicht;
  • Zeilen 29–32: Das Feld [id] ist dem Primärschlüssel [ID] der Tabelle [CATEGORIES] zugeordnet. Als Generierungsmodus wurde der Modus [IDENTITY] gewählt, also der Modus [AUTO_INCREMENT] für MySQL;
  • Zeilen 34–36: Das Feld [version] ist mit der Versionsspalte [VERSIONING] der Tabelle [CATEGORIES] verknüpft;
  • Zeilen 38–39: Der Typ der Entität [Categorie];
  • Zeilen 41–43: der einfache Name der Klasse [Categorie];
  • Zeilen 46–47: Das Feld [nom] ist mit der Spalte [NOM] der Tabelle [CATEGORIES] verknüpft. Ihm werden die Attribute JPA und [unique = true, length = 30, nullable=false] zugewiesen, damit bei der Generierung der Tabelle [CATEGORIES] die Spalte [NOM] die Attribute SQL und [UNIQUE, VARCHAR(30), NOT NULL] erhält;
  • Zeilen 50–51: die Produkte, die zu dieser Kategorie gehören;
  • Zeile 50: Die Annotation [@OneToMany] ist die inverse Beziehung zur Beziehung [@ManyToOne], auf die wir in der Entität [Produit] gestoßen sind. Das Attribut [mappedBy = "categorie"] gibt das Feld der Entität [Produit] an, das durch die umgekehrte Beziehung [@ManyToOne] annotiert ist. Das Attribut [cascade = { CascadeType.ALL }] legt fest, dass die Operationen (persist, merge, remove), die an einer @Entity [Categorie] durchgeführt werden, auf die [produits] in Zeile 51 kaskadiert werden. Teilweise Kaskadierungen können mit den Konstanten [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] angegeben werden;
  • Zeile 50: Das Attribut [fetch = FetchType.LAZY] legt fest, dass beim Abrufen einer Kategorie aus der Tabelle [CATEGORIES] deren Produkte nicht sofort mitgeliefert werden. Dies geschieht erst beim ersten Aufruf der Methode [getProduits]. Zu diesem Zweck erweitert die Schicht JPA die ursprüngliche Methode [getProduits] (die lediglich das Feld produits zurückgibt) um einen Aufruf der Methode SGBD erweitert, um die Produkte der Kategorie abzurufen. Dieses Attribut ist verbindlich. Die Implementierung JPA darf es nicht ignorieren. Da die Eigenschaft [produits] initialisiert sein kann oder auch nicht, haben wir den Filter jSON in Zeile 26 eingeführt, mit dem wir angeben können, ob diese Eigenschaft gewünscht ist oder nicht, sowie den Typ der Entität in Zeile 39;
  • Zeilen 71–88: Mit der Methode [addProduit] kann ein Produkt zur Kategorie hinzugefügt werden;
  • Zeilen 73–76: Um die Verwaltung von Proxys zwischen verschiedenen Implementierungen von JPA zu vereinheitlichen, wurde festgelegt, dass keine Produkte zu einer Entität vom Typ [Categorie] hinzugefügt werden dürfen, deren Typ PROXY ist;
  • Zeilen 92–112: Zwei Entitäten vom Typ [Categorie] gelten als gleich, wenn sie denselben Primärschlüssel [id] haben;

6.3.4. Die Datei [persistence.xml]

  

Die Anwendungen JPA müssen bestimmte Eigenschaften des verwendeten Anbieters JPA sowie die zu verwendenden Entitäten JPA in einer Datei [META-INF/persistence.xml] definieren, die sich im Classpath der Anwendung befindet. Im obigen Beispiel wurde sie im Ordner „[src/main/resources]“ abgelegt, der tatsächlich Teil des Classpaths eines Eclipse-Projekts ist. Bei der Verwendung von JPA in Verbindung mit Spring werden bestimmte Informationen, die eigentlich in der Datei [persistence.xml] enthalten sein sollten, an anderer Stelle in Spring-Konfigurationsklassen abgelegt. In einer Spring-Anwendung JPA steuert Spring die Datei JPA. Bei Spring JPA mit Hibernate lässt sich die Datei [persistence.xml] auf das Wesentliche reduzieren:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • Zeilen 1–5: Eine [persistence.xml]-Datei muss ein Stamm-Tag <persistence> enthalten. Die Attribute des Tags in Zeile 2 werden in dieser Anwendung nicht verwendet;
  • Eine Persistenzdatei kann eine oder mehrere Persistenzeinheiten mit dem Tag <persistence-unit> (Zeile 4) definieren. Eine Persistenzeinheit verwaltet den Zugriff auf eine bestimmte Datenbank. Wenn die Anwendung zwei Datenbanken gleichzeitig verwaltet, verfügt sie über zwei Persistenzeinheiten;
  • Zeile 4: Eine Persistenz-Einheit hat den Namen [attribut name], unterstützt den Transaktionstyp [attribut transaction-type], verfügt über Eigenschaften und definiert die Entitäten, die den Tabellen der von der Persistenz-Einheit verwalteten Datenbank zugeordnet sind. Da hier der Zugriff auf die Datenbank von [Spring JPA Hibernate] verwaltet wird, können diese beiden letzten Informationen an anderer Stelle platziert werden. Es gibt zwei Transaktionstypen:
    • [RESOURCE_LOCAL]: Die Transaktionen werden von der Anwendung selbst verwaltet. Dies ist hier der Fall, wo Spring die Transaktionen verwaltet;
    • [JTA] (Java-Transaktion API): Der Container EJB (Enterprise Java Bean), der die Anwendung ausführt, verwaltet die Transaktionen automatisch anhand der im Code gefundenen Java-Annotationen. Diese Konfiguration liegt hier nicht vor;

Wir werden im Folgenden sehen, dass der Inhalt dieser Datei [persistence.xml] von der verwendeten Implementierung JPA abhängt.

6.4. Das Projekt [spring-jpa-generic]

Erinnern wir uns daran, was wir erreichen wollen. Wir möchten die folgende Architektur implementieren:

, in der die Schicht [DAO] die in Kapitel 4 behandelte Schnittstelle [IDao<Produit>, IDao<Categorie>] implementieren würde. Es geht darum, zwei Implementierungen dieser Schnittstelle zu vergleichen:

  • die eine mit Spring erstellt (JDBC);
  • die andere mit Spring JPA erstellt;

In der obigen Architektur:

  • wird die Schicht [JDBC] durch das in Abschnitt 3.3 behandelte Projekt [mysql-config-jdbc] implementiert;
  • Die Schicht [JPA] wird durch das in Abschnitt 6.3 behandelte Projekt [mysql-config-jpa-hibernate] implementiert;

Das Projekt [spring-jpa-generic] sorgt für die Implementierung der Schichten [DAO] und [Spring Data].

  

6.4.1. Maven-Konfiguration

Das Projekt [spring-jpa-generic] ist ein Maven-Projekt, das durch die folgende Datei [pom.xml] 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-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jpa-generic</name>
    <description>démo spring data avec tables de catégories et de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Konfiguration von JPA für SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jpa</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 22–26: Das Projekt hat nur eine einzige Abhängigkeit, nämlich die vom Projekt, das die [JPA]-Schicht der Anwendung konfiguriert und das wir gerade untersucht haben. Es handelt sich um eine generische Anwendung:
    • Man wechselt von SGBD, indem man das Konfigurationsprojekt der Schicht [JDBC] ändert;
    • die Implementierung JPA wird geändert, indem das Konfigurationsprojekt der Schicht [JPA] geändert wird;

Letztendlich ergeben sich folgende Abhängigkeiten:

  

6.4.2. Spring-Konfiguration

  

Die Klasse [AppConfig] konfiguriert das Spring-Projekt:


package spring.data.config;

import generic.jpa.config.ConfigJpa;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {

}
  • Zeile 11: Die Klasse ist eine Spring-Konfigurationsklasse;
  • Zeile 10: Die Annotation [@EnableJpaRepositories] dient dazu, die Pakete zu kennzeichnen, die die Schnittstellen [CrudRepository] von Spring Data enthalten. Dadurch werden sie zu Spring-Komponenten, die in andere Spring-Komponenten injiziert werden können;
  • Zeile 12: Die Annotation [@ComponentScan] gibt an, dass das Paket [spring.data.dao] nach Spring-Komponenten durchsucht werden soll. Dabei werden die Komponenten [DaoCategorie] und [DaoProduit] gefunden;
  • Zeile 13: Die Beans der Konfigurationsklasse [ConfigJpa] werden importiert. Darin befindet sich die Bean der verwendeten Implementierung JPA (Hibernate, Eclipselink, OpenJpa), die zu verwendende Datenquelle, EntityManager, das die Operationen JPA verwaltet, sowie der Transaktionsmanager;

6.4.3. Die Schicht [Spring Data]

  

6.4.3.1. Die Schnittstelle [CategoriesRepository]

Die Schnittstelle [CategoriesRepository] verwaltet den Zugriff auf die Tabelle [CATEGORIES]:


package spring.data.repositories;

import generic.jpa.entities.dbproduitscategories.Categorie;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface CategoriesRepository extends CrudRepository<Categorie, Long> {

    // Kategorie mit ihren Produkten
    @Query("select c from Categorie c left join fetch c.produits where c.id=?1")
    public Categorie getLongCategorieById(Long id);

    @Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
    public Categorie getLongCategorieByName(String nom);

    @Query("select c from Categorie c where c.nom in ?1")
    public List<Categorie> getShortCategoriesByName(Iterable<String> names);

    @Query("select c from Categorie c where c.id in ?1")
    public List<Categorie> getShortCategoriesById(Iterable<Long> ids);

    @Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
    public List<Categorie> getLongCategoriesById(List<Long> names);

    @Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
    public List<Categorie> getLongCategoriesByName(List<String> names);

    @Query("select c from Categorie c")
    public List<Categorie> getAllShortCategories();

    @Query("select distinct c from Categorie c left join fetch c.produits")
    public List<Categorie> getAllLongCategories();

}
  • Zeile 10: Die Schnittstelle [CrudRepository] wurde in Abschnitt 5.1.3 verwendet und erläutert. Zur Erinnerung:
    • Der erste Parametertyp der Schnittstelle ist die Entität JPA, die für Zugriffe auf CRUD (findOne, findAll, Speichern, Löschen, deleteAll) verwaltet wird,
    • der zweite Parametertyp der Schnittstelle ist der Primärschlüssel der Entität JPA, hier eine Ganzzahl [Long];

Die Methoden der Schnittstelle werden durch Abfragen JPQL (Java Persistence Query Language) implementiert. Diese Abfrage bezieht sich auf Entitäten JPA. In einer solchen Abfrage:

  • werden die Tabellen durch die ihnen zugeordneten Entitäten JPA ersetzt;
  • die Spalten werden durch Felder der in der Abfrage verwendeten Entitäten JPA ersetzt;

Nehmen wir das Beispiel der Zeilen 31–32: Die Methode in Zeile 32 gibt alle Kategorien der Datenbank in ihrer Kurzform zurück. Sie wird durch die Abfrage JPQL (Java Persistence Query Language) in Zeile 31 implementiert, die ihrem Pendant SQL sehr ähnlich ist. Um mehr über JPQL zu erfahren, kann man [ref2] lesen (siehe Abschnitt 1.2).

Die Methoden der Schnittstelle [CategoriesRepository] lauten wie folgt:

  • Zeilen 13–14: Die Methode [getLongCategorieById] gibt die Langform einer Kategorie zurück, auf die über ihren Primärschlüssel [id] verwiesen wird, d. h. die Kategorie mit ihren Produkten. Zur Erinnerung: In der Entität [Categorie] hatte das Feld [produits] das Attribut [fetch = FetchType.LAZY] (Lazy Loading). In der Abfrage JPQL wird das Laden der Produkte mit dem Schlüsselwort [fetch] erzwungen. Der Parameter ?1 der Abfrage wird bei der Ausführung durch den Wert des ersten Parameters der Methode in Zeile 12 ersetzt, also durch den Parameter [Long id];
  • Zeilen 16–17: Die Methode [getLongCategorieByName] liefert die Langform einer Kategorie, auf die über ihren Namen [nom] verwiesen wird;
  • Zeilen 19–20: Die Methode [getShortCategoriesByName] gibt die Kurzversionen von Kategorien zurück, auf die über ihre Namen verwiesen wird. Das Feld [produits] dieser Kategorien ist nicht null. Es enthält die Referenz eines Proxys (eine von der Implementierung JPA erstellte Klasse), dessen Aufgabe es ist, bei Aufruf die Produkte der Kategorie zurückzugeben. Sein Aufruf außerhalb des Persistenzkontexts JPA löst eine Ausnahme aus (Hibernate und OpenJpa, jedoch nicht EclipseLink). Aus diesem Grund werden wir das Feld [produits] der Kurzversion einer Kategorie nicht verwenden;
  • Zeilen 22–23: Die Methode [getShortCategoriesById] gibt die Kurzversionen von Kategorien zurück, auf die über ihre Primärschlüssel [id] verwiesen wird;
  • Zeilen 25–26: Die Methode [getLongCategoriesById] liefert die Langversionen der Kategorien, auf die über ihre Primärschlüssel [id] verwiesen wird;
  • Zeilen [28-29]: Die Methode [getLongCategoriesByName] gibt die Langversionen der Kategorien zurück, die anhand ihrer Namen referenziert werden;
  • Zeilen 31–32: Die Methode [getAllShortCategories] gibt die Kurzversionen aller Kategorien zurück;
  • Zeilen 34–35: Die Methode [getAllLongCategories] gibt die Langversionen aller Kategorien zurück;

Hinweis: Nicht alle Implementierungen von JPA akzeptieren dieselbe Syntax wie JPQL. So wird die folgende Syntax von Hibernate und EclipseLink akzeptiert, nicht jedoch von OpenJpa:


@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")

OpenJpa akzeptiert den oben genannten Alias [p] nicht.

6.4.3.2. Die Schnittstelle [ProduitsRepository]

Die Schnittstelle [ProduitsRepository] verwaltet den Zugriff auf die Tabelle [PRODUITS]:


package spring.data.repositories;

import generic.jpa.entities.dbproduitscategories.Produit;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;

@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {

    // ein Produkt mit seiner Kategorie
    @Query("select p from Produit p left join fetch p.categorie where p.id=?1")
    public Produit getLongProduitById(Long id);

    @Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
    public Produit getLongProduitByName(String nom);

    @Query("select p from Produit p where p.id in ?1")
    public List<Produit> getShortProduitsById(List<Long> ids);

    @Query("select p from Produit p where p.nom in ?1")
    public List<Produit> getShortProduitsByName(List<String> names);

    @Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
    public List<Produit> getLongProduitsById(List<Long> ids);

    @Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
    public List<Produit> getLongProduitsByName(List<String> names);

    @Query("select distinct p from Produit p left join fetch p.categorie")
    public List<Produit> getAllLongProduits();

    @Query("select p from Produit p")
    public List<Produit> getAllShortProduits();
}
  • Zeilen [15-16]: Die Methode [getLongProduitById] liefert die Langbeschreibung eines Produkts, das durch seinen Primärschlüssel [id] identifiziert wird, also einschließlich seiner Kategorie. Wir erinnern uns, dass in der Entität [Produit] das Feld [categorie] das Attribut [fetch = FetchType.LAZY] (Lazy Loading) hatte. In der Abfrage JPQL erzwingen wir das Laden der Kategorie mit dem Schlüsselwort [fetch];
  • Zeilen 18–19: Die Methode [getLongProduitByName] liefert die Langform eines Produkts, das durch seinen Namen identifiziert wird;
  • Zeilen 21–22: Die Methode [getShortProduitsById] liefert die Kurzversion der Produkte, die durch ihren Primärschlüssel [id] identifiziert werden. In dieser Kurzversion hat das Feld [categorie] nicht den Wert null. Es enthält die Referenz eines Proxys, der von der Implementierung JPA generiert wurde und der, wenn er aufgerufen wird, die Produktkategorie abruft. Dieser Aufruf kann nur im Persistenzkontext JPA erfolgen. Ein Aufruf an anderer Stelle löst eine Ausnahme aus (Hibernate und OpenJpa, jedoch nicht EclipseLink). Daher werden wir in der Schicht [DAO] oder anderswo das Feld [categorie] eines Produkts in seiner Kurzversion nicht verwenden. In der Kurzversion des Produkts wird das Feld [idCategorie] initialisiert. Sein Wert ist der Primärschlüssel der Kategorie, zu der das Produkt gehört. Dies ermöglicht es später, diese Kategorie über die Methode [DaoCategorie. getShortCategoriesById(idCategorie)] von der Ebene [DAO] abzufragen;
  • Zeilen 24–25: Die Methode [getShortProduitsByName] liefert die Kurzversion der Produkte, die anhand ihrer Namen identifiziert werden;
  • Zeilen 27–28: Die Methode [getLongProduitsById] gibt die Langform der Produkte zurück, die anhand ihrer Primärschlüssel identifiziert werden;
  • Zeilen 30–31: Die Methode [getLongProduitsByName] gibt die Langform der anhand ihrer Namen identifizierten Produkte zurück;
  • Zeilen 33–34: Die Methode [getAllLongProduits] gibt die Langform aller Produkte zurück;
  • Zeilen 36–37: Die Methode [getAllShortProduits] gibt die Kurzversion aller Produkte zurück;

Diese Schnittstellen werden durch Klassen implementiert, die bei der Ausführung des Projekts von der Implementierung JPA generiert werden. Solche Klassen werden als [proxy]-Klassen bezeichnet. Standardmäßig werden die Methoden der Schnittstelle [CrudRepository] in einer Transaktion ausgeführt. Da die Schnittstellen [ProduitsRepository, CategoriesRepository] die Klasse [CrudRepository] erweitern, sind sie Spring-Komponenten. Als solche können sie in andere Spring-Komponenten injiziert werden.

6.4.4. Die Schicht [DAO]

  

6.4.4.1. Die Schnittstelle [IDao<T>]

Die Schnittstelle [IDao<T>] ist diejenige, die bereits bei der Implementierung der Schicht [DAO] mit Spring JDBC behandelt wurde (siehe Abschnitt 4.7);


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;

import java.util.List;

public interface IDao<T extends AbstractCoreEntity> {

    // Liste aller T-Entitäten
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // bestimmter Entitäten – Kurzversion
    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);

    // bestimmte Entitäten – Langfassung
    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);

    // Aktualisierung mehrerer Entitäten
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // Löschen aller Entitäten
    public void deleteAllEntities();

    // Löschen mehrerer Entitäten
    public void deleteEntitiesById(Iterable<Long> ids);

    public void deleteEntitiesById(Long... ids);

    public void deleteEntitiesByName(Iterable<String> names);

    public void deleteEntitiesByName(String... names);

    public void deleteEntitiesByEntity(Iterable<T> entities);

    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}

6.4.4.2. Die abstrakte Klasse [AbstractDao]

  

Die abstrakte Klasse [AbstractDao] ist die übergeordnete Klasse der Klassen, die die Schicht [DAO] implementieren:

  • die Klasse [DaoProduit], die die Schnittstelle [IDao<Produit>] implementiert und den Zugriff auf die Tabelle [PRODUITS] verwaltet;
  • die Klasse [DaoCategorie], die die Schnittstelle [IDao<Categorie>] implementiert und den Zugriff auf die Tabelle [CATEGORIES] verwaltet;

Der Code entspricht dem in Abschnitt 4.8 beschriebenen, mit folgendem Unterschied: Keine Methode verfügt über das Attribut [@Transactional], das bewirkt, dass die Methode in einer Transaktion ausgeführt wird. Hier wird die Tatsache genutzt, dass die Schnittstellen [CrudRepository] von Spring Data standardmäßig in einer Transaktion ausgeführt werden.

6.4.4.3. Die Klasse [DaoCategorie]

  

Die Klasse [DaoCategorie] implementiert die Schnittstelle [IDao<Categorie>] wie folgt:


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;

@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    @Autowired
    private ProduitsRepository produitsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Categorie> getAllShortEntities() {
        try {
            return setShortCategoriesType(categoriesRepository.getAllShortCategories());
        } catch (Exception e) {
            throw new DaoException(211, e, simpleClassName);
        }
    }

    private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
        for (Categorie categorie : categories) {
            categorie.setEntityType(EntityType.PROXY);
        }
        return categories;
    }

    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            return categoriesRepository.getAllLongCategories();
        } catch (Exception e) {
            throw new DaoException(202, e, simpleClassName);
        }
    }

    @Override
    public void deleteAllEntities() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(208, e, simpleClassName);
        }
    }

    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
        } catch (Exception e) {
            throw new DaoException(203, e, simpleClassName);
        }
    }

    @Override
    protected List<Categorie> getShortEntitiesByName(List<String> names) {
        try {
            return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
        } catch (Exception e) {
            throw new DaoException(204, e, simpleClassName);
        }
    }

    @Override
    protected List<Categorie> getLongEntitiesById(List<Long> ids) {
        try {
            return categoriesRepository.getLongCategoriesById(ids);
        } catch (Exception e) {
            throw new DaoException(205, e, simpleClassName);
        }
    }

    @Override
    protected List<Categorie> getLongEntitiesByName(List<String> names) {
        try {
            return categoriesRepository.getLongCategoriesByName(names);
        } catch (Exception e) {
            throw new DaoException(206, e, simpleClassName);
        }
    }

    @Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
    ...
    }

    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            categoriesRepository.delete(getShortEntitiesById(ids));
        } catch (Exception e) {
            throw new DaoException(209, e, simpleClassName);
        }
    }

    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            categoriesRepository.delete(getShortEntitiesByName(names));
        } catch (Exception e) {
            throw new DaoException(212, e, simpleClassName);
        }
    }

}
  • Zeile 17: Die Annotation [@Component] macht die Klasse [DaoCategorie] zu einer Spring-Komponente;
  • Zeile 18: Die Klasse [DaoCategorie] erweitert die Klasse [AbstractDao<Categorie>], wodurch sie die Schnittstelle [IDao<Categorie>] implementiert;
  • Zeilen 20–24: Injektion der Referenzen auf die beiden Schnittstellen [CrudRepository] und [Spring Data]. Diese Injektion erfolgt bei der Instanziierung der Spring-Objekte, in der Regel zu Beginn der Ausführung des Spring-Projekts;
  • Alle Methoden der Klasse delegieren die Arbeit an die gleichnamigen Methoden der Schnittstellen [CrudRepository];
  • alle Methoden, die die Entitäten in ihre Kurzform zurückführen, kennzeichnen dies, indem sie den Typ der Entität auf [EntityType.PROXY] setzen (Zeilen 29, 63, 72);

Die Methode [saveEntities] bedarf einer Erläuterung:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // Die Produkte, die eingefügt werden sollen, werden notiert
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // Bei dieser Gelegenheit wird (falls erforderlich) die Beziehung „Produkt → Kategorie“ wiederhergestellt
                    produit.setCategorie(categorie);
                }
            }
        }
        // Kategorien und Produkte werden gespeichert
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // Das Feld [idCategorie] der eingefügten Produkte wird aktualisiert
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // Ergebnis
        return categories;
    }
  • Zeile 2: Die als Parameter übergebenen Kategorien sind sowohl einzufügende Kategorien ([id==null]) als auch zu ändernde Kategorien ([id!=null]);
  • Zeile 20: Die Kategorien werden mit der Methode [categoriesRepository.save(entities)] gespeichert. Bei Tests stellt man fest, dass das Feld [idCategorie] der gespeicherten Produkte (id==null) nicht ausgefüllt ist. Um dieses Problem zu beheben, werden in den Zeilen 4–17 die Produkte vermerkt, die eingefügt werden sollen, und nach der Speicherung wird ihr Feld [idCategorie] (Zeilen 25–27) ausgefüllt;
  • Zeilen 5–17: Die Liste der Kategorien wird durchlaufen;
  • Zeilen 8–16: Für jede Kategorie wird die zugehörige Produktliste durchlaufen. Hier gibt es eine Schwierigkeit. Die Methode [saveEntities] wird sowohl zum Speichern als auch zum Ändern einer Kategorie verwendet. Im letzteren Fall kann die Kategorie möglicherweise in ihrer Kurzform abgerufen worden sein, sodass das Feld [produits] den Verweis auf eine Proxy-Methode enthält. Die Verwendung mit Hibernate löst dann eine Ausnahme aus, da sich die verwendete Kategorie nicht mehr im Persistenzkontext JPA befindet, der mit dem Ende der Transaktion der Methode geschlossen wurde, die die Kurzversionen der Kategorien zurückgegeben hat. Man verwendet dann das Feld [EntityType] der Entität [Categorie] in Zeile 8, um festzustellen, ob auf die Liste der Produkte der Kategorie zugegriffen werden kann oder nicht;
  • Zeile 14: Das Produkt wird seiner Kategorie zugeordnet. Normalerweise sollte dies bereits der Fall sein. Es ist jedoch nicht bekannt, wie dieses Produkt angelegt wurde und ob es seiner Kategorie zugeordnet wurde. Um also jegliche Probleme zu vermeiden (damit die Entität [Produit] verwaltet werden kann, muss JPA auf die Entität [Categorie] verweisen, mit der sie verknüpft ist), stellen wir diese Verknüpfung selbst her.

Vergleicht man diesen Code mit dem der Klasse [DaoProduit] aus der Spring-Implementierung JDBC (siehe Abschnitt 4.9), lässt sich feststellen, dass die Spring-Data-Bibliothek JPA das Schreiben der Schicht [DAO] erheblich erleichtert.

6.4.4.4. Die Klasse [DaoProduit]

  

Die Klasse [DaoProduit] implementiert die Schnittstelle [IDao<Produit>] wie folgt:


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;

import com.google.common.collect.Lists;

@Component
public class DaoProduit extends AbstractDao<Produit> {
    @Autowired
    private ProduitsRepository produitsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Produit> getAllShortEntities() {
        try {
            return setShortProduitsType(produitsRepository.getAllShortProduits());
        } catch (Exception e) {
            throw new DaoException(102, e, simpleClassName);
        }
    }

    private List<Produit> setShortProduitsType(List<Produit> produits) {
        for (Produit produit : produits) {
            produit.setEntityType(EntityType.PROXY);
        }
        return produits;
    }

    @Override
    public List<Produit> getAllLongEntities() {
        try {
            return produitsRepository.getAllLongProduits();
        } catch (Exception e) {
            throw new DaoException(117, e, simpleClassName);
        }
    }

    @Override
    public void deleteAllEntities() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(112, e, simpleClassName);
        }
    }

    @Override
    protected List<Produit> getShortEntitiesById(List<Long> ids) {
        try {
            return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
        } catch (Exception e) {
            throw new DaoException(103, e, simpleClassName);
        }
    }

    @Override
    protected List<Produit> getShortEntitiesByName(List<String> names) {
        try {
            return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
        } catch (Exception e) {
            throw new DaoException(104, e, simpleClassName);
        }
    }

    @Override
    protected List<Produit> getLongEntitiesById(List<Long> ids) {
        try {
            return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
        } catch (Exception e) {
            throw new DaoException(105, e, simpleClassName);
        }
    }

    @Override
    protected List<Produit> getLongEntitiesByName(List<String> names) {
        try {
            return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
        } catch (Exception e) {
            throw new DaoException(106, e, simpleClassName);
        }
    }

    private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
        for (Produit produit : produits) {
            Categorie categorie = produit.getCategorie();
            if (categorie != null) {
                produit.setCategorie(categorie);
                produit.setIdCategorie(categorie.getId());
            }
        }
        return produits;
    }

    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // Die Verknüpfung zwischen einem Produkt und seiner Kategorie wird (falls erforderlich) wiederhergestellt
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // Die Produkte werden gespeichert
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
    }

    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            produitsRepository.delete(getShortEntitiesById(ids));
        } catch (Exception e) {
            throw new DaoException(113, e, simpleClassName);
        }
    }

    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            produitsRepository.delete(getShortEntitiesByName(names));
        } catch (Exception e) {
            throw new DaoException(118, e, simpleClassName);
        }
    }

}

Der Code entspricht dem der Klasse [DaoCategorie]:

  • Bei den Langversionen der Kategorien wurde bei Tests festgestellt, dass das Feld [idCategorie] der Produkte nicht ausgefüllt ist. Die Methode [linkLongProduitsToCategories] in den Zeilen 96–105 behebt dieses Problem;
  • die Methode [saveEntities] in den Zeilen 108–121 fügt neue Produkte ein oder ändert bestehende Produkte. Die Ebene JPA erfordert, dass jede Entität [Produit] mit einer Entität [Categorie] verknüpft ist. Da nicht bekannt ist, ob der Benutzer dies getan hat, führen wir dies in den Zeilen 110–113 selbst durch. Es reicht aus, die Entität [Produit] mit einer Entität [Categorie] zu verknüpfen, deren Primärschlüssel dem Feld [idCategorie] der Entität [Produit] entspricht. Bei Tests stellt man fest, dass ein Fehler auftritt, wenn man für die Kategorieversion „null“ angibt. Daher wird hier der Wert 0 gesetzt, man kann jedoch einen beliebigen Wert eingeben. Abgesehen vom Primärschlüssel ist kein Feld der Entität [Categorie] für die Ebene JPA erforderlich, um eine Entität [Produit] einzufügen oder zu ändern;

6.4.5. Die Testschicht

  

Die oben genannten Tests entsprechen denen der Spring-Implementierung JDBC. Bei Bedarf siehe die folgenden Seiten:

  • [JUnitTestCheckArguments]: Abschnitt 4.11.1;
  • [JUnitTestDao]: Abschnitt 4.11.2;
  • [JUnitTestPushTheLimits]: Abschnitt 4.11.3;

Wir verwenden die folgenden Ausführungskonfigurationen:

Die Ergebnisse der verschiedenen Tests lauten wie folgt:

In [1] dem Test [JUnitTestPushTheLimits] mit der Spring-Data-Implementierung JPA (Hibernate) und in [2] mit der Spring-Implementierung JDBC. Man sieht, dass letztere leistungsfähiger ist. Daraus lässt sich eine erste Schlussfolgerung ziehen: Es ist deutlich einfacher, eine Schicht mit Spring Data zu entwickeln, aber diese ist weniger leistungsfähig als eine Spring-Implementierung.

Der Test [JUnitTestProxies] ist ein Scheintest JUnit. Er dient dazu, das Verhalten jeder Implementierung JPA gegenüber Proxies, also den Kurzversionen der Entitäten, aufzuzeigen:


package spring.data.tests;

import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;

import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.data.config.AppConfig;
import spring.data.dao.IDao;

import com.google.common.collect.Lists;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {

    // Ebene [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

    @Before
    public void clean() {
        // Die Datenbank wird vor jedem Test bereinigt
        log("Vidage de la base de données", 1);
        // Die Tabelle [CATEGORIES] wird geleert und in einer Kaskade auch die Tabelle [PRODUITS]
        daoCategorie.deleteAllEntities();
    }

    @Test
    public void doNothing() {
        System.out.println("doNothing");
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // Die Tabellen werden gefüllt
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < nbCategories; i++) {
            Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
            categorie.setProduits(new ArrayList<Produit>());
            for (int j = 0; j < nbProduits; j++) {
                Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
                        100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
                categorie.addProduit(produit);
            }
            categories.add(categorie);
        }
        // Hinzufügen der Kategorie – in einer kaskadierenden Aktion werden auch die Produkte
        // eingefügt
        daoCategorie.saveEntities(categories);
        // Ergebnis
        return categories;
    }

    @Test
    public void getShortCategoriesByName1() {
        // Ausfüllen
        fill(1, 1);
        // Test
        log("getShortCategoriesByName1", 1);
        Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
        System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
        System.out.println("Catégorie :");
        try {
            System.out.println(categorie.getProduits().size());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }

    @Test
    public void getShortProduitsByName1() {
        // Ausfüllen
        fill(1, 1);
        // Test
        log("getShortProduitsByName1", 1);
        Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
        System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
        System.out.println("Nom de la catégorie du produit :");
        try {
            System.out.println(produit.getCategorie().getNom());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }

    @Test
    public void getLongCategoriesByName1() {
        // Befüllung
        fill(1, 1);
        // Test
        log("getLongCategoriesByName1", 1);
        Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
        System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
        System.out.println("Catégorie :");
        try {
            System.out.println(categorie.getProduits().size());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }

    @Test
    public void getLongProduitsByName1() {
        // Befüllung
        fill(1, 1);
        // Test
        log("getLongProduitsByName1", 1);
        Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
        System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
        System.out.println("Nom de la catégorie du produit :");
        try {
            System.out.println(produit.getCategorie().getNom());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }

    private void log(String message, int mode) {
        // Meldung anzeigen
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }

}

Die erzielten Ergebnisse lauten wie folgt:


Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]

Hier ist zu sehen, dass beim Zugriff auf das Feld [Categorie.produits] einer Kategorie vom Typ PROXY und auf das Feld [Produit.categorie] eines Produkts vom Typ PROXY in beiden Fällen (Zeilen 7 und 17) eine Ausnahme vom Typ [org.hibernate.LazyInitializationException] auftritt.