Dieser Beitrag ist Teil einer (Tutorial-) Serie über die Einführung in das Spring Framework und beschreibt den Einsatz von JPA (Datenbankzugriff) und Hibernate (OR-Mapper) mit dem Spring Framework.
Die Struktur des Projektes
Dieses Beispiel baut auf dem vorherigem Beispiel Spring an einem einfachem Beispiel in der Version 2.0.x auf. Die verwendeten Frameworks und Werkzeuge sind hier beschrieben. Dieses Beispiel ist eine Konsolenanwendung, die eine Fahrerverwaltung umsetzt. In diesem Beispiel werden die folgenden Technologien des Spring Frameworks vorgestellt:
- Die Definition und die Verwendung von abstrakten und generischen Datenzugriffsobjekten (DAO).
- Die Spezialisierung von Datenzugriffsobjekten für JPA.
- Die Definition von Entitäten mit Relationen, Named Queries und Sequenzen.
- Die Berücksichtigen von Besonderheiten bei Entitäten.
- Das Erstellen einer XML Konfiguration für JPA.
- Das Deklarieren eines Datenzugriffsobjekt im Spring Framework.
- Ein Service im Spring Framework mit Transaktionen versehen.
- Die Datenbank-spezifische Konfiguration für das Spring Framework erzeugen.
- Einen JUnit-Test für Datenbankoperationen mit dem Spring Framework erstellen.
Die folgende Bibliotheken werden benötigt:
Die benötigten Bibliotheken - Dependencies (© Frank Rahn)
Die Literaturempfehlungen für dieses Beispiel
Das abstrakte und generische Datenzugriffsobjekt
Ein DAO (Data Access Object) dient zur Abstrahierung von der konkreten Persistenz-Technologie und entkoppelt die Geschäftslogik vom Datenbankzugriff. Die folgende Schnittstelle eines Datenzugriffsobjektes (DAO) ist noch unabhängig von einer konkreten Zugriffstechnologie.
Diese Schnittstelle definiert die elementarsten Methoden (CRUD) für den Datenzugriff. Als generische Parametern wird die Klasse der Entität und des primären Schlüssels (Primary Key) definiert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
package de.rahn.db.dao; import java.io.Serializable; /** * Ein generische DAO-Schnittstelle für eine Entität mit einem primären Key. * @author Frank W. Rahn * @param <Entity> Die Klasse der Entität * @param <PrimaryKey> Die Klasse des primären Key */ public interface GenericDAO<Entity, PrimaryKey extends Serializable> { /** * Liefere den primären Key für das angegebene Objekt. * @param persistentObject das persistente Objekt * @return den primären Key */ PrimaryKey getPrimaryKey(Entity persistentObject); /** * Speichere das neue Objekt in der Datenbank. * @param newPersistentObject das neue persistente Objekt * @return den primären Key */ PrimaryKey create(Entity newPersistentObject); /** * Aktualisiere das geänderte Objekt in der Datenbank. * @param persistentObject das persistente Objekt */ void save(Entity persistentObject); /** * Lösche das persistente Objekt aus der Datenbank. * @param persistentObject das persistente Objekt */ void remove(Entity persistentObject); /** * Lösche das persistente Objekt aus der Datenbank. * @param key der primäre Key */ void remove(PrimaryKey key); /** * Finde das persistente Objekt an Hand seines primären Keys. * @param key der primäre Key * @return das persistente Objekt */ Entity findByPrimaryKey(PrimaryKey key); } |
Nun folgt eine abstrakte Implementierung dieser Schnittstelle. In dieser Implementierung wurde …
- in Zeile 21 der Logger definiert und
- in Zeile 30 bis 41 im Object-Initializer die Ermittlung der konkreten Klassen der Entität und des primären Schlüssels durchgeführt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
package de.rahn.db.dao; import static org.slf4j.LoggerFactory.getLogger; import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import org.slf4j.Logger; /** * Eine abstrakte Implementierung der Schnittstelle {@link GenericDAO}. * @author Frank W. Rahn * @param <Entity> Die Klasse der Entität * @param <PrimaryKey> Die Klasse des primären Key */ public abstract class AbstractGenericDAO<Entity, PrimaryKey extends Serializable> implements GenericDAO<Entity, PrimaryKey> { /** Der zentrale Logger für die DAO's. */ protected final static Logger logger = getLogger(GenericDAO.class); /** Die Klasse der Entität. */ protected final Class<Entity> entityClass; /** Die Klasse des primären Key. */ protected final Class<PrimaryKey> primaryKeyClass; { ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass(); Type[] actualTypeArguments = type.getActualTypeArguments(); @SuppressWarnings("unchecked") Class<Entity> entityClass = (Class<Entity>) actualTypeArguments[0]; this.entityClass = entityClass; @SuppressWarnings("unchecked") Class<PrimaryKey> primaryKey = (Class<PrimaryKey>) actualTypeArguments[1]; this.primaryKeyClass = primaryKey; } /** * {@inheritDoc} * @see GenericDAO#remove(java.io.Serializable) */ @Override public void remove(PrimaryKey key) { remove(findByPrimaryKey(key)); } } |
Die Spezialisierung des Data Access Objects für JPA
Die grundlegende Schnittstelle eines Datenzugriffsobjektes diesmal abhängig von der Zugriffstechnologie Java Persistence API (JPA).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package de.rahn.db.jpa.dao; import java.io.Serializable; import de.rahn.db.dao.GenericDAO; /** * Eine Erweiterung der Schnittstelle {@link GenericDAO} für JPA. * @author Frank W. Rahn * @param <Entity> Die Klasse der Entität * @param <PrimaryKey> Die Klasse des primären Key * @see de.rahn.db.dao.GenericDAO */ public interface GenericJpaDAO<Entity, PrimaryKey extends Serializable> extends GenericDAO<Entity, PrimaryKey> { // Noch keine spezielle Definitionen. } |
In der folgenden abstrakten Implementierung dieser Schnittstelle wird die JPA Annotationen @PersistenceContext verwendet. Der EntityManager wird dadurch durch das Spring Framework injiziert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
package de.rahn.db.jpa.dao; import java.io.Serializable; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import de.rahn.db.dao.AbstractGenericDAO; import de.rahn.db.dao.GenericDAO; /** * Eine Implementierung der Schnittstelle {@link GenericJpaDAO} für JPA. * @author Frank W. Rahn * @param <Entity> Die Klasse der Entität * @param <PrimaryKey> Die Klasse des primären Key * @see de.rahn.db.dao.AbstractGenericDAO */ public abstract class AbstractGenericJpaDAO<Entity, PrimaryKey extends Serializable> extends AbstractGenericDAO<Entity, PrimaryKey> implements GenericJpaDAO<Entity, PrimaryKey> { @PersistenceContext private EntityManager entityManager; /** * @return Liefert den {@link #entityManager} */ protected final EntityManager getEntityManager() { return entityManager; } /** * {@inheritDoc} * @see GenericDAO#create(java.lang.Object) */ @Override public PrimaryKey create(Entity newPersistentObject) { entityManager.persist(newPersistentObject); return getPrimaryKey(newPersistentObject); } /** * {@inheritDoc} * @see GenericDAO#save(java.lang.Object) */ @Override public void save(Entity persistentObject) { entityManager.merge(persistentObject); } /** * {@inheritDoc} * @see GenericDAO#remove(java.lang.Object) */ @Override public void remove(Entity persistentObject) { entityManager.remove(persistentObject); } /** * {@inheritDoc} * @see GenericDAO#findByPrimaryKey(java.io.Serializable) */ @Override public Entity findByPrimaryKey(PrimaryKey key) { return entityManager.find(entityClass, key); } } |
Im folgendem Klassendiagramm ist der Sachverhalt der abstrakten generischen JPA Datenzugriffsobjekte grafisch per UML dargestellt.
Abstrakte generische Datenzugriffsobjekte für JPA (© Frank Rahn)
Die Definition des Service der Fahrerverwaltung
Zunächst wird die Schnittstelle dieses Services definiert. Auch dieses Beispiel ist relativ einfach gehalten.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
package de.rahn.services.drivers; import java.util.List; import de.rahn.services.drivers.entity.Car; import de.rahn.services.drivers.entity.Driver; /** * Das Interface zum Service Drivers. * @author Frank W. Rahn */ public interface Drivers { /** * Hole alle Fahrer. * @return die Liste der Fahrer */ List<Driver> getDrivers(); /** * Hole einen Fahrer. * @param id die Id eines Fahrers * @return der Fahrer */ Driver getDriver(Long id); /** * Lege einen Fahrer an. * @param name der Name des Fahrers * @param firstname der Vorname des Fahrers * @return die Id des Fahrers */ Long createDriver(String name, String firstname); /** * Lege einen Fahrer an. * @param driver der Fahrer * @return die Id des Fahrers */ Long create(Driver driver); /** * Speichere den Fahrer. * @param driver der Fahrer */ Driver save(Driver driver); /** * Füge ein Auto zum Fahrer hinzu. * @param id die Id des Fahrers * @param car das Auto * @return der komplette Fahrer */ Driver addCarToDriver(Long id, Car car); } |
Die XML Konfiguration für dieses Modul.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd "> <description> Dieses ist die zentrale Konfiguration für den Service Drivers. </description> <!-- Scanne das Package nach Spring Beans --> <context:component-scan base-package="de.rahn.services.drivers" /> </beans> |
Die Entitäten des Services Fahrerverwaltung
Unsere Fahrerverwaltung besteht aus Fahrern mit ihren Autos. Hier werden nun eine Reihe von Annotationen von JPA verwendet, um das Datenmodell zu definieren.
- In den Zeilen 27 bis 29 wird diese Klasse als Entität mit Zugriff auf Attribut-Ebene definiert. Zusätzlich wird das Datenbankschema angegeben. Der Tabellenname rahn.Driver wird aus dem Klassennamen generiert.
- In der Zeile 30 wird eine statische und benannte Abfrage mit JPQL definiert. Mit dieser Abfrage können alle Fahrer selektiert werden. Damit wird sogenanntes SQL-Injection ausgeschlossen, da für diese Abfragen ausschließlich PreparedStatement verwendet werden.
- In den Zeilen 37 bis 41 wird der primäre Schlüssel für diese Klasse definiert. Dabei wird dieser Identifizierer über eine DB-Sequenz automatisch vergeben.
- In der Zeile 54 wird eine gerichtete Relation (1-zu-n) mit weiterreichen der CRUD-Operationen (Cascade) zum Auto definiert.
- In der Zeile 55 wird die Spalte
driver-id in der Tabelle des Autos definiert. Dieses ist notwendig, da die Klasse Auto den Fahrer nicht kennt – also kein Attribut des Fahrers besitzt. Wenn in der Klasse Auto das Attribut
public Driver driver; definiert worden wäre, hatte folgende Anweisung für die Relation ausgereicht:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "driver") .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
package de.rahn.services.drivers.entity; import java.util.HashSet; import java.util.Set; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.Basic; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.Persistence; import javax.persistence.SequenceGenerator; import javax.persistence.Table; /** * Die Klasse eines Fahrers. * @author Frank W. Rahn */ @Entity @Table(schema = "rahn") @Access(AccessType.FIELD) @NamedQueries(@NamedQuery(name = Driver.FIND_ALL, query = "from Driver d")) public class Driver { /** Konstante für die NamedQuery. */ public static final String FIND_ALL = "Driver.findAll"; /** Der Identifizierer des Fahrers. */ @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "DriverSEQ") @SequenceGenerator(name = "DriverSEQ", sequenceName = "DriverSEQ", schema = "rahn") @Basic(optional = false) private Long id; /** Der Name des Fahrers. */ @Basic(optional = false) @Column(nullable = false) private String name; /** Der Vorname des Fahrers. */ @Basic private String firstname; /** Die Autos die der Fahrer fährt. */ @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "driver_id", nullable = false) private Set<Car> cars = new HashSet<>(); ... |
Im folgendem Abschnitt werden die von Eclipse IDE generierten Methoden für hashCode() und equals() dargestellt. Diese Methoden müssen überladen werden, da eine Entität eine eindeutige Identität besitzt, die über die beiden Methoden bestimmt werden muss.
Zwei eigentlich gleiche Entitäten werden als ungleich bewertet, wenn …
- die Entitäten von unterschiedlichen EntityManager gelesen wurden oder
- die eine Entität per Eager Loading (direkt beim Ausführen des Selects) und die andere Entität per Lazy Loading (durch einen Proxy beim Zugriff auf das Feld) geladen wurden.
Der automatische erzeugte Schlüssel ( id) wird erst beim Speichern der Entitäten gesetzt. Die beiden Methoden hashCode() und equals() müssen von denselben signifikanten Attributen abhängen.
- In der Zeile 67 und 99 wird mit Persistence.getPersistenceUtil().isLoaded(cars) geprüft, ob die Autos schon geladen wurden. Wenn nicht würden die Entitäten des Autos geladen oder einen Fehler auslösen, wenn der EntityManager schon geschlossen wurde.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
... // Ab hier von Eclipse generierte Methoden hashCode(), equals(), toString() /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; if (cars != null && Persistence.getPersistenceUtil().isLoaded(cars)) { result = prime * result + cars.hashCode(); } else { result = prime * result; } result = prime * result + (firstname == null ? 0 : firstname.hashCode()); result = prime * result + (id == null ? 0 : id.hashCode()); result = prime * result + (name == null ? 0 : name.hashCode()); return result; } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof Driver)) { return false; } Driver other = (Driver) obj; if (cars == null) { if (other.cars != null) { return false; } } else { if (Persistence.getPersistenceUtil().isLoaded(cars) && Persistence.getPersistenceUtil().isLoaded(other.cars)) { if (!cars.equals(other.cars)) { return false; } } } if (firstname == null) { if (other.firstname != null) { return false; } } else if (!firstname.equals(other.firstname)) { return false; } if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return true; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return new StringBuilder().append("Driver [id=").append(id) .append(", name=").append(name).append(", firstname=") .append(firstname).append("]").toString(); } // Ab hier von Eclipse generierte Setter und Getter ... |
Als nächstes folgt die Klasse für die Autos. Das Auto kennt seinen Fahrer nicht, daher gibt es auch kein Attribut für den Fahrer in der Klasse.
- Der primäre Schlüssel in Zeile 22 wird von Außen vorgegeben (Kennzeichen).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package de.rahn.services.drivers.entity; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.Basic; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; /** * Die Klasse eines Autos. * @author Frank W. Rahn */ @Entity @Table(schema = "rahn") @Access(AccessType.FIELD) public class Car { /** Die Identität eines angemeldeten Autos. */ @Id @Basic(optional = false) private String id; /** Der Typ des Autos. */ @Basic(optional = false) private String type; // Ab hier von Eclipse generierte Methoden hashCode(), equals(), toString(), // Setter und Getter ... |
Es fehlt noch die XML Konfiguration für die Persistenz, die nicht sehr umfangreich ist.
- In der Zeile 11 wird angegeben, dass die Anwendung die Transaktionen der Datenbank verwenden soll:
EntityTransaction etx = entityManager.getTransaction();
Im Gegensatz zu der Angabe JTA, wo der Transaktionsmanager des Java EE Containers verwendet würde:
UserTransaction utx = (UserTransaction) (new InitialContext()).lookup("java:comp/UserTransaction");
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd "> <persistence-unit name="test-spring-drivers" transaction-type="RESOURCE_LOCAL" /> </persistence> |
Das Data Access Object für die Fahrerverwaltung
Die Definition des konkreten Datenzugriffsobjekt für den Service der Fahrerverwaltung.
- In Zeile 16 wird die Annotation @Repository verwendet. Durch diese Annotation wird diese Klasse zu einem Spring Bean mit einer Behandlung für Ausnahmen der Persistenzschicht. Diese Ausnahmen werden in eine Unterklasse von org.springframework.dao.DataAccessException übersetzt.
- Ab der Zeile 32 wird eine Methode findAll() implementiert, die per @NamedQuery definierten Abfrage alle Instanzen dieser Klasse lädt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package de.rahn.services.drivers.dao; import java.util.List; import org.springframework.stereotype.Repository; import de.rahn.db.jpa.dao.AbstractGenericJpaDAO; import de.rahn.services.drivers.entity.Driver; /** * Ein DAO für den Fahrer. * @author Frank W. Rahn * @param <Entity> Die Klasse der Entität * @param <PrimaryKey> Die Klasse des primären Key */ @Repository public class DriverDAO extends AbstractGenericJpaDAO<Driver, Long> { /** * {@inheritDoc} * @see GenericDAO#getPrimaryKey(java.lang.Object) */ @Override public Long getPrimaryKey(Driver persistentObject) { return persistentObject.getId(); } /** * Suche alle Fahrer. * @return alle Fahrer */ public List<Driver> findAll() { return getEntityManager().createNamedQuery(Driver.FIND_ALL, Driver.class).getResultList(); } } |
Die Implementierung des Services der Fahrerverwaltung
Die Standard-Implementierung der Fahrerverwaltung.
- Durch die Annotation @Service("drivers") in Zeile 19 wird diese Implementierung als Spring Bean mit dem Namen drivers definiert.
- In der Zeile 24 wird das Datenzugriffsobjekt DriverDAO verwendet.
- In der Zeile 20 wird definiert, dass alle Methoden in einer Transaktion ausgeführt werden.
- In Zeile 31 und 41 wird durch die Annotation angegeben, dass diese Methoden eine existierende Transaktion unterstützen, aber keine eigene Transaktion benötigen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
package de.rahn.services.drivers.standard; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import de.rahn.services.drivers.Drivers; import de.rahn.services.drivers.dao.DriverDAO; import de.rahn.services.drivers.entity.Car; import de.rahn.services.drivers.entity.Driver; /** * Die Standard-Implementierung des Services {@link Drivers}. * @author Frank W. Rahn */ @Service("drivers") @Transactional public class StandardDrivers implements Drivers { @Autowired private DriverDAO driverDAO; /** * {@inheritDoc} * @see Drivers#getDrivers() */ @Override @Transactional(propagation = Propagation.SUPPORTS) public List<Driver> getDrivers() { return driverDAO.findAll(); } /** * {@inheritDoc} * @see Drivers#getDriver(Long) */ @Override @Transactional(propagation = Propagation.SUPPORTS) public Driver getDriver(Long id) { return driverDAO.findByPrimaryKey(id); } /** * {@inheritDoc} * @see Drivers#createDriver(String, String) */ @Override public Long createDriver(String name, String firstname) { Driver driver = new Driver(); driver.setName(name); driver.setFirstname(firstname); return create(driver); } /** * {@inheritDoc} * @see Drivers#create(Driver) */ @Override public Long create(Driver driver) { return driverDAO.create(driver); } /** * {@inheritDoc} * @see Drivers#save(Driver) */ @Override public Driver save(Driver driver) { driverDAO.save(driver); return driver; } /** * {@inheritDoc} * @see Drivers#addCarToDriver(Long, Car) */ @Override public Driver addCarToDriver(Long id, Car car) { Driver driver = driverDAO.findByPrimaryKey(id); driver.getCars().add(car); return save(driver); } } |
Im folgenden Bild wird der vollständige Service in einem UML-Klassendiagramm dargestellt.
Spring mit JPA und Hibernate (© Frank Rahn)
Die Anwendung mit Logging
Sie ist wie in dem Beispiel Spring an einem einfachem Beispiel aufgebaut. Es ändert sich nur die Anwendung, die XML Konfiguration de/rahn/app/application.xml und der Starter bleiben gleich. Die Änderung an der Application sind hier dargestellt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
package de.rahn.app; import static org.slf4j.LoggerFactory.getLogger; import java.util.List; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import de.rahn.services.drivers.Drivers; import de.rahn.services.drivers.entity.Car; import de.rahn.services.drivers.entity.Driver; /** * Die Anwendung zum Aufrufen der Fahrerverwaltung. * @author Frank W. Rahn */ @Component public class Application implements Runnable { private static final Logger logger = getLogger(Application.class); @Autowired(required = true) private Drivers drivers; /** * {@inheritDoc} * @see Runnable#run() */ @Override public void run() { // Lege einen Fahrer an Long id = drivers.createDriver("Rahn", "Frank"); logger.info("Einen Fahrer mit der Id '{}' angelegt", id); // Hole den Fahrer wieder Driver driver = drivers.getDriver(id); logger.info("Den Fahrer mit der Id '{}' geholt: {}", id, driver); // Lege ein Auto an Car car = new Car(); car.setId("K-XX 4711"); car.setType("Audi A6"); driver = drivers.addCarToDriver(id, car); logger.info("Den Fahrer mit der Id '{}' geändert: {}", id, driver); // Alle Fahrer selektieren List<Driver> listDrivers = drivers.getDrivers(); for (Driver driver2 : listDrivers) { logger.info( "Fahrer: Id '{}' Name: {} {}", new Object[] { driver2.getId(), driver2.getFirstname(), driver2.getName() }); } } } |
Die XML Konfiguration zum Einstieg in die Anwendung muß um die Datenbank- und die Transaktionsdefinitionen erweitert werden.
- In der Zeile 27 wird auf die Konfiguration mit den Datenbank- und die Transaktionsdefinitionen im gleichen Verzeichnis verwiesen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd "> <description> Dieses ist die zentrale Konfiguration für die Anwendungen. </description> <!-- Enabling des AspectJ Support --> <aop:aspectj-autoproxy proxy-target-class="true" /> <!-- Das Verwenden von allgemeinen Annotationen ermöglichen --> <context:annotation-config /> <!-- Die projektspezifischen Konfigurationen laden --> <import resource="db.xml" /> <import resource="classpath:/de/rahn/services/drivers/drivers.xml" /> <import resource="classpath:/de/rahn/app/application.xml" /> </beans> |
In der folgenden XML Konfiguration werden die Datenbank- und die Transaktionsdefinitionen vorgenommen. Hier wird erstmalig der p-Namesraum für das Setzen von Properties verwendet.
Die Syntax lautet dabei:
p:<Property-Name>="<Property-Wert>" oder
p:<Property-Name>-ref="<Name der referenzierten Bean>"
- Die Definitionen für den Datenbankzugriff ( EntityManagerFactory) in den Zeilen 27 bis 34 und die Behandlung der Transaktionen in den Zeilen 37 bis 42.
- In den Zeilen 22 bis 24 wird eine Datenbank gestartet und durch das angegebene Skript init.sql initialisiert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd "> <description> Dieses ist die zentrale Konfiguration für die Datenbank. </description> <!-- Starte die HSQL-Datenbank im Memory --> <jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:script location="classpath:/META-INF/spring/init.sql" /> </jdbc:embedded-database> <!-- Erzeuge die Persitence-Unit --> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" p:dataSource-ref="dataSource" p:persistenceUnitName="test-spring-drivers"> <property name="jpaVendorAdapter"> <1bean p:generateDdl="true" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" /> </property> </bean> <!-- Einen Transaktionmanager erzeugen --> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" /> <!-- Das Verwenden von Annotationen für die Transaktionen ermöglichen --> <tx:annotation-driven proxy-target-class="true" /> </beans> |
Im folgendem Kasten wird das Skript init.sql dargestellt. Der SQL Dialekt ist HSQL (HyperSQL Database). HSQLDB ist eine vollständig in Java geschrieben relationale Datenbank.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
create SCHEMA rahn; create sequence DriverSEQ; create table rahn.Driver ( id bigint not null, firstname varchar(255), name varchar(255) not null, primary key (id)); create table rahn.Car ( id varchar(255) not null, type varchar(255) not null, driver_id bigint not null, primary key (id)); alter table rahn.Car add constraint FK107B45ADEE2FE foreign key (driver_id) references rahn.Driver; insert into rahn.Driver (firstname, name, id) values ('Martin', 'Rahn', next value for DriverSEQ); |
Der Unit Test
Zunächst erstellen wir einen Test für die Klasse DriverDAO.
- In der Zeile 25 wird auf die benötigten XML Konfigurationen von Spring verwiesen. Das ist eine spezielle XML Konfiguration für den Test /META-INF/spring/context-test.xml, sie beinhaltet die Konfigurationen für den Datenbankzugriff, und die Konfiguration für die Fahrerverwaltung ../drivers.xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
package de.rahn.services.drivers.dao; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; import java.io.Serializable; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import de.rahn.services.drivers.entity.Driver; /** * Die Testklasse für {@link DriverDAO}. * @author Frank W. Rahn */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/META-INF/spring/context-test.xml", "../drivers.xml" }) @Transactional public class DriverDAOTest { @Autowired private DriverDAO driverDAO; /** * Test method for {@link DriverDAO#getPrimaryKey(Driver)}. */ @Test public void testGetPrimaryKey() { Driver driver = new Driver(); driver.setId(new Long(4711)); Long id = driverDAO.getPrimaryKey(driver); assertThat("Primärer Key unterschiedlich", driver.getId(), is(id)); } /** * Test method for {@link DriverDAO#findAll()}. */ @Test public void testFindAll() { List<Driver> drivers = driverDAO.findAll(); assertThat("kein Ergebnis geliefert", drivers, notNullValue()); assertThat("Anzahl der Treffer ungleich", drivers.size(), is(1)); Driver driver = drivers.get(0); assertThat("id ungleich", driver.getId(), is(0L)); assertThat("firstname ungleich", driver.getFirstname(), is("Martin")); assertThat("name ungleich", driver.getName(), is("Rahn")); } /** * Test method for * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#create(Object)}. */ @Test public void testCreate() { Driver driver = new Driver(); driver.setName("Rahn"); driver.setFirstname("Frank"); Long id = driverDAO.create(driver); assertThat("keine id geliefert", id, notNullValue()); assertThat("ungleiche id", driver.getId(), is(id)); Driver driver2 = driverDAO.findByPrimaryKey(id); assertThat("doch nicht gespeichert", driver2, notNullValue()); assertThat("ungleicher Fahrer", driver2, sameInstance(driver)); } /** * Test method for * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#save(Object)}. */ @Test public void testSave() { Driver driver = driverDAO.findByPrimaryKey(0L); assertThat("kein Fahrer gefunden", driver, notNullValue()); driver.setFirstname("Peter"); driverDAO.save(driver); Driver driver2 = driverDAO.findByPrimaryKey(0L); assertThat("kein Fahrer gefunden", driver2, notNullValue()); assertThat("ungleicher Fahrer", driver2, sameInstance(driver)); } /** * Test method for * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#remove(Object)}. */ @Test public void testRemoveEntity() { List<Driver> drivers = driverDAO.findAll(); assertThat("Anzahl der Treffer ungleich", drivers.size(), is(1)); Driver driver = drivers.get(0); driverDAO.remove(driver); drivers = driverDAO.findAll(); assertThat("Anzahl der Treffer ungleich", drivers.size(), is(0)); } /** * Test method for * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#findByPrimaryKey(Serializable)} * . */ @Test public void testFindByPrimaryKey() { Driver driver = driverDAO.findByPrimaryKey(0L); assertThat("kein Ergebnis geliefert", driver, notNullValue()); assertThat("id ungleich", driver.getId(), is(0L)); assertThat("firstname ungleich", driver.getFirstname(), is("Martin")); assertThat("name ungleich", driver.getName(), is("Rahn")); } /** * Test method for * {@link de.rahn.db.dao.AbstractGenericDAO#remove(Serializable)}. */ @Test public void testRemovePrimaryKey() { driverDAO.remove(0L); List<Driver> drivers = driverDAO.findAll(); assertThat("kein Ergebnis geliefert", drivers, notNullValue()); assertThat("Anzahl der Treffer ungleich", drivers.size(), is(0)); } } |
Für die allgemeinen Spring Beans wurde eine spezielle XML Konfiguration /META-INF/spring/context-test.xml erstellt. Sie lädt nur die Konfigurationen für den Datenbankzugriff.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd "> <description> Dieses ist die zentrale Konfiguration für die Tests. </description> <!-- Das Verwenden von allgemeinen Annotationen ermöglichen --> <context:annotation-config /> <!-- Die projektspezifischen Konfigurationen laden --> <import resource="db.xml" /> </beans> |
Jetzt fehlt nur noch der Unit Test für den Service der Fahrerverwaltung StandardDriver.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
package de.rahn.services.drivers.standard; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import de.rahn.services.drivers.Drivers; import de.rahn.services.drivers.entity.Car; import de.rahn.services.drivers.entity.Driver; /** * Die Testklasse für {@link StandardDrivers}. * @author Frank W. Rahn */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/META-INF/spring/context-test.xml", "../drivers.xml" }) @Transactional public class StandardDriversTest { /** CAR_TYPE */ private static final String CAR_TYPE = "Audi A6"; /** CAR_ID */ private static final String CAR_ID = "K-XX 4711"; @Autowired private Drivers drivers; /** * Test method for {@link StandardDrivers#getDriver(Long)}. */ @Test public void testGetDriver() { Driver driver = drivers.getDriver(0L); assertThat("kein Fahrer geliefert", driver, notNullValue()); assertThat("id ungleich", driver.getId(), is(0L)); assertThat("firstname ungleich", driver.getFirstname(), is("Martin")); assertThat("name ungleich", driver.getName(), is("Rahn")); } /** * Test method for {@link StandardDrivers#createDriver(String, String)}. */ @Test public void testCreateDriver() { Long id = drivers.createDriver("Rahn", "Frank"); assertThat("keine id geliefert", id, notNullValue()); Driver driver = drivers.getDriver(id); assertThat("doch nicht gespeichert", driver, notNullValue()); assertThat("ungleiche id", driver.getId(), is(id)); assertThat("firstname ungleich", driver.getFirstname(), is("Frank")); assertThat("name ungleich", driver.getName(), is("Rahn")); } /** * Test method for {@link StandardDrivers#addCarToDriver(Long, Car)}. */ @Test public void testAddCarToDriver() { Car car = new Car(); car.setId(CAR_ID); car.setType(CAR_TYPE); Driver driver = drivers.addCarToDriver(0L, car); assertThat("doch nicht gespeichert", driver, notNullValue()); assertThat("id ungleich", driver.getId(), is(0L)); assertThat("firstname ungleich", driver.getFirstname(), is("Martin")); assertThat("name ungleich", driver.getName(), is("Rahn")); assertThat("anzahl Autos ungleich", driver.getCars().isEmpty(), not(true)); car = driver.getCars().iterator().next(); assertThat("id ungleich", car.getId(), is(CAR_ID)); assertThat("type ungleich", car.getType(), is(CAR_TYPE)); } } |
Hier noch die entsprechende Erfolgsmeldung.
Die Erfolgsmeldung von JUnit (© Frank Rahn)
Der Quellcode und Download des Beispiels
Quellcode ansehen bei GitHub:
Spring mit JPA und Hibernate
Download einer ZIP-Datei von GitHub:
Spring mit JPA und Hibernate
Die Maven Befehl
Eclipse Konfiguration neu erzeugen: $ mvn eclipse:clean eclipse:eclipse
Anwendung bauen: $ mvn clean install
Anwendung ausführen: $ mvn initialize exec:exec
Update: Criteria API
Es wurde in der pom.xml ein Maven-Plugin für die Generierung des Metamodels der Criteria API aufgenommen. Dieses Metamodel wird in das Verzeichnis target/generated-sources/apt generiert. Mit der Criteria API können Datenbankabfragen objektorientiert im Quellcode programiert werden.
Nachfolgenden ist die Maven Konfiguration für die Erzeugung der Criteria API mit dem JPA 2 Metamodel Generator von Hibernate dargestellt.
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
... <plugin> <groupId>org.bsc.maven</groupId> <artifactId>maven-processor-plugin</artifactId> <version>22.2.1</version> <executions> <execution> <goals> <goal>process</goal> </goals> </execution> </executions> <configuration> <optionMap> <addGeneratedAnnotation>true</addGeneratedAnnotation> <addSuppressWarningsAnnotation>true</addSuppressWarningsAnnotation> <debug>true</debug> </optionMap> <processors> <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor> </processors> </configuration> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>${hibernate-jpamodelgen-version}</version> </dependency> </dependencies> </plugin> ... |
Zusätzlich wurde ein Template, nach dem Muster von Spring, erzeugt. Dadurch kann das abstrakte und generische JPA-Datenzugriffsobjekt die Erzeugung der Factory-Klassen übernehmen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package de.rahn.db.jpa.dao; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; /** * Ein typisches Spring-Template, dass die Erzeugung einiger notwendigen Factory * Klassen zur Erzeugung von Queries auf Basis der Criteria API an das * {@link AbstractGenericJpaDAO} deligiert. * @author Frank W. Rahn * @param <Entity> Die Klasse der Entität * @see AbstractGenericJpaDAO */ public interface CriteriaQueryTemplate<Entity> { /** * Diese Methode wird durch das {@link AbstractGenericJpaDAO} ausgeführt und * stellt einige Standardkomponenten der Criteria API zu Verfügung. * @param builder der {@link CriteriaBuilder} * @param query die an die Entität gebundene Abfrage * @param rootEntity die Projektionsvariable der FROM-Klausel */ void doBuild(CriteriaBuilder builder, CriteriaQuery<Entity> query, Root<Entity> rootEntity); } |
Das abstrakte und generische JPA-Datenzugriffsobjekt AbstractGenericJpaDAO, wird um die Methode buildQuery() erweitert.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
... /** * Erzeuge über die Criteria API eine JPA Query. * @param template der Builder * @return eine ausführbare JPA Query */ protected TypedQuery<Entity> buildQuery( CriteriaQueryTemplate<Entity> template) { // Erzeuge eine Builder CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); // Erzeuge die Query CriteriaQuery<Entity> criteriaQuery = criteriaBuilder.createQuery(entityClass); // Erzeuge die Referenz zur Hauptentität Root<Entity> rootEntity = criteriaQuery.from(entityClass); // Führe den Build durch template.doBuild(criteriaBuilder, criteriaQuery, rootEntity); // Erzeuge eine ausführbare JPA Query return entityManager.createQuery(criteriaQuery); } ... |
Das Datenzugriffsobjekt der Fahrer stellt die Methode findByName() bereit.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
... /** * Suche alle Fahrer mit dem Namen. * @param name der Name des Fahrers * @return die gefundenen Fahrer */ public List<Driver> findByName(final String name) { return buildQuery(new CriteriaQueryTemplate<Driver>() { @Override public void doBuild(CriteriaBuilder builder, CriteriaQuery<Driver> query, Root<Driver> rootEntity) { // Erzeuge eine logische Ausdruck Predicate predicate = builder.equal(rootEntity.get(Driver_.name), name); // Definiere die Abfrage query.select(rootEntity).where(predicate).distinct(true); } }).getResultList(); } ... |
Zum Abschluss darf natürlich der Test nicht fehlen.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
... /** * Test der Criteria API. */ @Test public void testCriteriaAPI() { List<Driver> drivers = driverDAO.findByName("Rahn"); assertThat("keine Ergebnis geliefert", drivers, notNullValue()); assertThat("Anzahl der Treffer ungleich", drivers.size(), is(1)); Driver driver = drivers.get(0); assertThat("id ungleich", driver.getId(), is(0L)); assertThat("firstname ungleich", driver.getFirstname(), is("Martin")); assertThat("name ungleich", driver.getName(), is("Rahn")); } ... |
Update am 28.09.2012
Es wurden folgende Änderungen an allen Projekten vorgenommen.
- Die Projekte wurden aus meinem lokalen Apache Subversion Repository in meine öffentlichen GitHub Repositories verschoben.
- Projekt test-spring-simple
- Projekt test-spring-jpa
- Projekt test-spring-web
- Die folgenden typischen Anpassungen an Git wurden an allen Projekten durchgeführt.
- .directory gelöscht
- .gitignore hinzugefügt
- README.md hinzugefügt
- COPYRIGHT.md hinzugefügt
- In jedem Repository wurde für jeden Beitrag der Serie ein Branch angelegt.
- develop-spring-an-einem-einfachen-beispiel
- develop-spring-mit-aop
- develop-spring-mit-jpa-und-hibernate
- develop-spring-mit-einer-einfachen-webanwendung
- develop-spring-mit-einer-webanwendung-mit-jpa-und-validierung
- develop-spring-security-mit-einer-webanwendung
- develop-spring-mit-restful-webservice
Updates von 06.10.2012 bis zum 14.10.2012
Es wurden folgende Änderungen an allen Projekten vorgenommen.
- Aktualisierung des OpenJDK auf die Version 1.7.0.
- Aktualisierung der Entwicklungsumgebung Eclipse auf die Version 4.2.1.
Die benötigten Plugins aus dem Eclipse Marketplace: - Aktualisierung der Datei
pom.xml:
- Anpassungen an die OpenJDK Version
- GitHub Einträge (SCM und URL) hinzugefügt
- Aktualisierung der Libraries auf aktuellerer Versionen (z. B. Spring Version 3.1.2.RELEASE, Hibernate Version 4.1.7.Final, JUnit Version 4.10, mockito Version 1.9.0, …)
Die genaueren Versionen bitte aus den jeweiligen pom.xml auf GitHub entnehmen.
Weitere Beiträge dieser Serie. [verstecken]
Frank Rahn
Benötigen Sie Unterstützung? Kontaktieren Sie ihn.
Hat Ihnen dieser Beitrag gefallen? Wir würden uns über Ihren Kommentar freuen! Bitte verwenden Sie Ihren bürgerlichen Namen und eine E-Mail-Adresse mit Gravatar.
Letzte Artikel von Frank Rahn (Alle anzeigen)
- Spring Boot Webanwendung: Die ersten Schritte (Tutorial) - Montag, 28. März 2016
- Wer ist der optimale Java Bean Mapper? - Sonntag, 19. Juli 2015
- Mainframe-Zugriff via Java - Sonntag, 04. Mai 2014
Dein Kommentar
An Diskussion beteiligen?Hinterlasse uns Deinen Kommentar!