Beispiel: Entity Inheritance


Inhalt:

Strategie SINGLE_TABLE
Strategie TABLE_PER_CLASS
Strategie JOINED
Application Client
Start des Clients
"jboss-deployment-structure.xml"
Ohne Annotations

Für WildFly 30 und JakartaEE 10: Beispiel für eine Entity Bean die drei Subklassen mit unterschiedlichen Feldern hat. Diese Vererbung wird mittels der drei im JavaEE-Standard definierten Vererbungsstrategien abgebildet.

Das Beispiel erweitert das Kuchen-Zutat-Beispiel: die Entity "Kuchen" bleibt unverändert (hier: "KuchenInheritanceBean"), aber es gibt eine abstrakte Basisklasse "Zutat" mit drei Subklassen "flüssige Zutat" ("ZutatFluessigkeitInheritanceBean"), "feste Zutat" ("ZutatGewichtInheritanceBean"), und "zählbare Zutat" ("ZutatStueckzahlInheritanceBean"), die jeweils eigene Felder haben.

Als Client wird ein Swing-Application Client verwendet (GUI-Design mit Jigloo erstellt, dürfte aber auch mit "WindowBuilder" noch bearbeitbar sein). Da es sehr viel doppelten Code erfordert hätte, alle drei Strategien in einem einzigen Beispiel abzubilden, wurde nur eine Vererbungs-Variante umgesetzt. Die anderen beiden Varianten unterscheiden sich nur in Annotations und liegen in auskommentierter Form im Code vor.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel): KuchenZutatInheritance.ear

Im Beispiel-EAR ist die Strategie "JOINED" aktiviert. Die anderen beiden Strategien sind auskommentiert.

Aufbau des Beispieles

a) Entity Bean "KuchenInheritance" mit Feldern "id" und "name" sowie einer @OneToMany-Relation zu den Zutaten (wie in den bisherigen Beispielen auch).
b) Abstrakte Entity Bean "ZutatBaseInheritanceBean" mit dem Feld "id" und einer @ManyToOne-Relation zum Kuchen.
c) Entity Bean "ZutatFluessigkeitInheritanceBean" als Subklasse von "ZutatBaseInheritanceBean", die eine flüssige Zutat abbildet. Ihre Felder sind eine Menge und eine Maßeinheit (Enumeration mit den Werten "Liter" und "Milliliter"), also z.B. "200 ml Milch".
d) Entity Bean "ZutatGewichtInheritanceBean" als Subklasse von "ZutatBaseInheritanceBean", die eine wiegbare Zutat abbildet. Ihre Felder sind eine Menge und eine Gewichtseinheit (Enumeration mit den Werten "Gramm" und "Kilogramm"), also z.B. "100 g Zucker".
e) Entity Bean "ZutatStueckzahlInheritanceBean" als Subklasse von "ZutatBaseInheritanceBean", die eine nur abzählbare Zutat abbildet. Ihre Felder sind nur eine Menge, also z.B. "5 Eier".
f) Worker Bean "KuchenZutatInheritanceWorkerBean" mit Methoden zum Speichern, Laden und verwalten von Kuchen und Zutaten.
g) Application Client für das Erfassen von Kuchen und den drei Zutaten-Typen pro Kuchen.


Strategie SINGLE_TABLE

Dies bedeutet: es wird nur eine einzige Tabelle in der Datenbank erzeugt, die so heißt wie die Basisklasse und alle Felder aller Subklassen enthält. Zur Unterscheidung der einzelnen Beans wird eine "Discriminator Column" eingeführt, die pro Datensatz eine Kennung des Typs enthält.

Basisklasse ZutatBaseInheritanceBean:
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@NamedQuery (name="findAllZutaten", query="select o from ZutatBaseInheritanceBean o")
public abstract class ZutatBaseInheritanceBean implements Serializable 
{
  private Integer intId;
  private String strZutatName;
  private KuchenInheritanceBean kuchen = null;
  
  public ZutatBaseInheritanceBean()
  { 
  }
  
  @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;
  }
  
  @ManyToOne ()
  public KuchenInheritanceBean getKuchen()
  {
    return this.kuchen;
  }

  public void setKuchen (KuchenInheritanceBean kuchen)
  {
    this.kuchen = kuchen;
  }
} 
Die Strategie wird über die Annotation @Inheritance definiert, als Typ haben wir hier InheritanceType.SINGLE_TABLE gewählt. Hier könnten Name und Typ der Discriminator Column über eine Annotation @DiscriminatorColumnangegeben werden, ich belasse das hier beim Default (resultiert in einer Spalte "DTYPE" in der Datenbank).
Die Named Query dient dazu alle Zutaten inklusive aller Subtypen zu laden, also den gesamten Tabelleninhalt.

Subklasse ZutatFluessigkeitInheritanceBean:
@Entity
@DiscriminatorValue(value="fluessigkeit")
@Entity
@NamedQuery (name="findAllZutatenFluessigkeit", query="select o from ZutatFluessigkeitInheritanceBean o")
public class ZutatFluessigkeitInheritanceBean extends ZutatBaseInheritanceBean
{
  private Integer intMasseinheitMenge;
  private Masseinheit masseinheit;

  @Column()
  public Integer getMasseinheitMenge()
  {
    return this.intMasseinheitMenge;
  }

  public void setMasseinheitMenge(Integer int_MasseinheitMenge)
  {
    this.intMasseinheitMenge = int_MasseinheitMenge;
  }
  
  @Column()
  @Enumerated(value=EnumType.STRING)
  public Masseinheit getMasseinheit()
  {
    return this.masseinheit;
  }

  public void setMasseinheit(Masseinheit _masseinheit)
  {
    this.masseinheit = _masseinheit;
  }
  
  @Override
  public String toString()
  {
    return this.getZutatName() + " (" + this.intMasseinheitMenge + " " + this.masseinheit + ")";
  }
}
Hier wird zur Erkennung von Instanzen dieser Klasse in der Datenbank ein Wert der Discriminator Column mittels der Annotation @DiscriminatorValue eingetragen: alle Käsekuchen sollen hier den Wert "fluessigkeit" stehen haben.
Die Named Query liefert NUR flüssige Zutaten zurück, alle anderen Typen werden nicht berücksichtigt.

Behandlung von Enums:
Die Property "Masseinheit" ist eine Enum, die so aussieht:
public enum Masseinheit
{
  Liter,
  Milliliter
}
Per Default werden Enum-Spalten mit ihren Integer-Werten in der Datenbank gespeichert. Da ich die Anzeigenamen der einzelnen Werte schöner finde, habe ich die Enum-Speicherungsstrategie an der Property deklariert:
  @Enumerated(value=EnumType.STRING)
  public Masseinheit getMasseinheit()
  {
    ...
Default ist EnumType.ORDINAL.


Ähnlich wie die "ZutatFluessigkeitInheritanceBean" sieht die Subklasse ZutatGewichtInheritanceBean aus:
@Entity
@DiscriminatorValue(value="gewicht")
@NamedQuery (name="findAllZutatenGewicht", query="select o from ZutatGewichtInheritanceBean o")
public class ZutatGewichtInheritanceBean extends ZutatBaseInheritanceBean
{
  private Integer intGewicht;
  private Gewicht gewichtEinheit;

  @Column()
  public Integer getGewicht()
  {
    return this.intGewicht;
  }

  public void setGewicht(Integer int_Gewicht)
  {
    this.intGewicht = int_Gewicht;
  }
  
  public void setGewichtEinheit(Gewicht _gewichtEinheit)
  {
    this.gewichtEinheit = _gewichtEinheit;
  }
  
  @Column()
  @Enumerated(value=EnumType.STRING)
  public Gewicht getGewichtEinheit()
  {
    return this.gewichtEinheit;
  }

  @Override
  public String toString()
  {
    return this.getZutatName() + " (" + this.intGewicht + " " + this.gewichtEinheit + ")";
  }
} 
Auch hier wird ein Discriminator Value gesetzt, und es wird eine spezielle Named Query deklariert die nur abzuwiegende Zutaten zurückliefert.


Zum Schluss noch die ZutatStueckzahlInheritanceBean:
@Entity
@DiscriminatorValue(value="stueckzahl")
@NamedQuery (name="findAllZutatenStueckzahl", query="select o from ZutatStueckzahlInheritanceBean o")
public class ZutatStueckzahlInheritanceBean extends ZutatBaseInheritanceBean
{
  private Integer intAnzahl;

  @Column()
  public Integer getAnzahl()
  {
    return this.intAnzahl;
  }

  public void setAnzahl(Integer int_Anzahl)
  {
    this.intAnzahl = int_Anzahl;
  }
  
  @Override
  public String toString()
  {
    return this.getZutatName() + " (" + this.intAnzahl + " Stück)";
  }
}
Der Discriminator Value wird auf "stueckzahl" gesetzt, und es wird eine spezielle Named Query deklariert die nur zählbare Zutaten zurückliefert.


Das Ergebnis in der Datenbank sieht so aus:
SINGLE_TABLE
Der EntityManager nutzt folgende Query, um alle Zutaten (also alle drei Subtypen) zu laden:
select z1_0.id,z1_0.DTYPE,z1_0.kuchen_id,z1_0.zutatName,z1_0.masseinheit,z1_0.masseinheitMenge,z1_0.gewicht,z1_0.gewichtEinheit,z1_0.anzahl from ZutatBaseInheritanceBean z1_0

Strategie TABLE_PER_CLASS

Dies bedeutet: es wird pro echter Bean-Klasse (abstrakte Klassen ausgenommen!) eine Tabelle in der Datenbank erzeugt, die alle Felder der jeweiligen Subklasse sowie aller Parentklassen enthält.

Basisklasse ZutatBaseInheritanceBean:
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
@NamedQuery (name="findAllZutaten", query="select o from ZutatBaseInheritanceBean o")
public abstract class ZutatBaseInheritanceBean implements Serializable
{
  ...
} 
Der Rest des Codes ist identisch mit dem vorherigen Beispiel (abgesehen von den DiscriminatorValue-Annotations, die hier in den Subklassen natürlich unsinnig wären.

Die drei Subklassen ZutatFluessigkeitInheritanceBean, ZutatGewichtInheritanceBean und ZutatStueckzahlInheritanceBean unterscheiden sich nicht von den Beans aus dem vorherigen Beispiel (abgesehen davon dass hier die Annotation @DiscriminatorValue natürlich nicht verwendet werden darf) und werden hier nicht nochmal wiederholt.

Das Ergebnis in der Datenbank sieht so aus:
TABLE_PER_CLASS
Für die abstrakte Basisklasse KuchenTablePerClassBaseBean wurde keine Tabelle erzeugt weil diese abstrakt ist.

Der EntityManager nutzt folgende Query, um alle Zutaten (also alle drei Subtypen) zu laden:
select z1_0.id,z1_0.clazz_,z1_0.kuchen_id,z1_0.zutatName,z1_0.masseinheit,z1_0.masseinheitMenge,z1_0.gewicht,z1_0.gewichtEinheit,z1_0.anzahl from ( select gewicht, id, kuchen_id, gewichtEinheit, zutatName, null as masseinheitMenge, null as masseinheit, null as anzahl, 1 as clazz_ from ZutatGewichtInheritanceBean union all select null as gewicht, id, kuchen_id, null as gewichtEinheit, zutatName, masseinheitMenge, masseinheit, null as anzahl, 2 as clazz_ from ZutatFluessigkeitInheritanceBean union all select null as gewicht, id, kuchen_id, null as gewichtEinheit, zutatName, null as masseinheitMenge, null as masseinheit, anzahl, 3 as clazz_ from ZutatStueckzahlInheritanceBean ) z1_0

Hinweis
Der JPA-Validator spuckte mir eine Warnmeldung aus:
Entity "ZutatBaseInheritanceBean" uses table-per-concrete-class inheritance which is not portable and may not be supported by the JPA provider

Aus dem JavaEE6-Tutorial unter
http://docs.oracle.com/javaee/6/tutorial/doc/bnbqn.html
The Table per Concrete Class Strategy
In this strategy, which corresponds to InheritanceType.TABLE_PER_CLASS, each concrete class is mapped to a separate table in the database. All fields or properties in the class, including inherited fields or properties, are mapped to columns in the class’s table in the database.

This strategy provides poor support for polymorphic relationships and usually requires either SQL UNION queries or separate SQL queries for each subclass for queries that cover the entire entity class hierarchy.

Support for this strategy is optional and may not be supported by all Java Persistence API providers. The default Java Persistence API provider in the GlassFish Server does not support this strategy.


Wir haben allerdings Glück, weil Hibernate diese Strategie unterstützt.

Strategie JOINED

Dies bedeutet: es wird pro Bean-Klasse (einschließlich abstrakter Klassen) eine Tabelle in der Datenbank erzeugt, die alle Felder der jeweiligen Klasse enthält.

Basisklasse KuchenJoinedBaseBean:
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@NamedQuery (name="findAllZutaten", query="select o from ZutatBaseInheritanceBean o")
public abstract class ZutatBaseInheritanceBean implements Serializable
{
  ...
} 
Der Rest des Codes ist identisch mit dem Beispiel "SINGLE_TABLE".

Die drei Subklassen ZutatFluessigkeitInheritanceBean, ZutatGewichtInheritanceBean und ZutatStueckzahlInheritanceBean unterscheiden sich nicht von den Beans aus dem vorherigen Beispiel (abgesehen davon dass hier die Annotation @DiscriminatorValue natürlich nicht verwendet werden darf) und werden hier nicht nochmal wiederholt.

Das Ergebnis in der Datenbank sieht so aus:
JOINED

Der EntityManager nutzt folgende Query, um alle Zutaten (also alle drei Subtypen) zu laden:
select zutatbasei0_.id as id86_, zutatbasei0_.kuchen_id as kuchen3_86_, zutatbasei0_.zutatName as zutatName86_,
zutatbasei0_1_.gewicht as gewicht87_, zutatbasei0_1_.gewichtEinheit as gewichtE2_87_,
zutatbasei0_2_.masseinheit as masseinh1_88_, zutatbasei0_2_.masseinheitMenge as masseinh2_88_,
zutatbasei0_3_.anzahl as anzahl89_,
case when zutatbasei0_1_.id is not null then 1 when zutatbasei0_2_.id is not null then 2 when zutatbasei0_3_.id is not null then 3 when zutatbasei0_.id is not null then 0 end as clazz_
from ZutatBaseInheritanceBean zutatbasei0_ left outer join ZutatGewichtInheritanceBean zutatbasei0_1_ on zutatbasei0_.id=zutatbasei0_1_.id left outer join ZutatFluessigkeitInheritanceBean zutatbasei0_2_ on zutatbasei0_.id=zutatbasei0_2_.id left outer join ZutatStueckzahlInheritanceBean zutatbasei0_3_ on zutatbasei0_.id=zutatbasei0_3_.id


Application Client

Aufbau des Clients:
Der Client ist eine schicke Swing-Anwendung mit diesen Features:


Start des Clients

Siehe Stateless-Beispiel: zum Start des Client muss die EAR-Datei exportiert werden, und danach ist folgender Aufruf nötig:
C:\Temp\wildfly-30.0.0.Final\bin\appclient.bat x:\PfadZurEarDatei\KuchenZutatInheritance.ear#KuchenZutatInheritanceClient.jar

Achtung: die EAR-Dateien, die hier zum Download stehen, wurden mit Java 11 erstellt. Mit Java 17 gab es folgende Fehlermeldung:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private static final java.lang.Object javax.swing.JFrame.defaultLookAndFeelDecoratedKey accessible: module java.desktop does not \"opens javax.swing\" to unnamed module @f7cd57e"}

Ursache: es fehlt eine "module-info"-Datei in meinem Projekt (da mit Java 11 ohne eine solche Datei erstellt).
Lösung: Durch Setzen der Umgebungsvariable "JAVA_OPTS", die in "appclient.bat" ausgewertet wurd, kann dieses Problem umgangen werden:
set JAVA_OPTS=--add-opens=java.desktop/javax.swing=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED
%WILDFLY_HOME%\bin\appclient.bat c:\temp\KuchenSimple.ear#KuchenSimpleClient.jar

In vorherigen Java-Versionen konnte man die JAVA_OPTS auf --illegal-access=permit setzen, aber dies wurde in Java 17 entfernt.


Die Oberfläche sieht so aus:
Client

"jboss-deployment-structure.xml"

Dieser Workaround ist seit WildFly 31 nicht mehr nötig, da das Verhalten beim Starten des Application Client geändert wurde. Ich belasse ihn hier trotzdem als Beispiel für die Verwendung von "jboss-deployment-structure.xml".

Mit WildFly 30 kam es beim Hinzufügen des ersten Kuchens zur Fehlermeldung java.lang.ClassNotFoundException: org.hibernate.collection.spi.PersistentBag from [Module "deployment.KuchenZutatInheritance.ear.KuchenZutatInheritanceClient.jar" from Service Module Loader] im Client (https://issues.redhat.com/browse/WFLY-19020):


2024-02-07 19:24:38,650 ERROR [stderr] (AWT-EventQueue-0) jakarta.ejb.EJBException: Failed to read response
2024-02-07 19:24:38,651 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.protocol.remote.EJBClientChannel$MethodInvocation$MethodCallResultProducer.apply(EJBClientChannel.java:1367)
...
2024-02-07 19:24:38,656 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:116)
2024-02-07 19:24:38,656 ERROR [stderr] (AWT-EventQueue-0) 	at deployment.KuchenZutatInheritance.ear.KuchenZutatInheritanceEJB.jar/jdk.proxy6/jdk.proxy6.$Proxy13.getKuchen(Unknown Source)
2024-02-07 19:24:38,656 ERROR [stderr] (AWT-EventQueue-0) 	at deployment.KuchenZutatInheritance.ear.KuchenZutatInheritanceClient.jar//de.hsrm.jakartaee.knauf.kuchenzutatinheritance.FrameKuchenZutatInheritance.fillKuchen(FrameKuchenZutatInheritance.java:483)
2024-02-07 19:24:38,656 ERROR [stderr] (AWT-EventQueue-0) 	at deployment.KuchenZutatInheritance.ear.KuchenZutatInheritanceClient.jar//de.hsrm.jakartaee.knauf.kuchenzutatinheritance.FrameKuchenZutatInheritance.jButtonKuchenNeuActionPerformed(FrameKuchenZutatInheritance.java:395)
2024-02-07 19:24:38,657 ERROR [stderr] (AWT-EventQueue-0) 	at deployment.KuchenZutatInheritance.ear.KuchenZutatInheritanceClient.jar//de.hsrm.jakartaee.knauf.kuchenzutatinheritance.FrameKuchenZutatInheritance$3.actionPerformed(FrameKuchenZutatInheritance.java:169)
2024-02-07 19:24:38,657 ERROR [stderr] (AWT-EventQueue-0) 	at java.desktop/javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1972)
...
2024-02-07 19:24:38,662 ERROR [stderr] (AWT-EventQueue-0) 	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
2024-02-07 19:24:38,662 ERROR [stderr] (AWT-EventQueue-0) Caused by: java.lang.ClassNotFoundException: org.hibernate.collection.spi.PersistentBag from [Module "deployment.KuchenZutatInheritance.ear.KuchenZutatInheritanceClient.jar" from Service Module Loader]
2024-02-07 19:24:38,662 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:200)
2024-02-07 19:24:38,662 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:410)
2024-02-07 19:24:38,662 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:398)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:116)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at java.base/java.lang.Class.forName0(Native Method)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at java.base/java.lang.Class.forName(Class.java:467)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling@2.1.3.SP1//org.jboss.marshalling.AbstractClassResolver.loadClass(AbstractClassResolver.java:129)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling@2.1.3.SP1//org.jboss.marshalling.AbstractClassResolver.resolveClass(AbstractClassResolver.java:110)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadClassDescriptor(RiverUnmarshaller.java:1227)
2024-02-07 19:24:38,663 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadNewObject(RiverUnmarshaller.java:1560)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadObject(RiverUnmarshaller.java:441)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadObject(RiverUnmarshaller.java:388)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.readFields(RiverUnmarshaller.java:2107)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doInitSerializable(RiverUnmarshaller.java:2020)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadNewObject(RiverUnmarshaller.java:1601)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadObject(RiverUnmarshaller.java:441)
2024-02-07 19:24:38,664 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadObject(RiverUnmarshaller.java:388)
2024-02-07 19:24:38,665 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadCollectionObject(RiverUnmarshaller.java:344)
2024-02-07 19:24:38,665 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.readCollectionData(RiverUnmarshaller.java:1055)
2024-02-07 19:24:38,665 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadObject(RiverUnmarshaller.java:908)
2024-02-07 19:24:38,665 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling.river@2.1.3.SP1//org.jboss.marshalling.river.RiverUnmarshaller.doReadObject(RiverUnmarshaller.java:373)
2024-02-07 19:24:38,665 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.marshalling@2.1.3.SP1//org.jboss.marshalling.AbstractObjectInput.readObject(AbstractObjectInput.java:41)
2024-02-07 19:24:38,665 ERROR [stderr] (AWT-EventQueue-0) 	at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.protocol.remote.EJBClientChannel$MethodInvocation$MethodCallResultProducer.apply(EJBClientChannel.java:1341)
2024-02-07 19:24:38,666 ERROR [stderr] (AWT-EventQueue-0) 	... 67 more
Workaround: die vermisste Klasse steckt in "%WILDFLY_HOME%\modules\system\layers\base\org\hibernate\main\hibernate-core-6.2.13.Final.jar" und ist Teil des WildFly-Moduls "org.hibernate".

Dieses Modul kann mit einer Datei "jboss-deployment-structure.xml" bekannt gemacht werden, die im EAR-Projekt nach "EarContent" gelegt wird:
jboss-deployment-structure.xml
Sie hat diesen Inhalt:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.3"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="urn:jboss:deployment-structure:1.3 http://www.jboss.org/schema/jbossas/jboss-deployment-structure-1_3.xsd">
    <deployment>
        <dependencies>
            <module name="org.hibernate" export="true">
            </module>
        </dependencies>
    </deployment>
</jboss-deployment-structure>
Fett markiert ist das Einbinden des Moduls "Hibernate".

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>KuchenZutatInheritanceEJB</display-name>
	
	<enterprise-beans>
		<session>
			<description>
				<![CDATA[Stateless Session Bean für das Arbeiten mit Kuchen und Zutaten.]]>
			</description>
			<display-name>KuchenZutatInheritanceWorkerBean</display-name>
			<ejb-name>KuchenZutatInheritanceWorkerBean</ejb-name>
			<business-remote>de.hsrm.jakartaee.knauf.kuchenzutatinheritance.KuchenZutatInheritanceWorkerRemote</business-remote>
			<ejb-class>de.hsrm.jakartaee.knauf.kuchenzutatinheritance.KuchenZutatInheritanceWorkerBean</ejb-class>
			<session-type>Stateless</session-type>
			<persistence-context-ref>
				<persistence-context-ref-name>KuchenZutatInheritancePersistenceUnitRef</persistence-context-ref-name>
				<persistence-unit-name>kuchenZutatInheritancePersistenceUnit</persistence-unit-name>
				<injection-target>
					<injection-target-class>
						de.hsrm.jakartaee.knauf.kuchenzutatinheritance.KuchenZutatInheritanceWorkerBean
					</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 KuchenZutatNM-Beispiel.

"orm.xml" mit den Deklarationen der Entity Beans sieht so aus (im Beispiel für die Strategie "Joined"):
Fett markiert sind hier neuen Elemente, die die Inheritance Strategy steuern.
<?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 KuchenInheritanceBean o</query>
	</named-query>
	
	<named-query name="findAllZutaten">
		<query>select o from ZutatBaseInheritanceBean o</query>
	</named-query>
	<named-query name="findAllZutatenFluessigkeit">
		<query>select o from ZutatFluessigkeitInheritanceBean o</query>
	</named-query>
	<named-query name="findAllZutatenGewicht">
		<query>select o from ZutatGewichtInheritanceBean o</query>
	</named-query>
	<named-query name="findAllZutatenStueckzahl">
		<query>select o from ZutatStueckzahlInheritanceBean o</query>
	</named-query>
	
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatBaseInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<inheritance strategy="JOINED"/>
		<attributes>
			<id name="id">
				<generated-value />
			</id>
			<basic name="zutatName"/>
			<many-to-one name="kuchen"
				target-entity="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.KuchenInheritanceBean">
			</many-to-one>
		</attributes>
	</entity>
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatFluessigkeitInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<basic name="masseinheitMenge"/>
			<basic name="masseinheit">
				<enumerated>STRING</enumerated>
			</basic>
		</attributes>
	</entity>
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatGewichtInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<basic name="gewichtEinheit">
				<enumerated>STRING</enumerated>
			</basic>
			<basic name="gewicht"/>
		</attributes>
	</entity>
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatStueckzahlInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<basic name="anzahl"/>
		</attributes>
	</entity>
	
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.KuchenInheritanceBean" 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.kuchenzutatinheritance.ZutatBaseInheritanceBean">
				<cascade>
					<cascade-all />
				</cascade>
			</one-to-many>
		</attributes>
	</entity>
</entity-mappings>

Bei Verwendung der Strategie "SINGLE_TABLE" würde es so aussehen (nur geänderte Stücke der Entity Beans dargestellt):
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatBaseInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<inheritance strategy="SINGLE_TABLE"/>
		<attributes>
			...
		</attributes>
	</entity>
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatFluessigkeitInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<discriminator-value>fluessigkeit</discriminator-value>
		<attributes>
			...
		</attributes>
	</entity>
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatGewichtInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<discriminator-value>gewicht</discriminator-value>
		<attributes>
			...
		</attributes>
	</entity>
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatStueckzahlInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<discriminator-value>stueckzahl</discriminator-value>
		<attributes>
			...
		</attributes>
	</entity>
Auch hier ist bei Verwendung von "TABLE_PER_CLASS" die explizite Angabe der strategy nötig.

Und schließlich die Strategie "TABLE_PER_CLASS" würde es so aussehen (nur geänderte Stücke der Entity Beans dargestellt):
	<entity class="de.hsrm.jakartaee.knauf.kuchenzutatinheritance.ZutatBaseInheritanceBean" access="PROPERTY"
		metadata-complete="true">
		<inheritance strategy="TABLE_PER_CLASS"/>
		<attributes>
			...
		</attributes>
	</entity>
	


Die modifizierte Version des Projekts gibt es hier:
KuchenZutatInheritanceNoAnnotation.ear.
Man beachte, dass die drei Strategien in orm.xml durch einkommentieren des entsprechenden Abschnitts umgeschaltet werden können, ohne Neucompilieren. Ein eindeutiger Vorteil der Deployment-Deskriptoren.

ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenInheritance-Beispiel existieren !



Stand 17.06.2024
Historie:
08.02.2024: Erstellt aus JavaEE6-Version, angepaßt an JakartaEE10 und WildFly 30
17.06.2024: Workaround "jboss-deployment-structure.xml" ist nicht mehr nötig.