Beispiel: Entity Relationships
Inhalt
Anlegen der Entity Bean "Kuchen"
Anlegen der Entity Bean "Zutat"
Hinzufügen der Relationships
persistence.xml
Anlegen der Session Bean "KuchenZutatWorker"
Anlegen des Webclients
Zeichensatz
Datenbank
Ohne Annotations
Troubleshooting
Für WildFly 30 und JakartaEE 10: Beispiel für zwei Container Managed Entity Bean, auf die per Webclient zugegriffen wird.
Zwischen den beiden Beans besteht eine Container Managed Relationship.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet
man im Stateless-Beispiel): KuchenZutat.ear
Aufbau des Beispieles
a) Entity Bean-Klasse für Kuchen.
b) Entity Bean-Klasse für Zutat.
c) KuchenZutatWorkerBean mit Local-Interfaces.
d) Webclient
Das Beispiel besteht aus einem "EAR Application Project" mit dem Namen "KuchenZutat", einem EJB-Projekt und einem Webprojekt.
Anlegen der Entity Bean "Kuchen"
Es wird eine Enterprise Bean angelegt. Im folgenden geschieht das ohne Verwendung der "Java Persistence"-Facet.
Dazu eine neue Klasse namens "KuchenBean" im Package "de.hsrm.jakartaee.knauf.kuchenzutat"
erstellen. Die Klasse soll das Interface "java.io.Serializable" implementieren.
Jetzt codieren wir die Bean-Klasse. Fett markiert sind Entity-Bean-relevante Elemente:
@Entity()
@NamedQuery(name="findAllKuchen", query="select o from KuchenBean o")
public class KuchenBean implements Serializable
{
private Integer intId;
private String strName;
@Column()
@Id ()
@GeneratedValue ()
public Integer getId()
{
return this.intId;
}
public void setId(Integer int_Id)
{
this.intId = int_Id;
}
@Column()
public String getName()
{
return this.strName;
}
public void setName(String str_Name)
{
this.strName = str_Name;
}
}
Die Bean enthält eine generierte ID sowie ein Feld "Name". Es gibt eine Named Query um
alle Kuchen aus der Datenbank zu holen.
Anlegen der Entity-Bean "Zutat"
Wir fügen eine weitere Java-Klasse "ZutatBean" zu.
Der Code sieht so aus:
@Entity()
public class ZutatBean implements Serializable
{
private Integer intId;
private String strZutatName;
private String strMenge;
public ZutatBean()
{
}
@Column()
@Id ()
@GeneratedValue ()
public Integer getId()
{
return this.intId;
}
public void setId(Integer int_Id)
{
this.intId = int_Id;
}
@Column()
public String getZutatName()
{
return this.strZutatName;
}
public void setZutatName(String str_ZutatName)
{
this.strZutatName = str_ZutatName;
}
@Column()
public String getMenge()
{
return this.strMenge;
}
public void setMenge(String str_Menge)
{
this.strMenge = str_Menge;
}
}
Sie enthält eine generierte ID sowie zwei Felder "Name" und "Menge.
Hinzufügen der Relationships
In "KuchenBean" fügen wir folgendes zu:
private Collection<ZutatBean> collZutaten = new ArrayList<ZutatBean>();
@OneToMany(mappedBy="kuchen", cascade=CascadeType.ALL, fetch=FetchType.EAGER)
public Collection<ZutatBean> getZutaten()
{
return this.collZutaten;
}
public void setZutaten (Collection<ZutatBean> coll_Zutaten)
{
this.collZutaten = coll_Zutaten;
}
Die "OneToMany"-Annotation gibt an welche Property in der ZutatBean das Gegenstück der Relationship
darstellt.
Der CascadeType
gibt an ob Speichern- oder Lösch-Aufrufe auf dem aktuellen Objekt zur Zutat weiterkaskadiert werden.
Der FetchType
schließlich gibt an ob beim Laden einer KuchenBean
direkt die Zutaten geholt werden sollen oder ob diese erst nachträglich geholt werden sollen.
Die ZutatBean erhält das Gegenstück des Mappings:
private KuchenBean kuchen = null;
@ManyToOne ()
public KuchenBean getKuchen()
{
return this.kuchen;
}
public void setKuchen (KuchenBean kuchen)
{
this.kuchen = kuchen;
}
Hier ist außer einer "ManyToOne"-Deklaration nichts weiter zu tun. Zu beachten ist dass ich bei der
Relationship weder CascadeType
noch FetchType
gesetzt habe.
Ein Kaskadieren von Datenänderungen würde in dieser Beispielanwendung keinen Sinn ergeben bzw. wäre sogar falsch:
Ein Löschen des Kuchens beim Löschen der Zutat wäre ein fataler Bug. Und die Anwendungslogik kennt keinen Fall, wo eine Zutat und der zugehörige Kuchen
geändert werden und deshalb ein Speichern kaskadiert werden muss.
Der nicht gesetzte FetchType
hat Auswirkungen in der Session-Bean, denn wir können den Kuchen zur
Zutat dadurch nur abrufen, solange die Bean noch unter Verwaltung des Entity-Managers steht.
Defaults für "fetch":
In der @OneToMany
-Annotation steht der Default-Wert für das Attribut "fetch" auf FetchType.LAZY
.
Im Gegenstück dazu, der @ManyToOne
-Annotation, steht der Default-Wert für das Attribut "fetch" auf FetchType.EAGER
.
Diese Unterschiede müssen beachtet werden, wenn man den FetchType
definiert.
persistence.xml
Für Entity Beans muss eine Persistence Unit deklariert werden. Dies geschieht über eine Datei "persistence.xml" im Unterverzeichnis
"META-INF" des EJB-Projekts.
Sie hat diesen Inhalt:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="kuchenZutatPersistenceUnit">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<properties>
<!-- Setzen dieser Property aktiviert das automatische Tabellen-Generieren und Löschen beim Deploy! -->
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<!-- SQL-Logging einschalten: -->
<property name="hibernate.show_sql" value="true"></property>
</properties>
</persistence-unit>
</persistence>
Es wird eine "persistence-unit" namens "kuchenZutatPersistenceUnit" deklariert. Sie ist verbunden mit einer JDBC-Datenquelle des
Servers die im JNDI unter dem Namen "java:jboss/datasources/ExampleDS" abgelegt ist und auf die JBoss-interne H2-Datenbank zeigt.
Die Property "hibernate.hbm2ddl.auto" ist JBoss-spezifisch und legt fest dass Datenbanktabelle beim Deploy einer Bean erzeugt
und beim Undeploy wieder gelöscht werden sollen. Ohne diesen Parameter müssten wir die Datenbanktabellen von Hand anlegen.
"hibernate.show_sql" sorgt dafür dass die erzeugten SQL-Statements im Log und auf der Server-Konsole angezeigt werden, sinnvoll für die Fehlersuche.
Anlegen der Session Bean "KuchenZutatWorker"
Es wird eine SessionBean "KuchenZutatWorkerBean" sowie ein Local Interface "KuchenZutatWorkerLocal" zugefügt, die diese Methoden enthalten:
@Stateless
public class KuchenZutatWorkerBean
{
@PersistenceContext(unitName="kuchenZutatPersistenceUnit")
private EntityManager entityManager = null;
public void saveKuchen (KuchenBean kuchen)
{
this.entityManager.merge(kuchen);
}
public List<KuchenBean> getKuchen()
{
Query query = this.entityManager.createNamedQuery("findAllKuchen");
List<KuchenBean> listKuchen = query.getResultList();
return listKuchen;
}
public KuchenBean findKuchenById(Integer int_Id)
{
KuchenBean kuchen = this.entityManager.find(KuchenBean.class, int_Id);
//"find" liefert NULL zurück. Deshalb Exception werfen.
if (kuchen == null)
{
throw new EntityNotFoundException("Kein Kuchen zur ID " + int_Id + " gefunden");
}
return kuchen;
}
public void deleteKuchen (KuchenBean kuchen)
{
//Zuerst einmal den Kuchen im EntityManager holen:
//Hier kann "getReference" verwendet werden, da er nicht detached wird.
//Das hat den Vorteil, dass eine Exception geworfen wird.
kuchen = this.entityManager.getReference (KuchenBean.class, kuchen.getId() );
this.entityManager.remove(kuchen);
}
public ZutatBean findZutatById(Integer int_Id)
{
ZutatBean zutat = this.entityManager.find(ZutatBean.class, int_Id);
//"find" liefert NULL zurück. Deshalb Exception werfen.
if (zutat == null)
{
throw new EntityNotFoundException("Keine Zutat zur ID " + int_Id + " gefunden");
}
//Den Kuchen abzurufen solange die Bean unter Container-Verwaltung steht,
//ist nicht nötig, da FetchType im "OneToMany" per Default auf "EAGER" steht!
//zutat.getKuchen();
return zutat;
}
public void deleteZutat (ZutatBean zutat)
{
//Zuerst einmal die Zutat im EntityManager holen:
//Hier kann "getReference" verwendet werden, da sie nicht detached wird.
//Das hat den Vorteil, dass eine Exception geworfen wird.
zutat = this.entityManager.getReference (ZutatBean.class, zutat.getId() );
//Jetzt wird es knifflig: wir müssen den Kuchen der Zutat holen,
//und aus der Zutat-Collection des Kuchen die Zutat entfernen.
//Grund scheint zu sein dass der EntityManager ansonsten noch die
//Zutat im Kuchen hält und dabei in einen Fehlerzustand läuft.
KuchenBean kuchenbean = zutat.getKuchen();
//Zutat wegwerfen:
kuchenbean.getZutaten().remove(zutat);
//Ein Speichern des Kuchens ist nicht nötig (bzw. bewirkt in
//unserem Falle nichtS)
//this.entityManager.merge(zutat);
//Jetzt endlich dürfen wir die Zutat löschen.
this.entityManager.remove(zutat);
}
public void saveZutat (ZutatBean zutat)
{
this.entityManager.merge(zutat);
}
}
Besonders interessant ist hier "deleteZutat", denn ein "entitymanager.remove(zutat)" alleine führte bei mir zu Fehlern,
erst das vorherige Entfernen aus dem Kuchen verschaffte Abhilfe (siehe Abschnitt Troubleshooting).
"find" versus "getReference":
Der EntityManager bietet zwei Methoden, um Entities zu laden:
find
liefert NULL zurück, wenn nichts gefunden wurde.
getReference
wirft eine javax.persistence.EntityNotFoundException
, wenn nichts gefunden wurde.
Leider hat
getReference
einen Nachteil: sie lädt keine abhängigen Objekte, für die das Attribut "fetch" auf
FetchType.EAGER
steht.
D.h. man kann sie nicht verwenden, wenn man z.B. in
findKuchenById
ein Objekt zurückgeben will, dessen Abhängigkeiten
automatisch mitgeladen werden sollen.
Trotzdem hat sie ihre Daseinsberechtigung, nämlich dann, wenn man explizit
nicht die Abhängigkeiten laden möchte, unabhängig vom definierten "FetchType".
Anlegen des Webclients
Der Webclient muss die EJB-JARs referenzieren. Dazu in die Eigenschaften des Webmoduls "KuchenZutatWeb"
wechseln und unter "Deployment Assembly" auf dem Karteireiter "Manifest Entries" das EJB-Projekt hinzufügen.
EJB-Verweise festlegen:
In "KuchenZutatWeb" -> "WebContent" -> "WEB-INF" -> "web.xml" gehen und folgendes einfügen:
<ejb-local-ref>
<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home>java.lang.Object</local-home>
<local>de.hsrm.jakartaee.knauf.kuchenzutat.KuchenZutatWorkerLocal</local>
</ejb-local-ref>
Außerdem muss die EJB-Referenz in einer Datei "WEB-INF\jboss-web.xml" angegeben werden:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web xmlns="urn:jboss:jakartaee:1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:jboss:jakartaee:1.0 https://www.jboss.org/schema/jbossas/jboss-web_15_0.xsd"
version="15.0">
<context-root>KuchenZutatWeb</context-root>
<ejb-local-ref>
<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
<local-jndi-name>java:global/KuchenZutat/KuchenZutatEJB/KuchenZutatWorkerBean!de.hsrm.jakartaee.knauf.kuchenzutat.KuchenZutatWorkerLocal</local-jndi-name>
</ejb-local-ref>
</jboss-web>
Es müssen vier JSP-Seiten "Kuchen.jsp", "KuchenEdit.jsp", "KuchenZutaten.jsp", "KuchenZutatEdit.jsp" zugefügt werden.
Nach dem Deploy auf den Server ist die Anwendung unter
http://localhost:8080/KuchenZutatWeb/Kuchen.jsp zu erreichen.
Achtung: Die Bedienung ist mehr als hakelig, wenn man von einer Unterseite aus zur Hauptseite ("Kuchen.jsp") zurückgeht
wird man häufig nicht die aktualisierte Kuchen-Liste sehen, in diesem Fall hilft es die Seite im Browser zu aktualisieren (F5).
Zeichensatz
In jeder Anwendung ist die Behandlung von Sonderzeichen ein Problem, aber in Webanwendungen ist der Problemkreis besonders groß.
Java selbst verwendet intern UTF-16. Da dieses im Prinzip alle Zeichen unterstützt, sind hier erstmal keine Probleme zu erwarten.
Allerdings sind Schnittstellen zu anderen Systemen immer ein Problem. Java versucht sein Bestes, die Zeichen aus einem Zeichensatz x in
ein Encoding y zu konvertieren, aber dies ist nicht in allen Fällen möglich, z.B. wenn es im Ziel-Encoding ein Zeichen garnicht gibt.
In diesem Beispiel (JSP-basierte Webanwendung) sind folgende Stellen zu beachten:
- Speichern der Daten in der Datenbank. Das macht uns keine Probleme, weil die H2-Datenbank intern mit Unicode arbeitet:
https://www.h2database.com/html/advanced.html#compatibility
Andere Datenbanken können da durchaus Probleme machen
- Parsen von JSP-Seiten durch den Server: hier können ebenfalls Sonderzeichen enthalten sein, d.h. der Server muss wissen,
in welchem Zeichensatz der Entwickler die Datei geschrieben hat. Dieses Problem entsteht nicht, wenn man Servlets verwendet, da hier
alle hartcodierten HTML-Fragmente im Java-Bytecode und damit im internen Encoding der JVM vorliegen.
- Generieren von Webseiten: hier muss man dem Webserver sagen, in welchem Zeichensatz er die Daten binär zum Browser schicken soll.
- Darstellung von Webseiten: der Browser muss wissen, in welchem Encoding die HTML-Seiten vorliegen, die er vom Server erhält. Er weiß nämlich
nicht, in welchem Zeichensatz der Server die Daten generiert hat.
- Nachdem der Browser Benutzereingaben in Formularfeldern zum Server geschickt hat, muss der Server diese Daten wiederum im korrekten
Zeichensatz interpretieren.
Die Lösung für die einzelnen Probleme:
Problem 2: Dateiformat der JSP-Seite
Dies wird dem Webserver über das "page"-Tag mitgeteilt:
<%@page ... pageEncoding="Cp1252" %>
Dies legt fest, dass die Datei im Windows-Dateiformat (auch als "Windows-1252" bekannt) vorliegt, d.h. beim Einlesen der Datei
sind alle Sonderzeichen gemäß diesem Format zu behandeln.
In Eclipse kann eingestellt werden, in welchem Encoding die JSP editiert und gespeichert wird. Dies geht für alle Seiten
über die Projekteigenschaften, oder pro Einzeldatei. Hier ein Screenshot aus den Projekteigenschaften:
Hier steht es auf dem Default (Windows-Encoding). Das geht solange gut, wie alle beteiligten Entwickler mit europäischen Windows-Systemen
arbeiten. Aber ein Linux/Mac-Entwickler im Team wird vermutlich eine falsch dargestellte Seite sehen. Deshalb muss man darauf achten,
in diesem Fall ein Encoding zu wählen, mit dem alle umgehen können.
Problem 3: Generieren der Webseite
Dies geschieht ebenfalls über das "page"-Tag:
<%@page contentType="text/html; charset=UTF-8" ... %>
Hier sollen also alle Zeichen, die zum Client geschickt werden, in UTF-8 vorliegen.
Dies führt nicht nur dazu, dass die Daten im gewünschten Format auf dem Server in den Outputsteam geschrieben werden, sondern
die es wird auch ein HTTP-Header generiert, der für den Client-Browser eine Information über den vorliegenden Zeichensatz ist.
Der folgende Screenshot ist mit der Firefox-Webentwicklerwerkzeugen entstanden:
Problem 4: Darstellung von Webseiten
Dem Client-Browser muss gesagt werden, in welchem Format die Daten vorliegen. Aktuelle Browser nutzen dafür wohl (auch) die HTTP-Header.
Meiner Erfahrung nach sollte man dies zusätzlich in einem Meta-Tag im Page-Header deklarieren:
<HEAD>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<TITLE>...</TITLE>
</HEAD>
<BODY>
...
Hier sieht man das Encoding einer Seite in den Firefox-Seiteneigenschaften:
Problem 5: Formulareingaben auf dem Server auswerten
BEVOR man das erste Mal in der Zielseite auf den HttpRequest zugreift, muss das Encoding gesetzt werden:
request.setCharacterEncoding ("UTF-8");
Erst danach darf man auf Request-Parameter zugreifen:
String kuchenName = request.getParameter("name");
Greift man schon vorher auf den Request zu, dann wird intern das Default-Encoding gesetzt, und ein folgender "setCharacterEncoding"-Aufruf
wird nichts bewirken.
Bei WildFly 30 ist das Default-Encoding übrigens "ISO-8859-1". Das heißt dass keine Umlaute funktionieren, und auch das Eurozeichen fehlt.
Für die Probleme 3, 4 und 5 muss das gleiche Encoding verwendet werden! Ich empfehle, nach Möglichkeit immer UTF-8 oder eines
der anderen Unicode-Encodings zu verwenden.
Datenbank
In der Datenbank sieht das so aus:
"Kuchen"-Tabelle:
"Zutat"-Tabelle:
Besonders zu beachten ist die automatisch generierte Spalte "KUCHEN_ID" mit dem Foreign Key-Feld zum Kuchen.
Ohne Annotations
"ejb-jar.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/ejb-jar_4_0.xsd"
version="4.0">
<display-name>KuchenZutatEJB</display-name>
<enterprise-beans>
<session>
<description>
<![CDATA[Stateless Session Bean für das Arbeiten mit Kuchen und Zutaten.]]>
</description>
<display-name>KuchenZutatWorkerBean</display-name>
<ejb-name>KuchenZutatWorkerBean</ejb-name>
<local>de.hsrm.jakartaee.knauf.kuchenzutat.KuchenZutatWorkerLocal</local>
<ejb-class>de.hsrm.jakartaee.knauf.kuchenzutat.KuchenZutatWorkerBean</ejb-class>
<session-type>Stateless</session-type>
<!--EntityManager-Injection -->
<persistence-context-ref>
<persistence-context-ref-name>KuchenZutatPersistenceUnitRef</persistence-context-ref-name>
<persistence-unit-name>kuchenZutatPersistenceUnit</persistence-unit-name>
<injection-target>
<injection-target-class>
de.hsrm.jakartaee.knauf.kuchenzutatt.KuchenZutatWorkerBean
</injection-target-class>
<injection-target-name>entityManager</injection-target-name>
</injection-target>
</persistence-context-ref>
</session>
</enterprise-beans>
</ejb-jar>
Es gibt keine Neuerungen im Vergleich zum KuchenSimple-Beispiel.
"orm.xml" enthält die Angaben über das Mapping:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm https://jakarta.ee/xml/ns/persistence/orm/orm_3_1.xsd"
version="3.1">
<named-query name="findAllKuchen">
<query>select o from KuchenBean o</query>
</named-query>
<entity class="de.hsrm.jakartaee.knauf.kuchenzutat.KuchenBean" access="PROPERTY"
metadata-complete="true">
<attributes>
<id name="id">
<generated-value />
</id>
<basic name="name">
</basic>
<one-to-many name="zutaten" mapped-by="kuchen" fetch="EAGER"
target-entity="de.hsrm.jakartaee.knauf.kuchenzutat.ZutatBean">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
</attributes>
</entity>
<entity class="de.hsrm.jakartaee.knauf.kuchenzutat.ZutatBean" access="PROPERTY"
metadata-complete="true">
<attributes>
<id name="id">
<generated-value />
</id>
<basic name="zutatName">
</basic>
<basic name="menge">
</basic>
<many-to-one name="kuchen"
target-entity="de.hsrm.jakartaee.knauf.kuchenzutat.KuchenBean">
</many-to-one>
</attributes>
</entity>
</entity-mappings>
Die XML-Elemente für das Mapping entsprechen weitgehend denen der Annotation. Einzige Besonderheit
ist die Definition der "target-entity", also der Entity die das Ziel der Relationship ist.
Die modifizierte Version des Projekts gibt es hier: KucheZutatNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenZutat-Beispiel existieren !
Troubleshooting
Hier werden ein paar Fehler beschrieben in die ich beim Programmieren gelaufen bin ;-)
- In KuchenBean sieht die Methode "getZutaten" so aus (es wird der Default-FetchType "Lazy" verwendet):
@OneToMany(mappedBy="kuchen", cascade=CascadeType.ALL)
public Collection<ZutatBean> getZutaten()
{
...
Beim Abrufen der Zutatenliste eines Kuchens (nach dem Aufruf von "KuchenZutatWorkerBean.findKuchenById") gibt es diese Fehlermeldung
auf der JSP-Seite "KuchenEdit.jsp" (beim Bearbeiten eines Kuchens):
...
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.hsrm.jakartaee.knauf.kuchenzutat.KuchenBean.zutaten: could not initialize proxy - no Session
at org.hibernate@6.2.13.Final//org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:635)
at org.hibernate@6.2.13.Final//org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
at org.hibernate@6.2.13.Final//org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:615)
at org.hibernate@6.2.13.Final//org.hibernate.collection.spi.AbstractPersistentCollection.read(AbstractPersistentCollection.java:136)
at org.hibernate@6.2.13.Final//org.hibernate.collection.spi.PersistentBag.iterator(PersistentBag.java:371)
at org.apache.jsp.KuchenEdit_jsp._jspService(KuchenEdit_jsp.java:169)
at io.undertow.jsp@2.2.6.Final//org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
at io.undertow.jsp@2.2.6.Final//org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:422)
... 47 more
Lösung 1: FetchType auf FetchType.EAGER
setzen. Dies kann allerdings in manchen Fällen nicht sinnvoll sein, z.B. wenn
viele Objekte miteinander verkettet sind und man dadurch die halbe Datenbank einlesen würde ;-).
Lösung 2: Bevor die KuchenBean detached wird (also nach dem Ende von "KuchenZutatWorkerBean.findKuchenById") werden in einer Schleife explizit
die Zutaten abgerufen:
Collection collZutaten = kuchen.getZutaten();
logger.info ("Anzahl Zutaten: " + collZutaten.size());
Wichtig hierbei ist dass wir irgendetwas mit der Zutaten-Collection anstellen damit der Container
die auch wirklich abruft. Im Beispiel reicht es die size()
-Methode für eine Logausgabe abzurufen.
- In KuchenBean sieht die Methode "getZutaten" so aus (es ist kein
CascadeType
gesetzt):
@OneToMany(mappedBy="kuchen", fetch=FetchType.EAGER)
public Collection<ZutatBean> getZutaten()
{
...
Das führt beim Aufruf von "KuchenZutatWorkerBean.saveKuchen(kuchen)" für einen Kuchen mit neuangelegten Zutaten dazu, dass die Zutaten einfach nicht gespeichert werden.
Einfachste Lösung ist es den CascadeType auf CascadeType.ALL
zu stellen, wobei das nicht in allen Fällen sinnvoll ist. Zum Beispiel ist ein kaskadierendes Löschen nicht immer gewünscht.
- Ebenfalls keine Exception aber trotzdem falsch ;-):
In KuchenBean sieht die Methode "getZutaten" so aus (mappedBy
-Attribut nicht gesetzt):
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
public Collection<ZutatBean> getZutaten()
{
...
Die Anwendung scheint zu funktionieren, allerdings wird eine n:m-Mapping-Tabelle "KUCHENBEAN_ZUTATBEAN" angelegt.
Die zusätzliche Spalte "KUCHEN_ID" in der Zutaten-Tabelle ist ebenfalls vorhanden und wird vom Container gepflegt.
- "KuchenZutatWorkerBean.deleteZutat" enthält nicht den Code aus obigem Beispiel sondern sieht so aus:
public void deleteZutat (ZutatBean zutat)
{
zutat = this.entityManager.find (ZutatBean.class, zutat.getId() );
this.entityManager.remove(zutat);
}
Hier soll also eine Zutat direkt gelöscht werden, statt sie über den zugehörigen Kuchen zu löschen.
In früheren WildFly-Versionen führte dies zu einer Exception, aber in WildFly 30 sieht es so aus (erkennbar am über "persistence.xml" eingeschalteten SQL-Logging), als würde Hibernate ein
SELECT ausführen, das ermittelt ob es Kuchen gibt, die diese Zutat haben, und in diesem Fall einfach die Löschung ohne Fehler ignorieren.
- Dieser Fehler tritt nicht in meinem Beispiel auf (die Info dazu stammt noch aus dem Jahr 2013), kann aber auftreten, wenn eine Klasse mit mehr als einer Relation und
FetchType.EAGER
verwendet wird:
17:13:07,921 ERROR [AbstractKernelController] Error installing to Start: name=persistence.unit:unitName=....ear/...Client.jar#...EJB state=Create
javax.persistence.PersistenceException: [PersistenceUnit: ...EJB] Unable to build EntityManagerFactory
at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:677)
at org.hibernate.ejb.HibernatePersistence.createContainerEntityManagerFactory(HibernatePersistence.java:132)
at org.jboss.jpa.deployment.PersistenceUnitDeployment.start(PersistenceUnitDeployment.java:311)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.jboss.reflect.plugins.introspection.ReflectionUtils.invoke(ReflectionUtils.java:59)
at org.jboss.reflect.plugins.introspection.ReflectMethodInfoImpl.invoke(ReflectMethodInfoImpl.java:150)
...
at java.lang.Thread.run(Unknown Source)
Caused by: org.hibernate.HibernateException: cannot simultaneously fetch multiple bags
at org.hibernate.loader.BasicLoader.postInstantiate(BasicLoader.java:89)
at org.hibernate.loader.entity.EntityLoader.<init>(EntityLoader.java:98)
at org.hibernate.loader.entity.EntityLoader.<init>(EntityLoader.java:66)
at org.hibernate.loader.entity.EntityLoader.<init>(EntityLoader.java:56)
at org.hibernate.loader.entity.BatchingEntityLoader.createBatchingEntityLoader(BatchingEntityLoader.java:126)
at org.hibernate.persister.entity.AbstractEntityPersister.createEntityLoader(AbstractEntityPersister.java:1775)
at org.hibernate.persister.entity.AbstractEntityPersister.createEntityLoader(AbstractEntityPersister.java:1779)
at org.hibernate.persister.entity.AbstractEntityPersister.createLoaders(AbstractEntityPersister.java:3012)
at org.hibernate.persister.entity.AbstractEntityPersister.postInstantiate(AbstractEntityPersister.java:3005)
at org.hibernate.persister.entity.SingleTableEntityPersister.postInstantiate(SingleTableEntityPersister.java:712)
at org.hibernate.impl.SessionFactoryImpl.<init>(SessionFactoryImpl.java:322)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1327)
at org.hibernate.cfg.AnnotationConfiguration.buildSessionFactory(AnnotationConfiguration.java:867)
at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:669)
... 100 more
Erklärung und Lösungsansatz findet man hier: http://blog.eyallupu.com/2010/06/hibernate-exception-simultaneously.html
Zusammenfassung: zuviele FetchType.EAGER
führen zu dieser Meldung. Umgehung: entweder auf FetchType.LAZY
wechseln, wo möglich,
oder die Collections der Relationships über java.util.Set
statt java.util.List
abbilden. Letzteres ist sowieso in den meisten UseCases
unpassend, da eine List
mehrfaches Vorkommen des gleichen Items zuläßt, und das wäre wohl in den meisten Fällen eher falsch.
Stand 15.01.2024
Historie:
15.01.2024: Aus JBoss7-Beispiel erstellt, Anpassungen an WildFly 30 und JakartaEE10