Skip to content

11. [Kurs]: Verwaltung relationaler Datenbanken mit Spring Data

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, JPA (Java Persistence API), Spring Data.

Wir werden die [DAO]-Schicht der Aufgabe mit [Spring Data], einer Komponente des Spring-Ökosystems, implementieren. [Spring Data] stützt sich auf eine JPA-Schicht (Java Persistence API), die es der [DAO]-Schicht ermöglicht, Objekte anstelle von SQL-Anweisungen zu manipulieren. Letztendlich ist der [DAO]-Schicht nicht bewusst, dass sie mit einer Datenbank interagiert. Sie kennt lediglich die Schnittstelle der [Spring Data]-Schicht.

Wir werden [Spring Data] zunächst anhand von zwei Beispielen näher betrachten.

11.1. Support

  • In [1] enthält der Ordner [support / chap-11] drei Eclipse-Projekte;
  • in [2] das SQL-Skript zum Erstellen der Beispieldatenbank für dieses Kapitel;

11.2. Beispiel 1

Die Spring-Website bietet zahlreiche Tutorials für den Einstieg in Spring [http://spring.io/guides]. Wir werden eines davon nutzen, um Spring Data vorzustellen. Dazu verwenden wir die Spring Tool Suite (STS).

  • In [1] importieren wir eines der Tutorials von [spring.io/guides];
  • In [2] wählen wir das Tutorial [Accessing Data Jpa] aus, das zeigt, wie man mit Spring Data auf eine Datenbank zugreift;
  • In [3] wählen wir ein von Maven konfiguriertes Projekt aus;
  • In [4] ist das Tutorial in zwei Formen verfügbar: [initial], eine leere Version, die Sie gemäß dem Tutorial ausfüllen, oder [complete], die endgültige Version des Tutorials. Wir wählen Letzteres;
  • In [5] können Sie das Tutorial in einem Browser anzeigen;
  • In [6] das fertige Projekt.

11.2.1. Die Maven-Konfiguration des Projekts

Die Maven-Abhängigkeiten des Projekts sind in der Datei [pom.xml] konfiguriert:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • Zeilen 5–9: Definieren Sie ein übergeordnetes Maven-Projekt. Dieses Projekt definiert die meisten Abhängigkeiten des Projekts. Diese können ausreichend sein, in diesem Fall werden keine zusätzlichen Abhängigkeiten hinzugefügt, oder sie können unzureichend sein, in diesem Fall werden die fehlenden Abhängigkeiten hinzugefügt;
  • Zeilen 12–15: Definieren eine Abhängigkeit von [spring-boot-starter-data-jpa]. Dieses Artefakt enthält die Spring-Data-Klassen;
  • Zeilen 16–19: Definieren eine Abhängigkeit vom H2-DBMS, mit dem Sie In-Memory-Datenbanken erstellen und verwalten können.

Sehen wir uns die von diesen Abhängigkeiten bereitgestellten Klassen an:

Es gibt viele davon:

  • Einige gehören zum Spring-Ökosystem (diejenigen, die mit „spring“ beginnen);
  • andere gehören zum Hibernate-Ökosystem (hibernate, jboss), dessen JPA-Implementierung wir hier verwenden;
  • wieder andere sind Testbibliotheken (junit, hamcrest);
  • wieder andere sind Logging-Bibliotheken (log4j, logback, slf4j);

Wir werden sie alle behalten. Für eine Produktionsanwendung sollten nur die notwendigen beibehalten werden.

In Zeile 26 der Datei [pom.xml] finden wir die Zeile:


<start-class>hello.Application</start-class>

Diese Zeile ist mit den folgenden Zeilen verknüpft:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Zeilen 6–9: Mit dem [spring-boot-maven-plugin] können Sie die ausführbare JAR-Datei der Anwendung generieren. In Zeile 26 der Datei [pom.xml] wird dann die ausführbare Klasse dieser JAR-Datei angegeben.

11.2.2. Die [JPA]-Schicht

Der Datenbankzugriff wird über eine [JPA]-Schicht, die Java Persistence API, abgewickelt:

  

Die Anwendung ist einfach gehalten und verwaltet [Customer]-Entitäten. Die [Customer]-Klasse ist Teil der [JPA]-Schicht und sieht wie folgt aus:


package hello;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class Customer {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;
 
    protected Customer() {
    }
 
    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }
 
}

Ein Kunde hat eine ID [id], einen Vornamen [firstName] und einen Nachnamen [lastName]. Jede [Customer]-Instanz repräsentiert eine Zeile in einer Datenbanktabelle.

  • Zeile 8: JPA-Annotation, die sicherstellt, dass die Persistenz von [Customer]-Instanzen (Erstellen, Lesen, Aktualisieren, Löschen) von einer JPA-Implementierung verwaltet wird. Anhand der Maven-Abhängigkeiten lässt sich erkennen, dass die JPA/Hibernate-Implementierung verwendet wird;
  • Zeilen 11–12: JPA-Annotationen, die das Feld [id] mit dem Primärschlüssel der Tabelle [Customer] verknüpfen. Zeile 12 gibt an, dass die JPA-Implementierung die für das verwendete DBMS spezifische Methode zur Primärschlüsselgenerierung verwendet, in diesem Fall H2;

Es gibt keine weiteren JPA-Annotationen. Daher werden Standardwerte verwendet:

  • Die Tabelle [Customer] wird nach der Klasse benannt, d. h. [Customer];
  • die Spalten dieser Tabelle werden nach den Klassenfeldern benannt: [id, firstName, lastName], wobei zu beachten ist, dass bei den Spaltennamen der Groß-/Kleinschreibung keine Bedeutung zukommt;

Beachten Sie, dass die verwendete JPA-Implementierung niemals benannt wird.

11.2.3. Die [Spring Data]-Schicht

Die Klasse [CustomerRepository] implementiert die Zugriffsebene für die Tabelle [Customer]. Ihr Code lautet wie folgt:

  

package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}

Es handelt sich also um eine Schnittstelle und nicht um eine Klasse (Zeile 7). Sie erweitert die Schnittstelle [CrudRepository], eine Spring-Data-Schnittstelle (Zeile 5). Diese Schnittstelle wird durch zwei Typen parametrisiert: Der erste ist der Typ der verwalteten Elemente, hier der Typ [Customer]; der zweite ist der Typ des Primärschlüssels der verwalteten Elemente, hier ein Typ [Long]. Die Schnittstelle [CrudRepository] sieht wie folgt aus:


package org.springframework.data.repository;
 
import java.io.Serializable;
 
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
 
    <S extends T> S save(S entity);
 
    <S extends T> Iterable<S> save(Iterable<S> entities);
 
    T findOne(ID id);
 
    boolean exists(ID id);
 
    Iterable<T> findAll();
 
    Iterable<T> findAll(Iterable<ID> ids);
 
    long count();
 
    void delete(ID id);
 
    void delete(T entity);
 
    void delete(Iterable<? extends T> entities);
 
    void deleteAll();
}

Diese Schnittstelle definiert die CRUD-Operationen (Create – Read – Update – Delete), die für einen JPA-Typ vom Typ T ausgeführt werden können:

  • Zeile 8: Die Methode „save“ ermöglicht es, eine T-Entität in der Datenbank zu speichern. Sie speichert die Entität unter Verwendung des Primärschlüssels, der ihr vom DBMS zugewiesen wurde. Sie ermöglicht es auch, eine T-Entität, die durch ihre Primärschlüssel-ID identifiziert wird, zu aktualisieren. Die Wahl zwischen diesen beiden Aktionen hängt vom Wert der Primärschlüssel-ID ab: Ist dieser null, erfolgt die Speichervorgang; andernfalls erfolgt die Aktualisierung;
  • Zeile 10: wie oben, jedoch für eine Liste von Entitäten;
  • Zeile 12: Die Methode `findOne` ruft eine Entität T ab, die durch ihre Primärschlüssel-ID identifiziert wird;
  • Zeile 22: Mit der Methode delete können Sie eine Entität T löschen, die durch ihre Primärschlüssel-ID identifiziert wird;
  • Zeilen 24–28: Varianten der Methode [delete];
  • Zeile 16: Die Methode [findAll] ruft alle persistierten T-Entitäten ab;
  • Zeile 18: wie oben, jedoch beschränkt auf Entitäten, für die eine Liste von Identifikatoren angegeben wurde;

Kehren wir zur Schnittstelle [CustomerRepository] zurück:


package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}
  • Zeile 9 ermöglicht es Ihnen, einen [Kunden] anhand seines [Nachnamens] abzurufen;

Und das war’s auch schon für die [DAO]-Schicht. Es gibt keine Implementierungsklasse für die vorherige Schnittstelle. Sie wird zur Laufzeit von [Spring Data] generiert. Die Methoden der [CrudRepository]-Schnittstelle werden automatisch implementiert. Bei den Methoden, die der [CustomerRepository]-Schnittstelle hinzugefügt wurden, kommt es darauf an. Kehren wir zur Definition von [Customer] zurück:


private long id;
private String firstName;
private String lastName;

Die Methode in Zeile 9 wird von [Spring Data] automatisch implementiert, da sie auf das Feld [lastName] (Zeile 3) von [Customer] verweist. Wenn Spring Data in der zu implementierenden Schnittstelle auf eine [findBySomething]-Methode stößt, implementiert es diese mithilfe der folgenden JPQL-Abfrage (Java Persistence Query Language):

select t from T t where t.something=:value

Daher muss der Typ T ein Feld mit dem Namen [something] haben. Somit muss die Methode

List<Customer> findByLastName(String lastName);

wird mit einem Code implementiert, der in etwa wie folgt aussieht:

return [em].createQuery("select c from Customer c where  c.lastName=:value").setParameter("value",lastName).getResultList()

wobei [em] sich auf den JPA-Persistenzkontext bezieht. Dies ist nur möglich, wenn die Klasse [Customer] ein Feld namens [lastName] enthält, was der Fall ist.

Zusammenfassend lässt sich sagen, dass Spring Data es uns in einfachen Fällen ermöglicht, die [DAO]-Schicht mit einer einfachen Schnittstelle zu implementieren.

11.2.4. Die [console]-Schicht

  

Die Klasse [Application] sieht wie folgt aus:


package hello;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Application implements CommandLineRunner {
 
    @Autowired
    CustomerRepository repository;
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
 
    @Override
    public void run(String... strings) throws Exception {
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        // fetch all customers
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by last name
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }
 
}
  • Zeile 9: Die Klasse implementiert die Schnittstelle [CommandLineRunner], bei der es sich um eine [Spring Boot]-Schnittstelle handelt (Zeile 4). Diese Schnittstelle hat nur eine Methode, nämlich die in Zeile 19;
  • Zeile 8: @SpringBootApplication ist eine Annotation, die mehrere [Spring Boot]-Annotationen zusammenfasst:
    • @Configuration: gibt an, dass es sich bei der Klasse um eine Konfigurationsklasse handelt;
    • @EnableAutoConfiguration: weist [Spring Boot] an, automatisch eine Reihe von Beans basierend auf verschiedenen Eigenschaften zu erstellen, insbesondere auf den Inhalten des Klassenpfads des Projekts. Da sich die Hibernate-Bibliotheken im Klassenpfad befinden, wird die [entityManagerFactory]-Bean unter Verwendung von Hibernate implementiert. Da sich die H2-DBMS-Bibliothek im Klassenpfad befindet, wird die [dataSource]-Bean unter Verwendung von H2 implementiert. In der [dataSource]-Bean müssen wir außerdem den Benutzernamen und das Passwort definieren. Hier verwendet Spring Boot den Standard-H2-Administrator, der kein Passwort hat. Da sich die [spring-tx]-Bibliothek im Klassenpfad befindet, wird der Transaktionsmanager von Spring verwendet;
    • @EnableWebMvc: wenn sich die [spring-mvc]-Bibliothek im Classpath befindet. In diesem Fall wird eine automatische Konfiguration für die Webanwendung durchgeführt;
    • @ComponentScan: Teilt Spring mit, wo nach anderen Beans, Konfigurationen und Diensten gesucht werden soll. Hier wird standardmäßig in dem Paket gesucht, das die annotierte Klasse enthält, d. h. im [hello]-Paket. Somit werden die Klassen [Customer] und [CustomerRepository] gefunden. Da die erste die Annotation [@Entity] trägt, wird sie als Entität katalogisiert, die von Hibernate verwaltet wird. Da die zweite die Schnittstelle [CrudRepository] erweitert, wird sie als Spring-Bean registriert;
  • Zeilen 11–12: Die [CustomerRepository]-Bean wird in den Code der Hauptklasse injiziert;
  • Zeile 15: Die statische Methode [run] der Klasse [SpringApplication] aus dem Spring-Boot-Projekt wird ausgeführt. Ihr Parameter ist die Klasse, die eine Annotation [Configuration] oder [EnableAutoConfiguration] trägt. Alles zuvor Erklärte findet dann statt. Das Ergebnis ist ein Spring-Anwendungskontext, d. h. eine Reihe von Beans, die von Spring verwaltet werden;

Die folgenden Operationen nutzen lediglich die Methoden der Bean, die die Schnittstelle [CustomerRepository] implementiert. Die Konsolenausgabe sieht wie folgt aus:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-10 15:35:43.661  INFO 5784 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 5784 (started by ST in C:\Users\Serge Tahé\Documents\workspace-sts-3.6.3.RELEASE\gs-accessing-data-jpa-complete)
2015-03-10 15:35:43.708  INFO 5784 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:45.230  INFO 5784 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:45.254  INFO 5784 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-10 15:35:45.331  INFO 5784 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-10 15:35:45.332  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-10 15:35:45.334  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-10 15:35:45.651  INFO 5784 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-10 15:35:45.754  INFO 5784 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2015-03-10 15:35:45.877  INFO 5784 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-10 15:35:46.154  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:46.169  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2015-03-10 15:35:46.779  INFO 5784 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2015-03-10 15:35:47.040  INFO 5784 --- [           main] hello.Application                        : Started Application in 3.623 seconds (JVM running for 4.324)
2015-03-10 15:35:47.042  INFO 5784 --- [       Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:47.044  INFO 5784 --- [       Thread-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2015-03-10 15:35:47.046  INFO 5784 --- [       Thread-1] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:47.047  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:47.051  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • Zeilen 1–8: das Spring Boot-Projektlogo;
  • Zeile 9: Die Klasse [hello.Application] wird ausgeführt;
  • Zeile 10: [AnnotationConfigApplicationContext] ist eine Klasse, die die [ApplicationContext]-Schnittstelle von Spring implementiert. Es handelt sich um einen Bean-Container;
  • Zeile 11: Die Bean [entityManagerFactory] wird mithilfe der Klasse [LocalContainerEntityManagerFactory], einer Spring-Klasse, implementiert;
  • Zeile 12: [Hibernate] erscheint. Dies ist die gewählte JPA-Implementierung;
  • Zeile 19: Ein Hibernate-Dialekt ist die SQL-Variante, die mit dem DBMS verwendet werden soll. Hier gibt der Dialekt [H2Dialect] an, dass Hibernate mit dem H2-DBMS arbeiten wird;
  • Zeilen 21–22: Die Datenbank wird angelegt. Die Tabelle [CUSTOMER] wird angelegt. Das bedeutet, dass Hibernate so konfiguriert wurde, dass es Tabellen aus JPA-Definitionen generiert, in diesem Fall aus der JPA-Definition der Klasse [Customer];
  • Zeilen 26–30: Ergebnis der Methode [findAll] der Schnittstelle;
  • Zeile 34: Ergebnis der Methode [findOne] der Schnittstelle;
  • Zeilen 38–39: Ergebnisse der Methode [findByLastName];
  • Zeilen 41 ff.: Protokolle aus dem Spring-Kontext-Closure.

11.2.5. Manuelle Konfiguration des Spring Data-Projekts

Wir duplizieren das vorherige Projekt in das Projekt [gs-accessing-data-jpa-02]:

  

In diesem neuen Projekt werden wir uns nicht auf die von Spring Boot bereitgestellte automatische Konfiguration verlassen. Wir werden es manuell konfigurieren. Dies kann nützlich sein, wenn die Standardkonfigurationen nicht unseren Anforderungen entsprechen.

Zunächst legen wir die erforderlichen Abhängigkeiten in der Datei [pom.xml] fest:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa-02</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>
 
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
 
</project>
  • Zeilen 10–14: das übergeordnete Maven-Projekt, dessen Bibliotheken wir verwenden werden;
  • Zeilen 18–21: Spring Data für den Zugriff auf die Datenbank;
  • Zeilen 23–26: die Hibernate-Implementierung der JPA-Spezifikation;
  • Zeilen 28–31: das H2-DBMS;
  • Zeilen 33–36: Datenbanken werden oft mit Verbindungspools verwendet, wodurch das wiederholte Öffnen und Schließen von Verbindungen vermieden wird. Hier wird die Implementierung [tomcat-jdbc] verwendet;

Im neuen Projekt bleiben die Entität [Customer] und die Schnittstelle [CustomerRepository] unverändert. Wir werden die Klasse [Application] ändern, die in zwei Klassen aufgeteilt wird:

  • [Config], die als Konfigurationsklasse dient;
  • [Main], die als ausführbare Klasse dient;
  

Die ausführbare Klasse [Application] sieht nun wie folgt aus:


package console;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;
 
public class Application {
    public static void main(String[] args) {
        // instantiation Spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
 
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        ...
 
        // closing context
        context.close();
    }
}
  • Zeile 9: Die Klasse [Application] enthält keine Konfigurationsanmerkungen mehr;
  • Zeilen 3–7: Beachten Sie, dass keine [Spring Boot]-Paketimporte mehr vorhanden sind;
  • Zeile 12: Wir instanziieren die Spring-Beans. Wir erhalten den Spring-Kontext, der Verweise auf die erstellten Beans enthält;
  • Zeile 13: Wir fordern eine Referenz auf die [CustomerRepository]-Bean an;

Die Klasse [ Config], die das Projekt konfiguriert, sieht wie folgt aus:


package config;
 
import javax.persistence.EntityManagerFactory;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
//@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {
 
    // h2 database
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // an initially open connection
        dataSource.setInitialSize(1);
        // result
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • Zeile 17: Die Annotation [@EnableTransactionManagement] gibt an, dass die Methoden der [CrudRepository]-Schnittstellen innerhalb einer Transaktion ausgeführt werden müssen. Sie wurde auskommentiert, da dies das Standardverhalten ist;
  • Zeile 18: Die Annotation [@EnableJpaRepositories] gibt die Verzeichnisse an, in denen sich die Spring Data [CrudRepository]-Schnittstellen befinden. Diese Schnittstellen werden zu Spring-Komponenten und stehen im Spring-Kontext zur Verfügung;
  • Zeile 19: Die Annotation [@Configuration] macht die Klasse [Config] zu einer Spring-Konfigurationsklasse;
  • Zeile 20: Die Annotation [@ComponentScan] listet die Verzeichnisse auf, in denen nach Spring-Komponenten gesucht werden soll. Spring-Komponenten sind Klassen, die mit Spring-Annotationen wie @Service, @Component, @Controller usw. versehen sind. Hier gibt es außer den in der Klasse [AppConfig] definierten keine weiteren, daher wurde die Annotation auskommentiert;
  • Zeilen 24–37: Definieren die Datenquelle, die H2-Datenbank. Es ist die Annotation @Bean in Zeile 25, die das von dieser Methode erstellte Objekt zu einer von Spring verwalteten Komponente macht. Der Methodenname kann hier beliebig gewählt werden. Er muss jedoch [dataSource] lauten, wenn die EntityManagerFactory in Zeile 51 fehlt und über die automatische Konfiguration definiert wird;
  • Zeile 30: Die Datenbank erhält den Namen [demo] und wird im Projektordner erstellt;
  • Zeilen 40–47: Definieren Sie die verwendete JPA-Implementierung, in diesem Fall eine Hibernate-Implementierung. Der Methodenname kann hier beliebig gewählt werden;
  • Zeile 43: keine SQL-Protokolle;
  • Zeile 44: Die Datenbank wird erstellt, falls sie noch nicht existiert;
  • Zeilen 50–58: Definieren die EntityManagerFactory, die die JPA-Persistenz verwaltet. Die Methode muss den Namen [entityManagerFactory] tragen;
  • Zeile 51: Die Methode erhält zwei Parameter der Typen der beiden zuvor definierten Beans. Diese werden dann von Spring als Methodenparameter konstruiert und injiziert;
  • Zeile 53: Legt die zu verwendende JPA-Implementierung fest;
  • Zeile 54: gibt die Verzeichnisse an, in denen die JPA-Entitäten zu finden sind;
  • Zeile 55: Legt die zu verwaltende Datenquelle fest;
  • Zeilen 61–66: der Transaktionsmanager. Die Methode muss den Namen [transactionManager] tragen. Sie erhält die Bean aus den Zeilen 51–58 als Parameter;
  • Zeile 64: Der Transaktionsmanager wird mit der EntityManagerFactory verknüpft;

Die vorangehenden Methoden können in beliebiger Reihenfolge definiert werden.

Die Ausführung des Projekts liefert die gleichen Ergebnisse. Im Projektordner erscheint eine neue Datei, die H2-Datenbankdatei:

  

11.2.6. Erstellen eines ausführbaren Archivs

Um ein ausführbares Archiv des Projekts zu erstellen, gehen Sie wie folgt vor:

  • in [1]: Erstellen Sie eine Laufzeitkonfiguration;
  • in [2]: vom Typ [Java-Anwendung]
  • in [3]: Geben Sie das auszuführende Projekt an (verwenden Sie die Schaltfläche „Durchsuchen“);
  • in [4]: Geben Sie die auszuführende Klasse an;
  • in [5]: den Namen der Ausführungskonfiguration – kann beliebig sein;
  • in [6]: das Projekt exportieren;
  • in [7]: als ausführbares JAR-Archiv;
  • in [8]: Geben Sie den Pfad und den Namen der zu erstellenden ausführbaren Datei an;
  • in [9]: den Namen der in [5] erstellten Laufzeitkonfiguration;
  • in [10], das erstellte Archiv;

Sobald dies erledigt ist, öffnen Sie eine Konsole in dem Ordner, der das ausführbare Archiv enthält:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-02.jar

Das Archiv wird wie folgt ausgeführt:


.....\dist>java -jar gs-accessing-data-jpa-02.jar

Die in der Konsole angezeigten Ergebnisse lauten wie folgt:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

11.3. Beispiel 2

11.3.1. Einführung

Wir greifen das Beispiel der Produkttabelle wieder auf, das wir zur Einführung der JDBC-API verwendet haben, und erstellen die folgende Architektur:

Die Datenbank [dbintrospringjpa] enthält zwei Tabellen: [PRODUCTS] und [CATEGORIES]. Die Tabelle [CATEGORIES] sieht wie folgt aus:

 
  • [ID]: Primärschlüssel im AUTO_INCREMENT-Modus;
  • [VERSION]: Versionsnummer des Datensatzes;
  • [NAME]: Name der Kategorie – eindeutig;

Die Tabelle [PRODUCTS] sieht wie folgt aus:

 
  • [ID]: Primärschlüssel im AUTO_INCREMENT-Modus;
  • [VERSION]: Versionsnummer des Datensatzes;
  • [NAME]: Produktname – eindeutig;
  • [CATEGORY_ID]: Kategorie-ID – Fremdschlüssel auf das Feld [CATEGORIES.ID];
  • [PRICE]: der Preis;
  • [DESCRIPTION]: eine Beschreibung des Produkts;

Aufgabe: Erstellen Sie die Datenbank [dbintrospringdata] mithilfe des SQL-Skripts [dbintrospringdata.sql] aus den Support-Materialien:


11.3.2. Erstellen des Maven-Projekts

Führen Sie die folgenden Schritte aus, um eine Spring Data-Projektvorlage zu erstellen:

  • Erstellen Sie unter [1] ein neues Projekt;
  • Wählen Sie in [2] den Typ [Spring Starter Project] aus;
  • Das generierte Projekt ist ein Maven-Projekt. Geben Sie in [3] den Namen der Projektgruppe an;
  • Geben Sie in [4] den Namen des Artefakts (in diesem Fall eine JAR-Datei) an, das beim Erstellen des Projekts generiert wird;
  • in [5]: der Eclipse-Projektname – kann beliebig sein (muss nicht mit [4] übereinstimmen);
  • in [7]: Geben Sie an, dass Sie ein Projekt mit einer [JPA]-Schicht unter Verwendung des MySQL-DBMS erstellen. Die für ein solches Projekt erforderlichen Abhängigkeiten werden dann in die Datei [pom.xml] aufgenommen;
  • in [8] den Namen des Projektordners eingeben;
  • Beenden Sie in [9] den Assistenten;
  • in [10]: das erstellte Projekt;

Die Datei [pom.xml] enthält die für ein JPA-Projekt erforderlichen Abhängigkeiten:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
  • Zeilen 14–19: Das übergeordnete Maven-Projekt – definiert eine große Anzahl von Bibliotheken mit ihren Versionen – wir verwenden diese Bibliotheken als Maven-Abhängigkeiten, ohne ihre Versionen anzugeben;
  • Zeilen 28–31: Die für JPA erforderliche Abhängigkeit – beinhaltet [Spring Data];
  • Zeilen 32–36: die Abhängigkeit vom MySQL-JDBC-Treiber;
  • Zeilen 37–41: die für JUnit-Tests erforderlichen Abhängigkeiten, die in Spring integriert sind;

Die ausführbare Klasse [Application] führt keine Aktionen aus, ist jedoch vorkonfiguriert:


package demo;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class IntroSpringData01Application {
 
    public static void main(String[] args) {
        SpringApplication.run(IntroSpringData01Application.class, args);
    }
}
  • Die Annotation [@SpringBootApplication] macht die Klasse zu einer Klasse für die automatische Projektkonfiguration;

Die Testklasse [ApplicationTests] führt keine Aktionen aus, ist jedoch vorkonfiguriert:


package demo;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {
 
    @Test
    public void contextLoads() {
    }
 
}
  • Zeile 9: Die Annotation [@SpringApplicationConfiguration] ermöglicht die Verwendung der Konfigurationsdatei [Application]. Die Testklasse profitiert somit von allen in dieser Datei definierten Beans;
  • Zeile 8: Die Annotation [@RunWith] ermöglicht die Integration von Spring mit JUnit: Die Klasse kann als JUnit-Test ausgeführt werden. [@RunWith] ist eine JUnit-Annotation (Zeile 4), während die Klasse [SpringJUnit4ClassRunner] eine Spring-Klasse ist (Zeile 6);

Da wir nun über ein JPA-Anwendungsgerüst verfügen, können wir es vervollständigen, um das Persistenzschicht-Projekt für die Produktdatenbank zu erstellen.

11.3.3. Das Eclipse-Projekt

Wir werden das vorherige Projekt wie folgt erweitern:

  
  • [AppConfig.java]: die Spring-Projektkonfigurationsklasse;
  • [Main.java]: die ausführbare Klasse des Projekts;
  • [IDao.java]: die Schnittstelle der [DAO]-Schicht;
  • [Dao.java]: die Implementierungsklasse der [DAO]-Schicht;
  • [AbstractEntity.java]: die Oberklasse der Klassen [Product] und [Category];
  • [Product.java]: Klasse, die einer Zeile in der Tabelle [PRODUCTS] in der Datenbank zugeordnet ist;
  • [Category.java]: Klasse, die einer Zeile in der Tabelle [CATEGORIES] in der Datenbank zugeordnet ist;
  • [ProductsRepository]: die Spring-Data-Schnittstelle für den Zugriff auf die Tabelle [PRODUCTS];
  • [CategoriesRepository]: die Spring-Data-Schnittstelle für den Zugriff auf die Tabelle [CATEGORIES];
  • [pom.xml]: die Maven-Projektkonfigurationsdatei;

Dieses Projekt implementiert die folgende Architektur:

Die [DAO]-Schicht sieht nur die von [Spring Data] implementierte Schicht.

11.3.4. Maven-Konfiguration

Die [pom.xml]-Datei für das Maven-Projekt lautet wie folgt:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- MySQL Database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- log library -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</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>

Diese Konfiguration ist diejenige, die in Abschnitt 11.2.5 verwendet und erläutert wird. Wir fügen die folgenden Bibliotheken hinzu:

  • Zeilen 42–49: eine JSON-Bibliothek, die von der Methode [toString] der Klasse [Product] verwendet wird;
  • Zeilen 51–55: die [Google Guava]-Bibliothek, die Hilfsmethoden zur Verwaltung von Elementesammlungen bereitstellt. Sie wird von der [Dao]-Klasse verwendet, die die [DAO]-Schicht implementiert;
  • Zeilen 56–67: die für JUnit-Tests erforderlichen Bibliotheken;
  • Zeilen 69–72: eine Logging-Bibliothek;
  • Zeilen 81–86: die für das Projekt erforderlichen Maven-Plugins;

11.3.5. Entitäten der [JPA]-Schicht

[DAO]-Schicht[Console]-Schicht[JPA]-Schicht[JDBC]-Treiber[Spring Data]-SchichtSpring 4DBMS

  

11.3.5.1. Die Klasse [AbstractEntity]

Die Klasse [AbstractEntity] sieht wie folgt aus:


package spring.data.entities;
 
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
@MappedSuperclass
public abstract class AbstractEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }
 
    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id.longValue() == other.id.longValue();
    }
 
    // signature jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    // getters and setters
....
}
 

Der Zweck dieser Klasse besteht darin, eine Oberklasse für JPA-Entitäten bereitzustellen, indem die Eigenschaften [id, version] (Zeilen 19, 22), die sowohl der Entität [Product] als auch der Entität [Category] gemeinsam sind und mit der Datenbank verknüpft sind, an einer einzigen Stelle gekapselt werden. Diese Eigenschaften sind mit den Spalten [ID, VERSION] der Tabellen verknüpft (Zeilen 18, 21).

  • Zeile 13: Die Annotation [@MappedSuperclass] gibt an, dass die Klasse eine übergeordnete Klasse von JPA-Entitäten ist;
  • Zeile 16: Die Annotation [@Id] gibt an, dass das Feld [id] (es könnte auch einen anderen Namen haben) mit dem Primärschlüssel einer Tabelle verknüpft ist;
  • Zeile 17: Die Annotation [@GeneratedValue(strategy=GenerationType.IDENTITY)] legt den Modus für die Primärschlüsselgenerierung fest. Der Modus [GenerationType.IDENTITY] verwendet bei MySQL den Modus [AUTO_INCREMENT]. Bei einem anderen DBMS würde dieser Modus eine andere Methode verwenden. Der Vorteil besteht darin, dass sich der Entwickler darum nicht kümmern muss und sein Code unabhängig vom verwendeten DBMS gültig bleibt;
  • Zeile 18: Die Annotation [@Column] gibt die Spalte an, die dem Feld zugeordnet ist. Fehlt diese Annotation, geht JPA davon aus, dass die Spalte denselben Namen wie das Feld hat. Dies ist hier der Fall. Daher hätten wir diese Annotation weglassen können;
  • Zeile 20: Die Annotation [@Version] gibt an, dass das Feld [version] mit einer Versionsspalte verknüpft ist. Die JPA-Implementierung erhöht diese Versionsnummer bei jeder Änderung der Entität. Diese Nummer 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 daraufhin zu V1+1. U2 ändert seinerseits E und speichert diese Änderung in der Datenbank: Er erhält eine Ausnahme, da seine Version (V1) von der in der Datenbank (V1+1) abweicht;
  • Zeilen 35–52: Neudefinition der Methoden [hashCode] und [equals]. Standardmäßig gibt [obj1.equals(obj2)] „true“ zurück, wenn [obj1 == obj2] gilt, d. h. wenn obj1 und obj2 zwei gleiche Zeiger sind. Wenn wir die Objekte vergleichen wollen, auf die verwiesen wird, anstatt die Zeiger selbst, müssen wir die Methoden [equals] und [hashCode] überschreiben. Letztere muss für zwei Objekte, die die Methode [equals] als gleichwertig einstuft, denselben Wert zurückgeben;
  • Zeilen 42–51: Zwei Objekte vom Typ [AbstractEntity] oder abgeleiteten Typen gelten als gleich, wenn ihre Primärschlüssel [id] gleich sind;
  • Zeilen 35–38: Die [hashCode]-Methode gibt tatsächlich denselben Wert für zwei identische [AbstractEntity]-Objekte zurück, die daher denselben Primärschlüssel [id] haben;
  • Zeilen 55–63: Die Methode [toString] gibt die JSON-Zeichenkette des Objekts [this] zurück. Wenn sich dieses Objekt auf eine untergeordnete Klasse bezieht, gibt diese Methode die JSON-Zeichenkette der untergeordneten Klasse zurück. Dadurch entfällt die Notwendigkeit, in den untergeordneten Klassen eine [toString]-Methode zu erstellen;

11.3.5.2. Die JPA-Entität [Product]

Die Klasse [Product] ist eine JPA-Entität, die einer Zeile in der Tabelle [PRODUCTS] zugeordnet ist:

 

package spring.data.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
import com.fasterxml.jackson.annotation.JsonFilter;
 
@Entity
@Table(name = "PRODUITS")
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
 
    // properties
    @Column(name = "NOM")
    private String nom;
 
    @Column(name = "CATEGORIE_ID", insertable = false, updatable = false)    
    private Long idCategorie;
 
    @Column(name = "PRIX")
    private double prix;
 
    @Column(name = "DESCRIPTION")
    private String description;
 
    // the category
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
...
}
  • Zeile 12: Die Annotation [@Entity] macht die Klasse [Product] zu einer Entität, die von der [JPA]-Schicht verwaltet wird;
  • Zeile 13: Die Annotation [@Table(name = "PRODUCTS")] gibt an, dass die Klasse [Product] eine Zeile in der Tabelle [PRODUCTS] in der Datenbank darstellt;
  • Zeile 14: Der Name des JSON-Filters, der auf die Entität angewendet werden soll. Wir werden sehen, dass die Eigenschaft [categorie] in Zeile 13 nicht immer verfügbar ist. Sie muss daher aus der JSON-Darstellung des Objekts ausgeschlossen werden. Dazu benötigen wir einen Filter. Wir legen daher in einem Filter namens [jsonFilterCategorie] fest, ob wir die Eigenschaft [categorie] einbeziehen wollen oder nicht;
  • Zeile 18: Die Annotation [@Column] verknüpft das Feld [nom] mit der Spalte [NOM] in der Tabelle [PRODUITS]. Wenn das Feld denselben Namen wie die zugehörige Spalte hat, kann die Annotation [@Column] weggelassen werden. Das wäre hier der Fall;
  • Zeilen 31–33: die Produktkategorie;
  • Zeile 31: Die Annotation [@ManyToOne] gibt an, dass die Spalte, auf die die Annotation in Zeile 32 [@JoinColumn(name = "CATEGORIE_ID")] verweist, ein Fremdschlüssel von der Tabelle [PRODUCTS] der Entität [Product] zur Tabelle [CATEGORIES] ist, die der Entität in Zeile 33 zugeordnet ist. Diese Annotation muss auf eine JPA-Entität angewendet werden. Daher muss die Klasse in Zeile 33 eine JPA-Entität sein;
  • Zeile 31: Die Annotation [fetch = FetchType.LAZY] legt fest, dass beim Abrufen eines Produkts aus der Tabelle [PRODUCTS] dessen Kategorie (Zeile 33) nicht sofort abgerufen wird (Lazy Loading). Sie wird dann beim ersten Aufruf der Methode [getCategory] abgerufen. Dieses Attribut ist nicht obligatorisch. Die verwendete JPA-Implementierung darf es ignorieren. Da die Eigenschaft [category] vorhanden sein kann oder auch nicht, haben wir in Zeile 14 den JSON-Filter eingeführt. Bestehende JPA-Implementierungen (Hibernate, Eclipselink, OpenJPA) behandeln diese Annotation nicht auf die gleiche Weise. Hibernate erweitert die ursprüngliche [getCategory]-Methode (die lediglich das Feld „category“ zurückgibt), indem es einen Aufruf an das DBMS sendet, um die Kategorie abzurufen. Damit dies funktioniert, muss die DBMS-Verbindung, die ursprünglich zum Abrufen des Produkts verwendet wurde, noch offen sein; andernfalls tritt eine Ausnahme auf.

11.3.5.3. Die JPA-Entität [Category]

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

 

Der Code lautet wie folgt:


package spring.data.entities;
 
import java.util.HashSet;
import java.util.Set;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;
 
import com.fasterxml.jackson.annotation.JsonFilter;
 
@Entity
@Table(name = "CATEGORIES")
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
 
    // properties
    @Column(name = "NOM")
    private String nom;
 
    // related products
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(String nom) {
        this.nom = nom;
    }
 
    // methods
    public void addProduit(Produit produit) {
        // we add the product
        produits.add(produit);
        // set your category
        produit.setCategorie(this);
    }
 
    // getters and setters
...
}
  • Zeilen 21–22: der Name der Kategorie;
  • Zeilen 25–26: die Produkte in dieser Kategorie;
  • Zeile 25: Die Annotation [@OneToMany] ist die inverse Beziehung der [@ManyToOne]-Beziehung, die wir in der Entität [Product] gesehen haben. Das Attribut [mappedBy = "category"] gibt das Feld in der Entität [ ] an, das durch die inverse [@ManyToOne]-Beziehung annotiert ist. Das Attribut [cascade = { CascadeType.ALL }] legt fest, dass Operationen (persist, merge, remove), die an einer @Entity [Category] durchgeführt werden, auf die [products] in Zeile 26 kaskadieren sollen. Teilkaskaden können mithilfe der Konstanten [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] festgelegt werden;
  • Zeile 25: Das Attribut [fetch = FetchType.LAZY] legt fest, dass beim Abrufen einer Kategorie aus der Tabelle [CATEGORIES] deren Produkte nicht sofort abgerufen werden. Sie werden beim ersten Aufruf der Methode [getProduits] abgerufen. Bestehende JPA-Implementierungen (Hibernate, Eclipselink, OpenJPA) behandeln diese Annotation nicht auf die gleiche Weise. Hibernate erweitert die ursprüngliche Methode [getProduits] (die lediglich das Feld „products“ zurückgibt), indem es einen Aufruf an das DBMS sendet, um die Produkte für die Kategorie abzurufen. Damit dies möglich ist, muss die Verbindung zum DBMS, die ursprünglich zum Abrufen der Kategorie verwendet wurde, noch offen sein. Dieses Attribut ist obligatorisch. Die JPA-Implementierung kann es nicht ignorieren. Da die Eigenschaft [products] initialisiert sein kann oder auch nicht, haben wir in Zeile 17 den JSON-Filter eingeführt, mit dem wir angeben können, ob wir diese Eigenschaft wollen oder nicht;
  • Zeile 26: Der Typ [Set] ist eine Schnittstelle. Der Typ [HashSet] ist eine Klasse, die diese Schnittstelle implementiert. Sie implementiert eine Sammlung von Elementen, die als Set bezeichnet wird. Ein Set darf keine zwei identischen Objekte enthalten. Hier sind die Objekte vom Typ [Product]. Daher können wir innerhalb des Sets keine zwei identischen Objekte haben. Da die Methode [equals] der übergeordneten Klasse [AbstractEntity] überschrieben wurde, um festzulegen, dass zwei Produkte identisch sind, wenn sie denselben Primärschlüssel haben, darf das Feld [products] keine zwei Produkte mit demselben Primärschlüssel enthalten;
  • Zeilen 38–43: Die Methode [addProduct] ermöglicht es, ein Produkt zur Kategorie hinzuzufügen;

11.3.6. Die [Spring Data]-Schicht

[DAO]-Schicht[Console]-Schicht[JPA]-Schicht[JDBC]-Treiber[Spring Data]-SchichtSpring 4DBMS

  

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


package spring.data.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import spring.data.entities.Categorie;
 
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
 
    // categorie avec ses produits
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);
 
    @Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
    public Categorie getCategorieByNameWithProduits(String nom);
 
    // une catégorie sans ses produits désignée par son nom
    public Categorie findByNom(String nom);
}
  • Zeile 8: Die Schnittstelle [CrudRepository] wurde in Abschnitt 11.2.3 verwendet und erläutert. Zur Erinnerung:
    • der erste Typ der Schnittstelle die JPA-Entität ist, die für CRUD-Operationen (findOne, findAll, save, delete, deleteAll) verwaltet wird,
    • der zweite Typ ist der Primärschlüssel der JPA-Entität, hier ein Ganzzahltyp [Long];
  • Zeile 12: Die Methode in Zeile 12 wird durch die JPQL-Abfrage (Java Persistence Query Language) in Zeile 11 implementiert. Diese Abfrage ruft JPA-Entitäten ab. In einer solchen Abfrage:
    • werden Tabellen durch die zugehörigen JPA-Entitäten ersetzt;
    • Spalten durch Felder der in der Abfrage verwendeten JPA-Entitäten ersetzt;
  • Zeile 11: Die JPQL-Abfrage gibt eine Kategorie zusammen mit ihren Produkten zurück. Erinnern Sie sich daran, dass in der [Category]-Entität das Feld [products] das Attribut [fetch = FetchType.LAZY] (Lazy Loading) hatte. In der JPQL-Abfrage erzwingen wir das Laden der Produkte mithilfe des Schlüsselworts [fetch]. Der Parameter ?1 der Abfrage wird zur Laufzeit durch den Wert des ersten Parameters der Methode in Zeile 12 ersetzt, d. h. durch den Parameter [Long id];
  • Zeilen 14–15: eine ähnliche Methode für eine Kategorie, die durch ihren Namen identifiziert wird;
  • Zeile 18: Die Methode [findByName] wird automatisch von [Spring Data] implementiert, da der Typ [Category] über ein Feld [name] verfügt;

Die Schnittstelle [ProductsRepository] verwaltet den Zugriff auf die Tabelle [PRODUCTS]:


package spring.data.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import spring.data.entities.Produit;
 
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
 
    // un produit avec sa catégorie
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);
 
    @Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
    public Produit getProduitByNameWithCategorie(String nom);
 
    // un produit sans sa catégorie désigné par son nom
    public Produit findByNom(String nom);
}

Die Erläuterungen entsprechen denen zur Schnittstelle [CategoriesRepository].

Diese Schnittstellen werden von Klassen implementiert, die von [Spring Data] bei der Ausführung des Projekts generiert werden. Solche Klassen werden als [Proxies] bezeichnet. Standardmäßig werden die Methoden der Implementierungsklasse innerhalb einer Transaktion ausgeführt. Da diese Schnittstellen die Klasse [CrudRepository] erweitern, sind sie Spring-Komponenten.

11.3.7. Die [DAO]-Schicht

[DAO]-Schicht[Console]-Schicht[JPA]-Schicht[JDBC]-Treiber[Spring Data]-SchichtSpring 4DBMS

  

Die [IDao]-Schnittstelle der [DAO]-Schicht lautet wie folgt:


package spring.data.dao;
 
import java.util.List;
 
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
 
public interface IDao {

    // insert product list
    public List<Produit> addProduits(List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits();
 
    // product list update
    public List<Produit> updateProduits(List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits();
 
    // inserting a list of categories
    public List<Categorie> addCategories(List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories();
 
    // updating a list of categories
    public List<Categorie> updateCategories(List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories();
 
    // a specific product with or without its category
    public Produit getProduitByIdWithoutCategorie(Long idProduit);
 
    public Produit getProduitByIdWithCategorie(Long idProduit);
 
    public Produit getProduitByNameWithCategorie(String nom);
 
    public Produit getProduitByNameWithoutCategorie(String nom);
 
    // a particular category with or without its products
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
 
    public Categorie getCategorieByIdWithProduits(Long idCategorie);
 
    public Categorie getCategorieByNameWithProduits(String nom);
 
    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Hier haben wir die Regel übernommen, dass jede Methode, die die als Eingabeparameter übergebenen Objekte verändert, diese in ihrem Ergebnis zurückgeben muss. Der Grund für diese Regel wurde in Abschnitt 4.2 erläutert: Sie ermöglicht es einer Schicht und ihrem Client, in zwei getrennten JVMs zu residieren und somit in einer Client/Server-Konfiguration zu arbeiten.

Die [Dao]-Implementierung dieser Schnittstelle lautet wie folgt:


package spring.data.dao;
 
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import com.google.common.collect.Lists;
 
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
 
@Component
public class Dao implements IDao {
 
    @Autowired
    private ProduitsRepository produitsRepository;
 
    @Autowired
    private CategoriesRepository categoriesRepository;
 
    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(101, getMessagesForException(e));
        }
    }
 
    @Override
    public void deleteAllProduits() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(102, getMessagesForException(e));
        }
    }
 
    @Override
    public List<Produit> updateProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(103, getMessagesForException(e));
        }
    }
 
    @Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(104, getMessagesForException(e));
        }
    }
 
    @Override
    public void deleteAllCategories() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(105, getMessagesForException(e));
        }
    }
 
    @Override
    public List<Categorie> updateCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(106, getMessagesForException(e));
        }
    }
 
    @Override
    public List<Categorie> getAllCategories() {
        try {
            return Lists.newArrayList(categoriesRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(107, getMessagesForException(e));
        }
    }
 
    @Override
    public List<Produit> getAllProduits() {
        try {
            return Lists.newArrayList(produitsRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(108, getMessagesForException(e));
        }
    }
 
    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        try {
            return produitsRepository.getProduitByIdWithCategorie(idProduit);
        } catch (Exception e) {
            throw new DaoException(109, getMessagesForException(e));
        }
    }
 
    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        try {
            return categoriesRepository.getCategorieByIdWithProduits(idCategorie);
        } catch (Exception e) {
            throw new DaoException(110, getMessagesForException(e));
        }
    }
 
    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        try {
            return categoriesRepository.getCategorieByNameWithProduits(nom);
        } catch (Exception e) {
            throw new DaoException(111, getMessagesForException(e));
        }
    }
 
    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        try {
            return produitsRepository.getProduitByNameWithCategorie(nom);
        } catch (Exception e) {
            throw new DaoException(112, getMessagesForException(e));
        }
    }
 
    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        try {
            return produitsRepository.findOne(idProduit);
        } catch (Exception e) {
            throw new DaoException(113, getMessagesForException(e));
        }
    }
 
    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        try {
            return categoriesRepository.findOne(idCategorie);
        } catch (Exception e) {
            throw new DaoException(114, getMessagesForException(e));
        }
    }
 
    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {
        try {
            return produitsRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(115, getMessagesForException(e));
        }
    }
 
    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        try {
            return categoriesRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(116, getMessagesForException(e));
        }
    }
 
}
  • Zeile 16: Die Annotation [@Component] macht die Klasse [Dao] zu einer Spring-Komponente;
  • Zeilen 19–23: Injektion von Referenzen auf die beiden [CrudRepository]-Schnittstellen aus [Spring Data]. Diese Injektion erfolgt während der Instanziierung von Spring-Objekten, typischerweise zu Beginn der Ausführung des Spring-Projekts;
  • Beachten Sie in den Zeilen 28 und 46, dass die Methode [save] der Schnittstelle [productsRepository] sowohl zum Einfügen als auch zum Aktualisieren von Produkten verwendet wird. [Spring Data] verwendet den Primärschlüssel des Produkts, um zu bestimmen, ob ein Einfügen oder ein Aktualisieren durchgeführt werden soll. Ist der Primärschlüssel [null], erfolgt ein Einfügen; andernfalls erfolgt ein Aktualisieren;
  • Zeile 82: Wir verwenden die Methode [Lists.newArrayList] aus der Guava-Bibliothek, um eine Liste von Produkten zu erhalten. Die Methode [productsRepository.findAll()] gibt einen Typ [Iterable<Product>] zurück;
  • Zeile 28: Die Methode [productsRepository.save(products)] gibt einen [Iterable<Product>] zurück. Dasselbe gilt für die anderen [save]-Operationen in der Klasse;

In der obigen [Dao]-Klasse werden möglicherweise auftretende Ausnahmen im folgenden Typ [DaoException] gekapselt:


package spring.data.dao;
 
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
 
// exception class for the Elections application
// the exception is uncontrolled
 
public class DaoException extends RuntimeException implements Serializable {
 
    // serial ID
    private static final long serialVersionUID = 1L;
 
    // local fields
    private int code;
    private List<String> erreurs;
 
    // manufacturers
    public DaoException() {
        super();
    }
 
    public DaoException(int code, Throwable e) {
        // parent
        super(e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public DaoException(int code, String message, Throwable e) {
        // parent
        super(message, e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public DaoException(int code, String message) {
        // parent
        super(message);
        // local
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }
 
    public DaoException(int code, List<String> erreurs) {
        // parent
        super();
        // local
        this.code = code;
        this.erreurs = erreurs;
    }
 
    // list of exception error messages
    private List<String> getErreursForException(Throwable th) {
        // retrieve the list of exception error messages
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
 
    // getters and setters
...
}
  • Zeile 10: Die Klasse erweitert die Klasse [RuntimeException] und ist daher eine unbehandelte Ausnahme;
  • Zeile 16: ein Fehlercode;
  • Zeile 17: eine Liste von Fehlermeldungen, die mit dem Ausnahmestapel verbunden sind, der die [DaoException] verursacht hat;
  • Zeilen 59–76: Die private Methode [getMessagesForException] ruft die Liste der Fehlermeldungen ab, die mit den Ausnahmen im Ausnahmestapel verbunden sind. Es ist tatsächlich möglich, Ausnahmen mithilfe der folgenden Konstruktoren der Klasse Exception zu stapeln:
    • Exception(String message, Throwable cause): Erstellt eine Ausnahme mit einer Meldung und der zu kapselnden Ausnahme;
    • Exception(Throwable cause): Erstellt eine Ausnahme, die die zu kapselnde Ausnahme enthält;

Der Typ [Throwable] ist die Oberklasse der Klasse [Exception]. Werden die vorgenannten Konstruktoren wiederholt ausgeführt, enthält die endgültige Ausnahme mehrere Ausnahmen. Dies wird als Ausnahmestapel bezeichnet.

  • Die letzte Ursache einer Ausnahme e1 wird durch den Ausdruck [e1.getCause()] ermittelt;
  • Die vorletzte Ursache einer Ausnahme e1 wird mit dem Ausdruck [e1.getCause().getCause()] ermittelt;
  • Dieser Vorgang wird fortgesetzt, bis [getCause()==null] erhalten wird;

11.3.8. Spring-Projektkonfiguration

  

Die Klasse [DaoConfig] konfiguriert die [DAO]-Schicht:


package spring.data.config;
 
import javax.persistence.EntityManagerFactory;
 
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.data.jpa.repository.config.EnableJpaRepositories;
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;
 
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
public class DaoConfig {
 
    // constants
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroSpringData";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    final static String[] ENTITIES_PACKAGES = { "spring.data.entities" };
 
    // the [tomcat-jdbc] data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // an initially open connection
        dataSource.setInitialSize(1);
        // result
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }
 
}

Eine ähnliche Konfiguration wurde in Abschnitt 11.2.5 besprochen und erläutert. Wir haben die folgenden Spring-Annotationen hinzugefügt:

  • Zeile 17: Die Annotation [@EnableJpaRepositories] wird verwendet, um die Pakete anzugeben, in denen sich die [CrudRepository]-Schnittstellen von [Spring Data] befinden;
  • Zeile 18: Die Klasse ist eine Spring-Konfigurationsklasse. Diese Information ist wichtig. Wenn wir sie entfernen, funktioniert das Projekt zwar weiterhin. Später in diesem Dokument, wenn wir Projekte erstellen, die auf diesem basieren, werden einige davon jedoch nicht mehr funktionieren, wenn die Annotation in Zeile 18 entfernt wird;
  • Zeile 19: Die Annotation [@ComponentScan] gibt die Pakete an, in denen sich Spring-Objekte befinden. Dies sind die Klassen, die mit [@Component, @Service, @Controller, ...] annotiert sind. Hier wird die Spring-Komponente [Dao] gefunden und instanziiert;
  • Zeilen 73–76: Wir haben eine Bean definiert, die das Array der Pakete repräsentiert, die nach JPA-Entitäten durchsucht werden sollen. Dies ermöglicht es einem Projekt, das die Klasse [DaoConfig] importiert, diese Bean neu zu definieren und somit die durchsuchten Pakete zu ändern (Zeile 59). Wir werden später in diesem Dokument auf dieses Problem stoßen;

Die Klasse [AppConfig] konfiguriert das gesamte Projekt:


package spring.data.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@Import({DaoConfig.class})
public class AppConfig {
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
}
  • Zeile 11: Die Klasse ist eine Spring-Konfigurationsklasse;
  • Zeile 12: Diese importiert die Beans, die von der soeben betrachteten [DaoConfig]-Klasse definiert wurden;
  • die [console]-Ebene verwendet die hier definierten JSON-Mapper;
  • Zeilen 14–64: definieren fünf JSON-Mapper;
  • Zeilen 15–18: Der JSON-Mapper [jsonMapper] hat keine Filter;
  • Zeilen 20–30: Der JSON-Filter [jsonMapperCategoryWithProducts] ermöglicht die Serialisierung/Deserialisierung eines [Category]-Objekts zusammen mit seinen Produkten;
  • Zeilen 32–42: Der JSON-Filter [jsonMapperProductWithCategory] ermöglicht die Serialisierung/Deserialisierung eines [Product]-Objekts mit seiner Kategorie;
  • Zeilen 43–53: Der JSON-Filter [jsonMapperCategorieWithoutProduits] ermöglicht die Serialisierung/Deserialisierung eines [Categorie]-Objekts ohne dessen Produkte;
  • Zeilen 55–64: Der JSON-Filter [jsonMapperProductWithoutCategory] ermöglicht die Serialisierung und Deserialisierung eines [Product]-Objekts ohne dessen Kategorie;

Beachten Sie, dass Sie beim Erstellen eines JSON-Filters für eine Entität T nicht nur den Filter für die Entität T, sondern auch die Filter für die Entitäten Ti konfigurieren müssen, die diese enthalten kann.

11.3.9. Die [console]-Schicht

Schicht[DAO]Schicht[console]Schicht[JPA]Treiber[JDBC]Schicht[Spring Data]Spring 4DBMS

  

Die [Main]-Klasse sieht wie folgt aus:


package spring.data.console;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
 
import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
 
public class Main {
 
    public static void main(String[] args) throws JsonProcessingException {
        AnnotationConfigApplicationContext context = null;
        try {
            // instantiation Spring context
            context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
                    ObjectMapper.class);
            ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
                    ObjectMapper.class);
            IDao dao = context.getBean(IDao.class);
            // --------------------------------------------------------------------------------------
            // empty the database
            log("Vidage de la base de données", 1);
            // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // fill the tables
            List<Categorie> categories = new ArrayList<Categorie>();
            for (int i = 0; i < 2; i++) {
                Categorie categorie = new Categorie(String.format("categorie%d", i));
                for (int j = 0; j < 5; j++) {
                    categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                            String.format("desc%d%d", i, j)));
                }
                categories.add(categorie);
            }
            // add the category - the products will be cascaded in as well
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // list of categories
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // product list
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // category 1 with its products
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // the product [product14] with its category
            Produit p = dao.getProduitByNameWithCategorie("produit14");
            log("Produit [produit14] avec sa catégorie", 2);
            affiche(p, jsonMapperProduitWithCategorie);
            // --------------------------------------------------------------------------------------
            log("Mise à jour du prix des produits de [categorie1]", 1);
            log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
            Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
            Set<Produit> produits = categorie1.getProduits();
            affiche(categorie1, jsonMapperCategorieWithProduits);
            for (Produit produit : produits) {
                produit.setPrix(1.1 * produit.getPrix());
            }
            dao.updateProduits(Lists.newArrayList(produits));
            log("Produits de la catégorie [categorie1] après la mise à jour", 2);
            affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
            // --------------------------------------------------------------------------------------
            log("Vidage de la base de données", 1);
            // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
            dao.deleteAllCategories();
            // base display
            log("Liste des categories avant l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits avant l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
            // we insert
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // add the category - the products will be cascaded in as well
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // check
            log("Liste des categories après l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits après l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
        } catch (DaoException e) {
            System.out.println(e);
        } finally {
            if (context != null) {
                // finish
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }
 
    private static void log(String message, int mode) {
        // poster message
        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);
    }
}
  • Zeile 25: Instanziierung von Spring-Beans aus der Konfigurationsklasse [AppConfig];
  • Zeilen 26–33: Abrufen von Referenzen auf die JSON-Mapper. Wir verwenden die folgende Signatur der Methode [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class class): Diese wird verwendet, wenn mehrere Beans vom Typ [class] vorhanden sind. In diesem Fall geben wir die Kennung des angeforderten Beans an. Wurde es mit der Annotation [@Bean] definiert, ist seine Kennung der Name der annotierten Methode. Wurde es mit der Annotation [@Bean("identifier")] definiert, ist seine Kennung die in der Annotation angegebene;
  • Zeile 34: Abrufen einer Referenz aus der [DAO]-Schicht;
  • Zeilen 37–39: Löschen der Datenbank. Wir löschen die Tabelle „categories“ (Zeile 39). Da wir geschrieben haben:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

Wenn eine Kategorie gelöscht wird, werden auch alle damit verknüpften Produkte gelöscht;

  • Zeilen 43–53: Befüllen der Tabelle mit 2 Kategorien, die jeweils 5 Produkte enthalten. In Zeile 50 werden durch das Einfügen der beiden Kategorien gleichzeitig deren Produkte eingefügt, wiederum weil wir [cascade = { CascadeType.ALL }] geschrieben haben;
  • Zeile 58: Wir zeigen die Kategorien an. Wir verwenden den JSON-Mapper [jsonMapperCategorieWithoutProduits], um die Kategorien ohne ihre Produkte anzuzeigen. Tatsächlich gibt die Methode [dao.getAllCategories()] die Kategorien ohne ihre Produkte zurück (Lazy Loading);
  • Zeile 61: Wir zeigen die Produkte ohne ihre Kategorie an. Dies liegt daran, dass die Methode [dao.getAllProduits()] die Produkte ohne ihre Kategorie zurückgibt (Lazy Loading);
  • Zeilen 63–65: Anzeige der Kategorie mit dem Namen [categorie1] mit ihren Produkten (Eager Loading);
  • Zeilen 67–69: Anzeige eines Produkts mit seiner Kategorie;
  • Zeilen 71–81: Alle Produktpreise in der Kategorie [categorie1] werden um 10 % erhöht;
  • Zeilen 91–101: Fügen Sie eine Kategorie mit zwei Produkten desselben Namens hinzu. In der Tabelle [PRODUCTS] gibt es jedoch eine Eindeutigkeitsbeschränkung für die Spalte [NAME]. Das Einfügen des zweiten Produkts wird daher abgelehnt und eine Ausnahme ausgelöst. Die Methode [dao.addProducts] läuft jedoch innerhalb einer Transaktion. Da das Einfügen des zweiten Produkts fehlschlägt, muss daher auch das Einfügen des ersten Produkts sowie das der zugehörigen Kategorie [cat1] rückgängig gemacht werden. Genau das wollen wir überprüfen;
  • Zeilen 119–121: eine generische Methode, die die JSON-Zeichenkette für jedes Element vom Typ T anzeigen kann. Die JSON-Serialisierung wird durch den als Parameter übergebenen Mapper gesteuert;
  • Zeilen 124–128: eine ähnliche Methode, diesmal für eine Liste von Elementen vom Typ T;

Die Ausführung der Klasse [Main] liefert folgende Ergebnisse (ohne Spring-Protokolle):


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Affichage de la base --------------------------------
-- Liste des catégories
{"id":4,"version":0,"nom":"categorie0"}
{"id":5,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":13,"version":0,"nom":"produit00","idCategorie":4,"prix":100.0,"description":"desc00"}
{"id":14,"version":0,"nom":"produit01","idCategorie":4,"prix":101.0,"description":"desc01"}
{"id":15,"version":0,"nom":"produit02","idCategorie":4,"prix":102.0,"description":"desc02"}
{"id":16,"version":0,"nom":"produit03","idCategorie":4,"prix":103.0,"description":"desc03"}
{"id":17,"version":0,"nom":"produit04","idCategorie":4,"prix":104.0,"description":"desc04"}
{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"}
{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"}
{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"}
{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"}
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}
-- Catégorie 1 avec ses produits
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produit [produit14] avec sa catégorie
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14","categorie":{"id":5,"version":0,"nom":"categorie1"}}
Mise à jour du prix des produits de [categorie1] --------------------------------
-- Produits de la catégorie [categorie1] avant la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produits de la catégorie [categorie1] après la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":1,"nom":"produit10","idCategorie":5,"prix":121.0,"description":"desc10"},{"id":19,"version":1,"nom":"produit11","idCategorie":5,"prix":122.1,"description":"desc11"},{"id":20,"version":1,"nom":"produit12","idCategorie":5,"prix":123.2,"description":"desc12"},{"id":21,"version":1,"nom":"produit13","idCategorie":5,"prix":124.3,"description":"desc13"},{"id":22,"version":1,"nom":"produit14","idCategorie":5,"prix":125.4,"description":"desc14"}]}
Vidage de la base de données --------------------------------
-- Liste des categories avant l'ajout
-- Liste des produits avant l'ajout
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
-- Liste des categories après l'ajout
-- Liste des produits après l'ajout
Travail terminé
  • Zeilen 4–17: die in die Tabelle eingefügten Kategorien und Produkte;
  • Zeilen 18–19: eine Kategorie mit ihren Produkten;
  • Zeilen 20–21: ein Produkt mit seiner Kategorie;
  • Zeilen 22–26: Preisaktualisierung für bestimmte Produkte. In Zeile 24 sehen wir, dass die Preise tatsächlich um 10 % gestiegen sind;
  • Zeilen 27–36: Hinzufügen der Kategorie [cat1] mit zwei Produkten gleichen Namens. Wir sehen, dass die Tabelle vor (Zeilen 28–29) und nach dem Hinzufügen (Zeilen 35–36) identisch ist, was zeigt, dass alle Einfügungen in der Transaktion tatsächlich zurückgesetzt wurden;
  • Zeilen 31–34: Die Ausnahme, die beim Einfügen des zweiten Produkts auftrat und zum Scheitern der gesamten Transaktion führte;

11.3.10. Der JUnit-Unit-Test

  

Die Klasse [Test01] sieht wie folgt aus:


package spring.data.tests;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
 
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.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
 
import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
 
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
 
    // layer [DAO]
    @Autowired
    private IDao dao;
 
    // filters jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;
 
    @Before
    public void cleanAndFill() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // add the category - the products will be cascaded in as well
        categories = dao.addCategories(categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // a few checks
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
 
    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }
 
    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }
 
    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }
 
    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }
...
    // -------------- private methods
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }
 
    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }
 
    private static void log(String message, int mode) {
        // poster message
        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);
    }
 
    private static void show(String title, List<String> messages) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
 
}
  • Zeile 27: Der Unit-Test wird durch die bereits in Abschnitt 11.3.8 vorgestellte Klasse [AppConfig] konfiguriert;
  • Zeilen 32–33: Einfügen einer Referenz auf die [DAO]-Schicht;
  • Zeilen 36–50: Einfügen der fünf JSON-Mapper;
  • Zeilen 60–71: Nach dem Leeren der Datenbank (Zeile 57) wird die Datenbank mit 2 Kategorien gefüllt, die jeweils 5 Produkte enthalten. Diese Methode wird aufgrund der Annotation [@Before] in Zeile 52 vor jedem Test ausgeführt;
  • Zeilen 75–93: Zeigt den Inhalt der Datenbank an;
  • Zeilen 95–101: Ruft eine Kategorie zusammen mit ihren Produkten ab, identifiziert durch ihren Namen;
  • Zeilen 103–109: Ruft eine Kategorie ohne ihre Produkte ab, identifiziert durch ihren Namen;
  • Zeilen 111–120: Ruft ein Produkt zusammen mit seiner Kategorie ab, identifiziert durch seine ID;
  • Zeilen 122–130: Ruft ein Produkt ohne seine Kategorie ab, identifiziert durch seine Nummer;
  • Zeilen 133–184: private Methoden, die von den verschiedenen Tests gemeinsam genutzt werden;

Zu erledigende Aufgabe: Führen Sie den Test aus. Er sollte erfolgreich sein.


11.3.11. Protokollverwaltung

Die Protokolle für die Konsolenanwendung oder den JUnit-Test werden durch die folgende [logback.xml]-Datei konfiguriert:

  

Die Datei muss den Namen [logback.xml] tragen und sich im Klassenpfad des Projekts befinden. Um dies sicherzustellen, wurde sie hier im Ordner [src/main/resources] abgelegt, der Teil des Klassenpfads ist. Ihr Inhalt lautet wie folgt:


<configuration> 
 
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <!-- log level control -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • Zeile 12: Das Tag [<root level="info">] zeigt Protokolle der Stufe [info] an. Anstelle von [info] können Sie Folgendes verwenden:
    • [debug]: Dies ist die detaillierteste Protokollstufe. Es wird empfohlen, sie während der Debugging-Phase des Projekts zu verwenden, da sie sehr nützliche Protokolle zu Client-Server-Interaktionen liefert. Auf diese Weise lässt sich nachvollziehen, was „unter der Haube“ geschieht;
    • [off]: überhaupt keine Protokolle;
    • [warn]: eine mittlere Protokollierungsstufe, bei der Spring Anomalien anzeigt, die nicht unbedingt Fehler sind. Sie sollten diese überprüfen, wenn Sie nicht das erwartete Ergebnis erhalten;

Aufgabe: Setzen Sie die Stufe in Zeile 12 auf [debug] und führen Sie dann den Unit-Test aus. Beobachten Sie den Unterschied in den Protokollen.


11.3.12. Erstellen des Maven-Archivs des Projekts

Um das Projektarchiv im lokalen Maven-Repository zu installieren, führen Sie die folgenden Schritte [1–3] aus:

Das Archiv wird anhand der in der Datei [pom.xml] enthaltenen Bezeichner generiert:


    <groupId>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

Der Speicherort des lokalen Maven-Repositorys ist in der Eclipse-Konfiguration zu finden:

 

Anschließend können Sie überprüfen, ob das Maven-Artefakt korrekt installiert wurde:

 

Nun kann ein anderes lokales Maven-Projekt dieses Archiv verwenden.