11. [Cours]: Verwaltung relationaler Datenbanken mit Spring Data
Stichworte: mehrschichtige Architektur, Spring, Abhängigkeitsinjektion, API, JPA (Java Persistence API), Spring Data.
Wir werden die Schicht [DAO] des TD mit [Spring Data], einem Zweig des Spring-Ökosystems, implementieren. [Spring Data] basiert auf einer JPA-Schicht (Java Persistence API), die es der Schicht [DAO] ermöglicht, Objekte anstelle von Befehlen SQL zu verarbeiten. Letztendlich weiß die Schicht [DAO] nicht, dass sie mit einer Datenbank kommuniziert. Sie kennt lediglich die Schnittstelle der Schicht [Spring Data].
![]() |
Zunächst werden wir [Spring Data] 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 Skript SQL, mit dem die Beispieldatenbank dieses Kapitels erstellt werden kann;
11.2. Beispiel 1
Auf der Spring-Website gibt es 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 aus [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;
- Bei [3] wählt man ein von Maven konfiguriertes Projekt aus;
- In [4] kann das Tutorial in zwei Formen bereitgestellt werden: [initial], eine leere Version, die man gemäß dem Tutorial ausfüllt, oder [complete], die endgültige Version des Tutorials. Wir wählen Letzteres;
- In [5] kann man das Tutorial in einem Browser anzeigen lassen;
- bei [6] das fertige Projekt.
11.2.1. Die Maven-Konfiguration des Projekts
Die Maven-Abhängigkeiten des Projekts werden 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>
<!-- Verwende UTF-8 für alles -->
<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 ein übergeordnetes Maven-Projekt. Dieses legt den Großteil der Projektabhängigkeiten fest. Entweder sind diese bereits ausreichend, sodass keine weiteren hinzugefügt werden müssen, oder es fehlen noch Abhängigkeiten, die dann ergänzt werden müssen;
- 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 von SGBD und H2, mit denen In-Memory-Datenbanken erstellt und verwaltet werden können.
Sehen wir uns die Klassen an, die durch diese Abhängigkeiten bereitgestellt werden:
![]() | ![]() | ![]() |
Es sind sehr viele:
- Einige gehören zum Spring-Ökosystem (diejenigen, die mit „spring“ beginnen);
- andere gehören zum Hibernate-Ökosystem (hibernate, jboss), von dem wir hier die Implementierung JPA verwenden;
- wieder andere sind Testbibliotheken (junit, hamcrest);
- wieder andere sind Protokollierungsbibliotheken (log4j, logback, slf4j);
Wir werden sie alle beibehalten. Für eine Anwendung im Produktivbetrieb sollten jedoch nur die notwendigen Bibliotheken beibehalten werden.
In Zeile 26 der Datei [pom.xml] findet sich die Zeile:
<start-class>hello.Application</start-class>
Diese Zeile steht in Zusammenhang mit den folgenden Zeilen:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
In den Zeilen 6–9 ermöglicht das Plugin [spring-boot-maven-plugin] die Generierung der ausführbaren JAR-Datei der Anwendung. Zeile 26 der Datei [pom.xml] bezeichnet dann die ausführbare Klasse dieser JAR-Datei.
11.2.2. Die Schicht [JPA]
Der Zugriff auf die Datenbank erfolgt über eine Schicht [JPA], Java Persistence API:
![]() |
![]() |
Die Anwendung ist einfach aufgebaut und verwaltet Kunden [Customer]. Die Klasse [Customer] ist Teil der Schicht [JPA] 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 Instanz [Customer] repräsentiert eine Zeile einer Datenbanktabelle.
- Zeile 8: Annotation JPA, die bewirkt, dass die Persistenz der Instanzen [Customer] (Create, Read, Update, Delete) durch eine Implementierung JPA verwaltet wird. Den Maven-Abhängigkeiten zufolge wird die Implementierung JPA / Hibernate verwendet;
- Zeilen 11–12: Annotationen JPA, die das Feld [id] mit dem Primärschlüssel der Tabelle [Customer] verknüpfen. Zeile 12 gibt an, dass die Implementierung JPA die für das verwendete SGBD spezifische Methode zur Primärschlüsselgenerierung verwendet, in diesem Fall H2;
Es gibt keine weiteren Anmerkungen zu JPA. In diesem Fall werden Standardwerte verwendet:
- Die Tabelle [Customer] erhält den Namen der Klasse, d. h. [Customer];
- die Spalten dieser Tabelle tragen die Namen der Felder der Klasse: [id, firstName, lastName], wobei bei den Namen von Tabellenspalten die Groß-/Kleinschreibung nicht berücksichtigt wird;
Es ist zu beachten, dass die verwendete Implementierung JPA zu keinem Zeitpunkt namentlich genannt wird.
11.2.3. Die Schicht [Spring Data]
Die Klasse [CustomerRepository] implementiert die Zugriffsebene auf 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 Schnittstelle von Spring Data (Zeile 5). Diese Schnittstelle wird durch zwei Typen parametrisiert: Der erste ist der Typ der verwalteten Elemente, hier der Typ [Customer], der zweite der Typ des Primärschlüssels der verwalteten Elemente, hier ein Typ [Long]. Die Schnittstelle [CrudRepository] lautet wie folgt:
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 Operationen CRUD (Create – Read – Update – Delete), die an einem Typ JPA T durchgeführt werden können:
- Zeile 8: Die Methode „save“ ermöglicht es, eine Entität vom Typ T in der Datenbank zu speichern. Sie speichert die Entität unter dem Primärschlüssel, der ihr von SGBD zugewiesen wurde. Außerdem ermöglicht sie die Aktualisierung einer Entität vom Typ T, die durch ihren Primärschlüssel id identifiziert wird. Die Wahl der einen oder anderen Aktion richtet sich nach dem Wert des Primärschlüssels id: Ist dieser null, wird die Persistenzoperation durchgeführt, andernfalls die Aktualisierungsoperation;
- Zeile 10: dasselbe, jedoch für eine Liste von Entitäten;
- Zeile 12: Mit der Methode findOne kann eine Entität T abgerufen werden, die durch ihren Primärschlüssel id identifiziert wird;
- Zeile 22: Mit der Methode „delete“ kann eine Entität T gelöscht werden, die durch ihren Primärschlüssel id identifiziert wird;
- Zeilen 24–28: Varianten der Methode [delete];
- Zeile 16: Mit der Methode [findAll] lassen sich alle persistierten Entitäten T abrufen;
- Zeile 18: dasselbe, jedoch beschränkt auf die Entitäten, für die die Liste der Identifikatoren übergeben 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, ein [Customer] anhand seines Namens [lastName] zu finden;
Und das war’s auch schon für die Schicht [DAO]. Es gibt keine Implementierungsklasse für die vorherige Schnittstelle. Diese wird zur Laufzeit von [Spring Data] generiert. Die Methoden der Schnittstelle [CrudRepository] werden automatisch implementiert. Bei den Methoden, die in der Schnittstelle [CustomerRepository] 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 automatisch von [Spring Data] implementiert, da sie auf das Feld [lastName] (Zeile 3) von [Customer] verweist. Wenn Spring Data in der zu implementierenden Schnittstelle auf eine Methode [findBySomething] stößt, implementiert es diese mithilfe der folgenden JPQL-Abfrage (Java Persistence Query Language):
Der Typ T muss daher ein Feld mit dem Namen [something] besitzen. Somit lautet die Methode
wird durch einen 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] den Persistenzkontext JPA bezeichnet. Dies ist nur möglich, wenn die Klasse [Customer] ein Feld namens [lastName] besitzt, was der Fall ist.
Zusammenfassend lässt sich sagen, dass Spring Data es uns in einfachen Fällen ermöglicht, die Schicht [DAO] mit einer einfachen Schnittstelle zu implementieren.
11.2.4. Die Schicht [console]
![]() |
![]() |
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 {
// einige Kunden speichern
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"));
// alle Kunden abrufen
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : repository.findAll()) {
System.out.println(customer);
}
System.out.println();
// einen einzelnen Kunden über ID abrufen
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// Kunden nach Nachnamen abrufen
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], die eine Schnittstelle [Spring Boot] (Zeile 4) ist. Diese Schnittstelle hat nur eine Methode, nämlich die in Zeile 19;
- Zeile 8: @SpringBootApplication ist eine Annotation, die mehrere Annotationen [Spring Boot] zusammenfasst:
- @Configuration: gibt an, dass es sich bei der Klasse um eine Konfigurationsklasse handelt;
- @EnableAutoConfiguration: weist [Spring Boot] an, selbst eine bestimmte Anzahl von Beans basierend auf verschiedenen Eigenschaften zu erstellen, insbesondere dem Inhalt des Classpath des Projekts. Da sich die Hibernate-Bibliotheken im Classpath befinden, wird das Bean [entityManagerFactory] mit Hibernate implementiert. Da sich die Bibliothek von SGBD (H2) im Classpath befindet, wird die Bean [dataSource] mit H2 implementiert. In der Bean [dataSource] müssen außerdem der Benutzer und sein Passwort definiert werden. Hier verwendet Spring Boot den Standardadministrator von H2, der kein Passwort hat. Da sich die Bibliothek [spring-tx] im Classpath befindet, wird der Transaktionsmanager von Spring verwendet;
- @EnableWebMvc: Wenn sich die Bibliothek [spring-mvc] im Classpath befindet. In diesem Fall erfolgt eine automatische Konfiguration für die Webanwendung;
- @ComponentScan: Diese Anweisung teilt Spring mit, wo nach den anderen Beans, Konfigurationen und Diensten gesucht werden soll. Hier werden sie standardmäßig in dem Paket gesucht, das die mit dem Tag versehene Klasse enthält, d. h. im Paket [hello]. So werden die Klassen [Customer] und [CustomerRepository] gefunden. Da die erste die Annotation [@Entity] trägt, wird sie als von Hibernate zu verwaltende Entität katalogisiert. Da die zweite die Schnittstelle [CrudRepository] erweitert, wird sie als Spring-Bean registriert;
- Zeilen 11–12: Die Bean [CustomerRepository] wird in den Code der Hauptklasse injiziert;
- Zeile 15: Die statische Methode [run] der Klasse [SpringApplication] des Spring-Boot-Projekts wird ausgeführt. Ihr Parameter ist die Klasse, die eine Annotation [Configuration] oder [EnableAutoConfiguration] trägt. Anschließend läuft alles ab, was zuvor erläutert wurde. Das Ergebnis ist ein Spring-Anwendungskontext, d. h. eine Sammlung von Beans, die von Spring verwaltet werden;
Die folgenden Operationen nutzen lediglich die Methoden des Beans, der die Schnittstelle [CustomerRepository] implementiert. Die Konsolenausgaben lauten wie folgt:
- Zeilen 1–8: das Logo des Spring-Boot-Projekts;
- Zeile 9: Die Klasse [hello.Application] wird ausgeführt;
- Zeile 10: [AnnotationConfigApplicationContext] ist eine Klasse, die die Spring-Schnittstelle [ApplicationContext] implementiert. Es handelt sich um einen Bean-Container;
- Zeile 11: Das Bean [entityManagerFactory] wird durch die Klasse [LocalContainerEntityManagerFactory] implementiert, eine Spring-Klasse;
- Zeile 12: Hier taucht [Hibernate] auf. Diese Implementierung JPA wurde ausgewählt;
- Zeile 19: Als Hibernate-Dialekt wird die Variante SQL zur Verwendung mit SGBD festgelegt. Hier zeigt der Dialekt [H2Dialect], dass Hibernate mit den Dialekten SGBD und H2 arbeiten wird;
- Zeilen 21–22: Die Datenbank wird angelegt. Die Tabelle [CUSTOMER] wird angelegt. Das bedeutet, dass Hibernate so konfiguriert wurde, dass es die Tabellen anhand der Definitionen JPA generiert, in diesem Fall anhand der Definition JPA 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 zum Schließen des Spring-Kontexts.
11.2.5. Manuelle Konfiguration des Spring-Data-Projekts
Wir duplizieren das vorherige Projekt im Projekt [gs-accessing-data-jpa-02]:
![]() |
In diesem neuen Projekt verlassen wir uns nicht auf die automatische Konfiguration durch Spring Boot. Wir nehmen die Konfiguration manuell vor. Dies kann nützlich sein, wenn uns die Standardkonfigurationen nicht zusagen.
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-Datenbank -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
</dependencies>
<properties>
<!-- Verwende UTF-8 für alles -->
<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 definierte Bibliotheken wir verwenden werden;
- Zeilen 18–21: Spring Data, das für den Zugriff auf die Datenbank verwendet wird;
- Zeilen 23–26: die Hibernate-Implementierung der Spezifikation JPA;
- Zeilen 28–31: das SGBD H2;
- Zeilen 33–36: Datenbanken werden häufig mit Pools offener Verbindungen verwendet, wodurch das wiederholte Öffnen und Schließen von Verbindungen vermieden wird. Hier kommt die Implementierung [tomcat-jdbc] zum Einsatz;
Im neuen Projekt bleiben die Entität [Customer] und die Schnittstelle [CustomerRepository] unverändert. Die Klasse [Application] wird in zwei Klassen aufgeteilt:
- [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) {
// Instanziierung des Spring-Kontexts
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// einige Kunden speichern
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"));
...
// Kontext schließen
context.close();
}
}
- Zeile 9: Die Klasse [Application] enthält keine Konfigurationsanmerkungen mehr;
- Zeilen 3–7: Es ist zu beachten, dass keine Importe von [Spring Boot]-Paketen mehr vorhanden sind;
- Zeile 12: Die Spring-Beans werden instanziiert. Man erhält den Spring-Kontext, der die Referenz auf die so erstellten Beans enthält;
- Zeile 13: Es wird eine Referenz auf das Bean vom Typ [CustomerRepository] angefordert;
Die Klasse [Config] , die das Projekt konfiguriert, lautet wie folgt:
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 {
// die Datenbank H2
@Bean
public DataSource dataSource() {
// Datenquelle TomcatJdbc
DataSource dataSource = new DataSource();
// Zugriffskonfiguration JDBC
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
// eine ursprünglich geöffnete Verbindung
dataSource.setInitialSize(1);
// Ergebnis
return dataSource;
}
// der 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();
}
// Transaktionsmanager
@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 Schnittstellen [CrudRepository] innerhalb einer Transaktion ausgeführt werden müssen. Sie wurde auskommentiert, da dies standardmäßig der Fall ist;
- Zeile 18: Die Annotation [@EnableJpaRepositories] ermöglicht es, die Ordner anzugeben, in denen sich die Spring-Data-Schnittstellen [CrudRepository] befinden. Diese Schnittstellen werden zu Spring-Komponenten und stehen in dessen Kontext zur Verfügung;
- Zeile 19: Die Annotation [@Configuration] macht die Klasse [Config] zu einer Spring-Konfigurationsklasse;
- Zeile 20: Die Annotation [@ComponentScan] ermöglicht es, die Verzeichnisse aufzulisten, 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 keine anderen als die, die innerhalb der Klasse [AppConfig] definiert sind, daher wurde die Annotation auskommentiert;
- Zeilen 24–37: Hier wird die Datenquelle, die Datenbank H2, definiert. Die Annotation @Bean in Zeile 25 sorgt dafür, dass das durch diese Methode erstellte Objekt zu einer von Spring verwalteten Komponente wird. Der Name der Methode kann hier beliebig gewählt werden. Sie muss jedoch [dataSource] heißen, wenn die Methode EntityManagerFactory aus Zeile 51 fehlt und per Autokonfiguration definiert wird;
- Zeile 30: Die Datenbank wird den Namen [demo] tragen und im Projektordner generiert;
- Zeilen 40–47: Definieren die verwendete Implementierung JPA, in diesem Fall eine Hibernate-Implementierung. Der Name der Methode kann hier beliebig gewählt werden;
- Zeile 43: keine Protokolle für SQL;
- Zeile 44: Die Datenbank wird angelegt, falls sie noch nicht existiert;
- Zeilen 50–58: Definieren die Methode EntityManagerFactory, die die Persistenz von JPA verwaltet. Die Methode muss zwingend [entityManagerFactory] heißen;
- Zeile 51: Die Methode erhält zwei Parameter vom Typ der beiden zuvor definierten Beans. Diese werden dann instanziiert und von Spring als Parameter der Methode injiziert;
- Zeile 53: Legt die verwendete Implementierung JPA fest;
- Zeile 54: Legt die Verzeichnisse fest, in denen die Entitäten JPA zu finden sind;
- Zeile 55: Legt die zu verwaltende Datenquelle fest;
- Zeilen 61–66: Der Transaktionsmanager. Die Methode muss zwingend den Namen [transactionManager] tragen. Sie erhält als Parameter die Bean aus den Zeilen 51–58;
- Zeile 64: Der Transaktionsmanager wird mit EntityManagerFactory verknüpft;
Die vorangegangenen Methoden können in beliebiger Reihenfolge definiert werden.
Die Ausführung des Projekts liefert dieselben Ergebnisse. Im Projektordner erscheint eine neue Datei, nämlich die Datenbankdatei H2:
![]() |
11.2.6. Erstellung eines ausführbaren Archivs
Um ein ausführbares Archiv des Projekts zu erstellen, kann man wie folgt vorgehen:
![]() |
- in [1]: Man erstellt eine Ausführungskonfiguration;
- in [2]: vom Typ [Java Application]
- in [3]: gibt das auszuführende Projekt an (die Schaltfläche Browse verwenden);
- in [4]: gibt die auszuführende Klasse an;
- in [5]: der Name der Ausführungskonfiguration – kann beliebig sein;
![]() |
- in [6]: Das Projekt wird exportiert;
- in [7]: in Form eines ausführbaren JAR-Archivs;
- in [8]: gibt den Pfad und den Namen der zu erstellenden ausführbaren Datei an;
- in [9]: den Namen der in [5] erstellten Ausführungskonfiguration;
![]() |
- in [10] das erstellte Archiv;
Anschließend öffnen Sie eine Konsole in dem Ordner, der das ausführbare Archiv enthält:
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:
11.3. Beispiel 2
11.3.1. Einleitung
Wir greifen das Beispiel der Produkttabelle wieder auf, das wir zur Einführung von API und JDBC verwendet haben, und erstellen die folgende Architektur:
![]() |
Die Datenbank [dbintrospringjpa] enthält zwei Tabellen: [PRODUITS] und [CATEGORIES]. Die Tabelle [CATEGORIES] sieht wie folgt aus:
![]() |
- [ID]: Primärschlüssel im Modus AUTO_INCREMENT;
- [VERSION]: Versionsnummer des Datensatzes;
- [NOM]: Name der Kategorie – eindeutig;
Die Tabelle [PRODUITS] sieht wie folgt aus:
![]() |
- [ID]: Primärschlüssel im Modus AUTO_INCREMENT;
- [VERSION]: Versionsnummer des Datensatzes;
- [NOM]: Name eines Produkts – eindeutig;
- [ID_CATEGORIE]: Nummer seiner Kategorie – Fremdschlüssel auf dem Feld [CATEGORIES.ID];
- [PRIX]: Preis;
- [DESCRIPTION]: eine Beschreibung des Produkts;
Aufgabe: Erstellen Sie die Datenbank [dbintrospringdata] mit dem Skript SQL [dbintrospringdata.sql] aus dem Support:
11.3.2. Erstellung des Maven-Projekts
Um ein Spring-Data-Projektgerüst zu erstellen, kann man wie folgt vorgehen:
![]() |
- In [1] wird ein neues Projekt angelegt;
- in [2]: vom Typ [Spring Starter Project];
- Das generierte Projekt ist ein Maven-Projekt. In [3] gibt man den Namen der Projektgruppe an;
- in [4]: Geben Sie den Namen des Artefakts (hier eine JAR-Datei) an, das beim Erstellen des Projekts generiert wird;
- in [5]: der Eclipse-Name des Projekts – kann beliebig sein (muss nicht mit [4] übereinstimmen);
- in [7]: Es wird angegeben, dass ein Projekt mit einer Ebene [JPA] und den Ebenen SGBD sowie MySQL erstellt wird. Die für ein solches Projekt erforderlichen Abhängigkeiten werden dann in die Datei [pom.xml] aufgenommen;
![]() |
- in [8] den Namen des Projektordners angeben;
- in [9] den Assistenten abschließen;
![]() |
- in [10]: das erstellte Projekt;
Die Datei [pom.xml] enthält die erforderlichen Abhängigkeiten für ein Projekt JPA:
<?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/> <!-- Übergeordnetes Element aus dem Repository suchen -->
</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 Vielzahl von Bibliotheken mit ihren Versionen – diese Bibliotheken werden als Maven-Abhängigkeiten verwendet, ohne ihre Version anzugeben;
- Zeilen 28–31: Die für JPA erforderliche Abhängigkeit – wird [Spring Data] einbinden;
- Zeilen 32–36: Die Abhängigkeit von JDBC als Treiber für MySQL;
- Zeilen 37–41: Die erforderlichen Abhängigkeiten für die in Spring integrierten Tests JUnit;
Die ausführbare Klasse [Application] führt keine Aktion 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 Selbstkonfigurationsklasse des Projekts;
Die Testklasse [ApplicationTests] führt keine Aktion 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 Nutzung der Konfigurationsdatei [Application]. Die Testklasse profitiert somit von allen Beans, die in dieser Datei definiert sind;
- Zeile 8: Die Annotation [@RunWith] ermöglicht die Integration von Spring mit JUnit: Die Klasse kann somit als JUnit-Test ausgeführt werden. [@RunWith] ist eine Annotation JUnit (Zeile 4), während die Klasse [SpringJUnit4ClassRunner] eine Spring-Klasse ist (Zeile 6);
Da wir nun über ein Anwendungsgerüst JPA verfügen, können wir es vervollständigen, um das Projekt der Persistenzschicht zu erstellen, die mit der Produktdatenbank verknüpft ist.
11.3.3. Das Eclipse-Projekt
Wir erweitern das vorherige Projekt wie folgt:
![]() |
- [AppConfig.java]: die Konfigurationsklasse des Spring-Projekts;
- [Main.java]: die ausführbare Klasse des Projekts;
- [IDao.java]: die Schnittstelle der Schicht [DAO];
- [Dao.java]: die Implementierungsklasse der Schicht [DAO];
- [AbstractEntity.java]: die übergeordnete Klasse der Klassen [Produit] und [Categorie];
- [Produit.java]: Klasse, die einer Zeile der Tabelle [PRODUITS] in der Datenbank zugeordnet ist;
- [Categorie.java]: Klasse, die einer Zeile der Tabelle [CATEGORIES] in der Datenbank zugeordnet ist;
- [ProduitsRepository]: die Spring-Data-Schnittstelle für den Zugriff auf die Tabelle [PRODUITS];
- [CategoriesRepository]: die Spring-Data-Schnittstelle für den Zugriff auf die Tabelle [CATEGORIES];
- [pom.xml]: die Konfigurationsdatei des Maven-Projekts;
Dieses Projekt implementiert die folgende Architektur:
![]() |
Die Schicht [DAO] sieht nur die von [Spring Data] implementierte Schicht.
11.3.4. Maven-Konfiguration
Die Datei [pom.xml] des Maven-Projekts 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 Datenbank -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- Bibliothek 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>
<!-- Logging-Bibliothek -->
<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 entspricht der in Abschnitt 11.2.5 verwendeten und erläuterten. Wir fügen folgende Bibliotheken hinzu:
- Zeilen 42–49: eine Bibliothek jSON, die von der Methode [toString] der Klasse [Produit] verwendet wird;
- Zeilen 51–55: die Bibliothek [Google Guava], die Hilfsmethoden zur Verwaltung von Elementesammlungen bereitstellt. Sie wird von der Klasse [Dao] verwendet, die die Schicht [DAO] implementiert;
- Zeilen 56–67: die für die Tests erforderlichen Bibliotheken JUnit;
- Zeilen 69–72: eine Log-Bibliothek;
- Zeilen 81–86: die für das Projekt erforderlichen Maven-Plugins;
11.3.5. Die Entitäten der Schicht [JPA]
Schicht
[DAO]
Schicht
[console]
Ebene
[JPA]
Pilot
[JDBC]
Schicht
[Spring Data]
Spring 4
SGBD
![]() |
11.3.5.1. Die Klasse [AbstractEntity]
Die Klasse [AbstractEntity] lautet wie folgt:
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 {
// Eigenschaften
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
protected Long id;
@Version
@Column(name = "VERSION")
protected Long version;
// Konstruktoren
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// Neudefinition von [equals] und [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();
}
// Signatur jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// Getter und Setter
....
}
Diese Klasse dient dazu, eine übergeordnete Klasse für die Entitäten JPA bereitzustellen, indem sie an einer einzigen Stelle die Eigenschaften [id, version] (Zeilen 19, 22), die den beiden mit der Datenbank verknüpften Entitäten [Produit] und [Categorie] gemeinsam sind. Diese Eigenschaften sind mit den Spalten [ID, VERSION] der Tabellen verknüpft (Zeilen 18, 21).
- Zeile 13: Die Anmerkung [@MappedSuperclass] gibt an, dass die Klasse eine übergeordnete Klasse der Entitäten JPA ist;
- Zeile 16: Die Anmerkung [@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 Generierung der Primärschlüssel fest. Der Modus [GenerationType.IDENTITY] verwendet zusammen mit MySQL den Modus [AUTO_INCREMENT]. Bei einem anderen SGBD würde dieser Modus eine andere Methode verwenden. Der Vorteil besteht darin, dass sich der Entwickler nicht darum kümmern muss und sein Code unabhängig vom verwendeten SGBD gültig bleibt;
- Zeile 18: Die Anmerkung [@Column] gibt die Spalte an, die dem Feld zugeordnet ist. Fehlt diese Anmerkung, geht JPA davon aus, dass die Spalte denselben Namen wie das Feld trägt. Dies ist hier der Fall. Man hätte diese Anmerkung also weglassen können;
- Zeile 20: Die Anmerkung [@Version] gibt an, dass das Feld [version] einer Versionsspalte zugeordnet ist. Die Implementierung JPA erhöht diese Versionsnummer jedes Mal, wenn die Entität geändert wird. Diese Nummer dient dazu, die gleichzeitige Aktualisierung der Entität durch zwei verschiedene Benutzer zu verhindern: Zwei Benutzer, U1 und U2, lesen die Entität E mit einer Versionsnummer 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: Dabei wird eine Ausnahme ausgelöst, da seine Versionsnummer (V1) von der in der Datenbank gespeicherten (V1+1) abweicht;
- Zeilen 35–52: Neudefinition der Methoden [hashCode] und [equals]. Standardmäßig ist [obj1.equals(obj2)] „true“, wenn [obj1==obj2] „true“ ist, d. h., wenn ob1 und obj2 zwei gleiche Zeiger sind. Wenn man die Objekte, auf die verwiesen wird, anstatt der Zeiger selbst vergleichen möchte, muss man die Methode [equals] und die Methode [hashCode] neu definieren. Letztere muss für zwei Objekte denselben Wert zurückgeben, die von der Methode [equals] als gleich bezeichnet werden;
- Zeilen 42–51: Zwei Objekte vom Typ [AbstractEntity] oder davon abgeleitete Objekte gelten als gleich, wenn ihre Primärschlüssel [id] gleich sind;
- Zeilen 35–38: Die Methode [hashCode] gibt tatsächlich denselben Wert für zwei identische Objekte vom Typ [AbstractEntity] zurück, die somit denselben Primärschlüssel [id] haben;
- Zeilen 55–63: Die Methode [toString] gibt die Zeichenkette jSon des Objekts [this] zurück. Wenn dieses Objekt eine Unterklasse bezeichnet, gibt diese Methode die Zeichenfolge jSON der Unterklasse zurück. Dadurch entfällt die Notwendigkeit, in den Unterklassen eine Methode [toString] zu erstellen;
11.3.5.2. Die Entität JPA [Produit]
Die Klasse [Produit] ist eine Entität JPA, die einer Zeile der Tabelle [PRODUITS] 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 {
// Eigenschaften
@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;
// die Kategorie
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CATEGORIE_ID")
private Categorie categorie;
// Konstruktoren
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// Getter und Setter
...
}
- Zeile 12: Die Anmerkung [@Entity] macht die Klasse [Produit] zu einer Entität, die von der Schicht [JPA] verwaltet wird;
- Zeile 13: Die Anmerkung [@Table(name = "PRODUITS")] gibt an, dass die Klasse [Produit] das Objektbild einer Zeile der Tabelle [PRODUITS] in der Datenbank ist;
- Zeile 14: Der Name des Filters jSON, der auf die Entität angewendet werden soll. Wir werden sehen, dass die Eigenschaft [categorie] aus Zeile 13 nicht immer verfügbar ist. Sie muss daher aus der Darstellung jSON des Objekts ausgeschlossen werden. Dazu benötigen wir einen Filter. In einem Filter mit dem Namen [jsonFilterCategorie] legen wir also fest, ob die Eigenschaft [categorie] berücksichtigt werden soll oder nicht;
- Zeile 18: Die Anmerkung [@Column] ordnet das Feld [nom] der Spalte [NOM] der Tabelle [PRODUITS] zu. Wenn das Feld denselben Namen wie die zugehörige Spalte trägt, kann die Anmerkung [@Column] weggelassen werden. Dies wäre hier der Fall;
- Zeilen 31–33: die Produktkategorie;
- Zeile 31: Die Anmerkung [@ManyToOne] gibt an, dass die Spalte der Anmerkung in Zeile 32 ([@JoinColumn(name = "CATEGORIE_ID")]) ein Fremdschlüssel der Tabelle [PRODUITS] derEntität [Produit] auf die Tabelle [CATEGORIES], die der Entität in Zeile 33 zugeordnet ist. Diese Annotation muss eine Entität JPA annotieren. Somit muss die Klasse in Zeile 33 eine Entität JPA sein;
- Zeile 31: Die Annotation [fetch = FetchType.LAZY] legt fest, dass beim Abrufen eines Produkts aus der Tabelle [PRODUITS] dessen Kategorie (Zeile 33) nicht sofort abgerufen wird (Lazy Loading). Sie wird dann beim ersten Aufruf der Methode [getCategorie] abgerufen. Dieses Attribut ist nicht zwingend. Die verwendete Implementierung JPA darf dieses Attribut ignorieren. Da die Eigenschaft [categorie] vorhanden sein kann oder auch nicht, haben wir den Filter jSON in Zeile 14 eingeführt. Die bestehenden Implementierungen JPA (Hibernate, Eclipselink, OpenJPA) behandeln diese Annotation nicht auf dieselbe Weise. Hibernate erweitert die ursprüngliche Methode [getCategorie] (die lediglich das Feld categorie zurückgibt) um einen Aufruf von SGBD, um die Kategorie abzurufen. Damit dies möglich ist, muss die Verbindung zu SGBD, die ursprünglich zum Abrufen des Produkts verwendet wurde, noch offen sein; andernfalls wird eine Ausnahme ausgelöst.
11.3.5.3. Die Entität JPA [Categorie]
Die Klasse [Categorie] ist eine Entität JPA, die einer Zeile der Tabelle [CATEGORIES] zugeordnet ist:
![]() |
Ihr Code lautet wie folgt:
package 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 {
// Eigenschaften
@Column(name = "NOM")
private String nom;
// verwandte Produkte
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
public Set<Produit> produits = new HashSet<Produit>();
// Konstruktoren
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// Methoden
public void addProduit(Produit produit) {
// Das Produkt hinzufügen
produits.add(produit);
// Kategorie festlegen
produit.setCategorie(this);
}
// Getter und Setter
...
}
- Zeilen 21–22: der Name der Kategorie;
- Zeilen 25–26: die Produkte dieser Kategorie;
- Zeile 25: Die Annotation [@OneToMany] ist die umgekehrte Beziehung zur Beziehung [@ManyToOne], auf die wir in der Entität [Produit] gestoßen sind. Das Attribut [mappedBy = "categorie"] gibt das Feld der Entität [Produit] an, das durch die umgekehrte Beziehung [@ManyToOne] annotiert ist. Das Attribut [cascade = { CascadeType.ALL }] legt fest, dass die Operationen (persist, merge, remove), die an einer @Entity [Categorie] durchgeführt werden, auf die [produits] in Zeile 26 kaskadiert werden. Teilweise Kaskadierungen können mit den Konstanten [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] angegeben werden;
- Zeile 25: Das Attribut [fetch = FetchType.LAZY] sorgt dafür, dass beim Abrufen einer Kategorie aus der Tabelle [CATEGORIES] deren Produkte nicht sofort mitgeliefert werden. Dies geschieht erst beim ersten Aufruf der Methode [getProduits]. Die bestehenden Implementierungen von JPA (Hibernate, Eclipselink, OpenJPA) behandeln diese Annotation nicht auf dieselbe Weise. Hibernate erweitert die ursprüngliche Methode [getProduits] (die lediglich das Feld produits zurückgibt) um einen Aufruf der Methode SGBD, um die Produkte der Kategorie abzurufen. Damit dies möglich ist, muss die Verbindung zu SGBD, die ursprünglich zum Abrufen der Kategorie verwendet wurde, noch offen sein. Dieses Attribut ist zwingend. Die Implementierung JPA kann dies nicht ignorieren. Da die Eigenschaft [produits] initialisiert sein kann oder auch nicht, haben wir den Filter jSON in Zeile 17 eingeführt, mit dem wir angeben können, ob diese Eigenschaft gewünscht ist 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 namens ensemble. Eine Menge kann keine zwei identischen Objekte enthalten. Hier sind die Objekte vom Typ [Produit]. Somit können in der Menge keine zwei identischen Objekte vorkommen. Da die Methode [equals] der übergeordneten Klasse [AbstractEntity] so umdefiniert wurde, dass zwei Produkte als identisch gelten, wenn sie denselben Primärschlüssel haben, kann das Feld [produits] keine zwei Produkte mit demselben Primärschlüssel enthalten;
- Zeilen 38–43: Mit der Methode [addProduit] kann ein Produkt zur Kategorie hinzugefügt werden;
11.3.6. Die Ebene [Spring Data]
Ebene
[DAO]
Ebene
[console]
Schicht
[JPA]
Fahrer
[JDBC]
Unterhemd
[Spring Data]
Spring 4
SGBD
![]() |
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> {
// Kategorie mit ihren Produkten
@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);
// eine Kategorie ohne ihre Produkte, bezeichnet durch ihren Namen
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 ist die Entität JPA, die für Zugriffe auf CRUD (findOne, findAll, Speichern, Löschen, deleteAll) verwaltet wird,
- der zweite Typ ist der Primärschlüssel der Entität JPA, hier eine Ganzzahl [Long];
- Zeile 12: Die Methode in Zeile 12 wird durch die Abfrage JPQL (Java Persistence Query Language) aus Zeile 11 implementiert. Diese fragt Entitäten vom Typ JPA ab. In einer solchen Abfrage:
- werden die Tabellen durch die zugehörigen Entitäten JPA ersetzt;
- werden die Spalten durch Felder der in der Abfrage verwendeten Entitäten JPA ersetzt;
- Zeile 11: Die Abfrage JPQL liefert eine Kategorie mit ihren Produkten zurück. Wir erinnern uns, dass in der Entität [Categorie] das Feld [produits] das Attribut [fetch = FetchType.LAZY] (Lazy Loading). In der Abfrage JPQL erzwingen wir das Laden der Produkte mit dem Schlüsselwort [fetch]. Der Parameter ?1 der Abfrage wird bei der Ausführung durch den Wert des ersten Parameters der Methode in Zeile 12 ersetzt, also durch den Parameter [Long id];
- Zeilen 14–15: Eine analoge Methode für eine Kategorie, die durch ihren Namen identifiziert wird;
- Zeile 18: Die Methode [findByNom] wird automatisch durch [Spring Data] implementiert, da der Typ [Category] ein Feld [nom] besitzt;
Die Schnittstelle [ProduitsRepository] verwaltet den Zugriff auf die Tabelle [PRODUITS]:
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> {
// ein Produkt mit seiner Kategorie
@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);
// ein Produkt ohne seine Kategorie, bezeichnet durch seinen Namen
public Produit findByNom(String nom);
}
Die Erläuterungen entsprechen denen für die Schnittstelle [CategoriesRepository].
Diese Schnittstellen werden bei der Ausführung des Projekts durch Klassen implementiert, die von [Spring Data] generiert werden. Solche Klassen werden als [proxy] bezeichnet. Standardmäßig werden die Methoden der Implementierungsklasse in einer Transaktion ausgeführt. Da diese Schnittstellen die Klasse [CrudRepository] erweitern, sind sie Spring-Komponenten.
11.3.7. Die Schicht [DAO]
Schicht
[DAO]
Schicht
[console]
Schicht
[JPA]
Fahrer
[JDBC]
Windel
[Spring Data]
Spring 4
SGBD
![]() |
Die Schnittstelle [IDao] der Schicht [DAO] lautet wie folgt:
package spring.data.dao;
import java.util.List;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
public interface IDao {
// Einfügen einer Produktliste
public List<Produit> addProduits(List<Produit> produits);
// Löschen aller Produkte
public void deleteAllProduits();
// Aktualisierung einer Produktliste
public List<Produit> updateProduits(List<Produit> produits);
// Abruf aller Produkte
public List<Produit> getAllProduits();
// Einfügen einer Kategorieliste
public List<Categorie> addCategories(List<Categorie> categories);
// Löschen aller Kategorien
public void deleteAllCategories();
// Aktualisierung einer Kategorieliste
public List<Categorie> updateCategories(List<Categorie> categories);
// Abruf aller Kategorien
public List<Categorie> getAllCategories();
// ein bestimmtes Produkt mit oder ohne zugehörige Kategorie
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// eine bestimmte Kategorie mit oder ohne zugehörige Produkte
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
Hier wurde die Regel übernommen, dass jede Methode, die die Objekte verändert, die sie als Eingabeparameter erhält, diese anschließend 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, sich in zwei getrennten JVM zu befinden und somit im Client-Server-Modus zu arbeiten.
Die Implementierung [Dao] 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: Einfügen der Referenzen in die beiden Schnittstellen „[CrudRepository]“ und „[Spring Data]“. Diese Einfügung erfolgt bei der Instanziierung der Spring-Objekte, in der Regel zu Beginn der Ausführung des Spring-Projekts;
- In den Zeilen 28 und 46 ist zu beachten, dass die Methode [save] der Schnittstelle [produitsRepository] sowohl für das Einfügen als auch für die Aktualisierung von Produkten verwendet wird. [Spring Data] verwendet den Primärschlüssel des Produkts, um zu ermitteln, ob ein Einfügen oder ein Aktualisieren erfolgen soll. Wenn der Primärschlüssel den Wert [null] hat, handelt es sich um ein Einfügen, andernfalls um ein Aktualisieren;
- Zeile 82: Es wird die Methode [Lists.newArrayList] aus der Guava-Bibliothek verwendet, um eine Liste von Produkten abzurufen. Die Methode [produitsRepository.findAll()] gibt einen Typ [Iterable<Produit>] zurück;
- Zeile 28: Die Methode [produitsRepository.save(produits)] gibt einen Typ [Iterable<Produit>] zurück. Das Gleiche gilt für die anderen Operationen [save] der Klasse;
In der oben genannten Klasse [Dao] sind die möglicherweise auftretenden Ausnahmen im folgenden Typ [DaoException] gekapselt:
package spring.data.dao;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
// Ausnahmeklasse für die Anwendung „Wahlen“
// Die Ausnahme wird nicht geprüft
public class DaoException extends RuntimeException implements Serializable {
// Seriennummer ID
private static final long serialVersionUID = 1L;
// lokale Felder
private int code;
private List<String> erreurs;
// Konstruktoren
public DaoException() {
super();
}
public DaoException(int code, Throwable e) {
// übergeordnet
super(e);
// lokal
this.code = code;
this.erreurs = getErreursForException(e);
}
public DaoException(int code, String message, Throwable e) {
// übergeordnet
super(message, e);
// lokal
this.code = code;
this.erreurs = getErreursForException(e);
}
public DaoException(int code, String message) {
// übergeordnet
super(message);
// lokal
this.code = code;
List<String> erreurs = new ArrayList<>();
erreurs.add(message);
this.erreurs = erreurs;
}
public DaoException(int code, List<String> erreurs) {
// übergeordnet
super();
// lokal
this.code = code;
this.erreurs = erreurs;
}
// Liste der Fehlermeldungen einer Ausnahme
private List<String> getErreursForException(Throwable th) {
// Die Liste der Fehlermeldungen der Ausnahme wird abgerufen
Throwable cause = th;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// Die Meldung wird nur abgerufen, wenn sie != null und nicht leer ist
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// nächste Ursache
cause = cause.getCause();
}
return erreurs;
}
// Getter und Setter
...
}
- Zeile 10: Die Klasse leitet sich von der Klasse [RuntimeException] ab und ist somit eine unkontrollierte Ausnahme;
- Zeile 16: ein Fehlercode;
- Zeile 17: eine Liste von Fehlermeldungen, die zu dem Ausnahmestapel gehören, der die Ausnahme [DaoException] ausgelöst hat;
- Zeilen 59–76: Die private Methode [getMessagesForException] ermöglicht es, die Liste der Fehlermeldungen abzurufen, die mit den Ausnahmen im Ausnahmestapel verbunden sind. Es ist nämlich möglich, Ausnahmen mit den folgenden Konstruktoren der Klasse „Exception“ zu stapeln:
- Exception(String message, Throwable cause): Erzeugt eine Ausnahme mit einer Meldung und der Ausnahme, die man kapseln möchte;
- Exception(Throwable cause): Erstellt eine Ausnahme mit der Ausnahme, die gekapselt werden soll;
Der Typ [Throwable] ist die übergeordnete Klasse der Klasse [Exception]. Werden die oben genannten Konstruktoren wiederholt ausgeführt, enthält die endgültige Ausnahme mehrere Ausnahmen. Man spricht dann von einem Ausnahmestapel.
- Die letzte Ursache einer Ausnahme e1 wird durch den Ausdruck [e1.getCause()] ermittelt;
- die vorletzte Ursache einer Ausnahme e1 wird durch den Ausdruck [e1.getCause().getCause()] ermittelt;
- so fährt man fort, bis man [getCause()==null] erhält;
11.3.8. Konfiguration des Spring-Projekts
![]() |
Die Klasse [DaoConfig] konfiguriert die Schicht [DAO]:
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 {
// Konstanten
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" };
// Die Datenquelle [tomcat-jdbc]
@Bean
public DataSource dataSource() {
// Datenquelle TomcatJdbc
DataSource dataSource = new DataSource();
// Zugriffskonfiguration JDBC
dataSource.setDriverClassName(DRIVER_CLASSNAME);
dataSource.setUsername(USER);
dataSource.setPassword(PASSWD);
dataSource.setUrl(URL);
// eine ursprünglich geöffnete Verbindung
dataSource.setInitialSize(1);
// Ergebnis
return dataSource;
}
// der 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();
}
// Transaktionsmanager
@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 bereits in Abschnitt 11.2.5 behandelt und erläutert. Wir haben dort die folgenden Spring-Annotationen hinzugefügt:
- Zeile 17: Die Annotation [@EnableJpaRepositories] dient dazu, die Pakete anzugeben, in denen sich die Schnittstellen [CrudRepository] und [Spring Data] befinden;
- Zeile 18: Die Klasse ist eine Spring-Konfigurationsklasse. Diese Information ist wichtig. Wenn man sie entfernt, funktioniert das Projekt zwar weiterhin. Später im Dokument jedoch, wenn Projekte erstellt werden, die auf diesem basieren, funktionieren einige davon nicht mehr, wenn die Annotation in Zeile 18 entfernt wird;
- Zeile 19: Die Annotation [@ComponentScan] gibt die Pakete an, in denen sich die Spring-Objekte befinden. Dabei handelt es sich um die mit [@Component, @Service, @Controller, ...] annotierten Klassen. Hier wird die Spring-Komponente [Dao] gefunden und instanziiert;
- Zeilen 73–76: Wir haben eine Bean definiert, die das Array der zu durchsuchenden Pakete darstellt, um Instanzen von JPA zu finden. Dadurch kann ein Projekt, das die Klasse [DaoConfig] importiert, diese Bean neu definieren und so die durchsuchten Pakete ändern (Zeile 59). Weiter unten im Dokument werden wir auf diese Problematik 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 {
// Filter jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// Mapper jSON
ObjectMapper mapper = new ObjectMapper();
// Filter
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// Ergebnis
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// Mapper jSON
ObjectMapper mapper = new ObjectMapper();
// Filter
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// Ergebnis
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// Mapper jSON
ObjectMapper mapper = new ObjectMapper();
// Filter
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// Ergebnis
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// Mapper jSON
ObjectMapper mapper = new ObjectMapper();
// Filter
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// Ergebnis
return mapper;
}
}
- Zeile 11: Die Klasse ist eine Spring-Konfigurationsklasse;
- Zeile 12: Diese importiert die Beans, die von der soeben betrachteten Klasse [DaoConfig] definiert wurden;
- Die Schicht [console] verwendet Mapper jSON, die hier definiert sind;
- Zeilen 14–64: definieren fünf Mapper jSON;
- Zeilen 15–18: Der Filter jSON [jsonMapper] hat keine Filter;
- Zeilen 20–30: Der Filter jSON [jsonMapperCategorieWithProduits] ermöglicht die Serialisierung/Deserialisierung eines Objekts [Categorie] mit seinen Produkten;
- Zeilen 32–42: Der Filter jSON [jsonMapperProduitWithCategorie] ermöglicht die Serialisierung/Deserialisierung eines Objekts [Produit] mit seiner Kategorie;
- Zeilen 43–53: Mit dem Filter jSON [jsonMapperCategorieWithoutProduits] kann ein Objekt [Categorie] ohne seine Produkte serialisiert bzw. deserialisiert werden;
- Zeilen 55–64: Der Filter jSON [jsonMapperProduitWithoutCategorie] ermöglicht die Serialisierung/Deserialisierung eines Objekts [Produit] ohne dessen Kategorie;
Es ist zu beachten, dass beim Erstellen eines Filters jSON für eine Entität T nicht nur der Filter der Entität T, sondern auch die Filter der Entitäten Ti, die diese selbst enthalten kann, konfiguriert werden müssen.
11.3.9. Die Schicht [console]
Ebene
[DAO]
Ebene
[console]
Schicht
[JPA]
Fahrer
[JDBC]
Windel
[Spring Data]
Spring 4
SGBD
![]() |
Die Klasse [Main] lautet wie folgt:
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 {
// Instanzierung des Spring-Kontexts
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);
// --------------------------------------------------------------------------------------
// Die Datenbank wird geleert
log("Vidage de la base de données", 1);
// Die Tabelle [CATEGORIES] wird geleert – die Tabelle [PRODUITS] wird kaskadierend geleert
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// Die Tabellen werden gefüllt
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);
}
// Hinzufügen der Kategorie – durch die Kaskadenwirkung werden auch die Produkte eingefügt
dao.addCategories(categories);
// --------------------------------------------------------------------------------------
log("Affichage de la base", 1);
// Liste der Kategorien
log("Liste des catégories", 2);
affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
// Produktliste
log("Liste des produits", 2);
affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
// Kategorie 1 mit ihren Produkten
Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
log("Catégorie 1 avec ses produits", 2);
affiche(categorie, jsonMapperCategorieWithProduits);
// das Produkt [produit14] mit seiner Kategorie
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);
// Die Tabelle [CATEGORIES] wird geleert – in der Folge wird auch die Tabelle [PRODUITS] geleert
dao.deleteAllCategories();
// Anzeige der Datenbank
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);
// Einfügen
categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// Hinzufügen der Kategorie – durch die Kaskadenwirkung werden auch die Produkte eingefügt
try {
dao.addCategories(Lists.newArrayList(categorie));
} catch (DaoException e) {
System.out.println(e);
}
// Überprüfung
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) {
// Fertig
context.close();
}
}
System.out.println("Travail terminé");
}
// Anzeige eines Elements vom Typ T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// Anzeige einer Liste von Elementen vom Typ 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) {
// Meldung anzeigen
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
- Zeile 25: Instanziierung der Spring-Beans anhand der Konfigurationsklasse [AppConfig];
- Zeilen 26–33: Abruf der Referenzen auf die Mapper jSON. Dabei wird die folgende Signatur der Methode [ApplicationContext].getBean verwendet:
- [ApplicationContext].getBean(String id, Class Klasse): Diese wird verwendet, wenn mehrere Beans vom Typ [classe] vorhanden sind. In diesem Fall wird die Kennung des angeforderten Beans angegeben. Wurde diese mit der Annotation [@Bean] definiert, ist ihre Kennung der Name der annotierten Methode. Wurde sie mit der Annotation [@Bean(« identifiant »] definiert, ist ihre Kennung die in der Annotation angegebene;
- Zeile 34: Abruf einer Referenz auf der Ebene [DAO];
- Zeilen 37–39: Leeren der Datenbank. Die Kategorietabelle wird geleert (Zeile 39). Denn es wurde geschrieben:
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
public Set<Produit> produits = new HashSet<Produit>();
Wenn eine Kategorie gelöscht wird, werden alle damit verknüpften Produkte ebenfalls gelöscht;
- Zeilen 43–53: Befüllen der Tabelle mit zwei Kategorien mit jeweils fünf Produkten. In Zeile 50 führt das Einfügen der beiden Kategorien gleichzeitig zum Einfügen ihrer Produkte, wiederum weil wir [cascade = { CascadeType.ALL }] geschrieben haben;
- Zeile 58: Die Kategorien werden angezeigt. Wir verwenden den Mapper jSON [jsonMapperCategorieWithoutProduits], um die Kategorien ohne ihre Produkte anzuzeigen. Tatsächlich sorgt die Methode [dao.getAllCategories()] dafür, dass die Kategorien ohne ihre Produkte angezeigt werden (Lazy Loading);
- Zeile 61: Die Produkte werden ohne ihre Kategorie angezeigt. Die Methode [dao.getAllProduits()] stellt die Produkte nämlich ohne ihre Kategorie dar (Lazy Loading);
- Zeilen 63–65: Zeigen die Kategorie mit dem Namen [categorie1] zusammen mit ihren Produkten an (Eager Loading);
- Zeilen 67–69: Ein Produkt wird mit seiner Kategorie angezeigt;
- Zeilen 71–81: Alle Preise der Produkte der Kategorie [categorie1] werden um 10 % erhöht;
- Zeilen 91–101: Es wird eine Kategorie mit zwei Produkten gleichen Namens hinzugefügt. In der Tabelle [PRODUITS] besteht jedoch eine Eindeutigkeitsbeschränkung für die Spalte [NOM]. Das Einfügen des zweiten Produkts wird daher abgelehnt und eine Ausnahme ausgelöst. Da die Methode [dao.addProduits] jedoch in einer Transaktion ausgeführt wird, muss das Scheitern des zweiten Einfügungsversuchs auch das Einfügen des ersten Produkts sowie das Einfügen ihrer Kategorie [cat1] rückgängig machen. Genau das soll hier überprüft werden;
- Zeilen 119–121: Eine generische Methode, die die Zeichenkette jSON für jedes Element vom Typ T anzeigen kann. Die Serialisierung jSON wird durch den als Parameter übergebenen Mapper gesteuert;
- Zeilen 124–128: eine analoge 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: Aktualisierung der Preise bestimmter Produkte. In Zeile 24 ist zu sehen, dass die Preise tatsächlich um 10 % gestiegen sind;
- Zeilen 27–36: Hinzufügen der Kategorie [cat1] mit zwei Produkten gleichen Namens. Man sieht, dass die Tabelle vor (Zeilen 28–29) und nach dem Hinzufügen (Zeilen 35–36) identisch ist, was zeigt, dass alle Einfügungen der Transaktion tatsächlich rückgängig gemacht wurden;
- Zeilen 31–34: Die Ausnahme, die beim Einfügen des zweiten Produkts aufgetreten ist und die gesamte Transaktion zum Scheitern gebracht hat;
11.3.10. Der Unit-Test JUnit
![]() |
![]() |
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 {
// Ebene [DAO]
@Autowired
private IDao dao;
// Filter 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() {
// Die Datenbank wird vor jedem Test bereinigt
log("Vidage de la base de données", 1);
// Die Tabelle [CATEGORIES] wird geleert – in der Folge wird auch die Tabelle [PRODUITS] geleert
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// Die Tabellen werden gefüllt
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);
}
// Hinzufügen der Kategorie – durch die Kaskadenwirkung werden auch die Produkte eingefügt
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// Liste der Kategorien
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// Liste der Produkte
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// einige Überprüfungen
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 Methoden
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;
}
// Anzeige eines Elements vom Typ T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// Anzeige einer Liste von Elementen vom Typ 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) {
// Meldung anzeigen
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// Titel
System.out.println(String.format("%s : ", title));
// Meldungen
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
- Zeile 27: Der Unit-Test wird durch die Klasse [AppConfig] konfiguriert, die bereits in Abschnitt 11.3.8 vorgestellt wurde;
- Zeilen 32–33: Einfügen einer Referenz auf die Schicht [DAO];
- Zeilen 36–50: Einfügen der fünf Mapper jSON;
- Zeilen 60–71: Nach dem Leeren der Datenbank (Zeile 57) wird die Datenbank mit zwei Kategorien gefüllt, die jeweils fünf Produkte enthalten. Diese Methode wird aufgrund der Anmerkung [@Before] in Zeile 52 vor jedem Test ausgeführt;
- Zeilen 75–93: Zeigt den Inhalt der Datenbank an;
- Zeilen 95–101: Abfrage einer Kategorie mit ihren Produkten, wobei die Kategorie anhand ihres Namens identifiziert wird;
- Zeilen 103–109: Abfrage einer Kategorie ohne ihre Produkte, wobei die Kategorie anhand ihres Namens identifiziert wird;
- Zeilen 111–120: Abfrage eines Produkts mit seiner Kategorie, wobei das Produkt anhand seiner Nummer identifiziert wird;
- Zeilen 122–130: Abfrage eines Produkts ohne dessen Kategorie, wobei das Produkt anhand seiner Nummer identifiziert wird;
- Zeilen 133–184: private Methoden, die von den verschiedenen Tests gemeinsam genutzt werden;
Zu erledigende Aufgabe: Führen Sie den Test aus. Er muss erfolgreich sein.
11.3.11. Protokollverwaltung
Die Protokolle der Konsolenanwendung bzw. des Tests JUnit werden durch die folgende Datei [logback.xml] konfiguriert:
![]() |
Die Datei muss den Namen [logback.xml] tragen und sich im Classpath des Projekts befinden. Zu diesem Zweck wurde sie hier im Ordner [src/main/resources] abgelegt, der Teil des Classpaths ist. Ihr Inhalt lautet wie folgt:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- Encodern wird standardmäßig der Typ zugewiesen
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Protokollstufe steuern -->
<root level="info"> <!-- Info, Debug, Warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- Zeile 12: Das Tag [<root level="info">] zeigt die Protokolle der Stufe [info] an. Anstelle von [info] kann Folgendes eingegeben werden:
- [debug]: Dies ist die detaillierteste Protokollstufe. Es wird empfohlen, sie während der Debugging-Phase des Projekts zu verwenden, da sie sehr interessante Protokolle zum Datenaustausch zwischen Client und Server enthält. Auf diese Weise lässt sich nachvollziehen, was „unter der Haube“ vor sich geht;
- [off]: überhaupt keine Protokolle;
- [warn]: eine mittlere Protokollstufe, bei der Spring Anomalien anzeigt, die jedoch keine Fehler darstellen. Diese sollten überprüft werden, wenn das erwartete Ergebnis nicht erzielt wird;
Zu erledigende Aufgabe: Ändern Sie die Stufe in Zeile 12 auf [debug] und führen Sie dann den Unit-Test aus. Achten Sie auf den Unterschied in den Logs.
11.3.12. Erstellung des Maven-Archivs des Projekts
Um das Projektarchiv im lokalen Maven-Repository zu installieren, gehen Sie wie folgt vor: [1-3]:
![]() |
Das Archiv wird mit den in der Datei „[pom.xml]“ gefundenen Identifikatoren erstellt:
<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 kann überprüft werden, ob das Maven-Artefakt korrekt installiert wurde:
![]() |
Nun kann ein anderes lokales Maven-Projekt dieses Archiv nutzen.





































