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
Datenbank
Ohne Annotations
Troubleshooting

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 das Vorgehen ohne Verwendung der "Java Persistence"-Facet. Dazu eine neue Klasse namens "KuchenBean" im Package "de.fhw.komponentenarchitekturen.knauf.kuchenzutat" erstellen. Die Klasse soll das Interface "java.io.Serializable" implementieren.
KuchenBean


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="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_1_0.xsd"
		version="1.0">
		<persistence-unit name="kuchenZutatPersistenceUnit">
			<jta-data-source>java:/DefaultDS</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:/DefaultDS" abgelegt ist und auf die JBoss-interne Hypersonic-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: 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 "JEE Module Dependencies" das EJB-JAR wählen.
EJB-Dependencies
EJB-Verweise festlegen (ich verwende hier keine Injection, da die in der Version ohne Annotations nicht funktioniert, deshalb dem einheitlichen Quellcode zulieber hier Lookup):
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.fhw.komponentenarchitekturen.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"?>
	<!DOCTYPE jboss-web PUBLIC
		"-//JBoss//DTD Web Application 5.0//EN"
		"http://www.jboss.org/j2ee/dtd/jboss-web_5_0.dtd">
	<jboss-web>
		<context-root>KuchenZutatWeb</context-root>
		<ejb-local-ref>
			<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
			<local-jndi-name>KuchenZutat/KuchenZutatWorkerBean/local</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).

Datenbank

In der Datenbank sieht das so aus (Hypersonic-Databasemanager über JMX-Console starten):
"Kuchen"-Tabelle:
Kuchen-Tabelle
"Zutat"-Tabelle:
Zutat-Tabelle
Besonders zu beachten ist die automatisch generierte Spalte mit dem Foreign Key-Feld zum Kuchen.


Ohne Annotations

"ejb-jar.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar id="ejb-jar_ID" version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                           http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
	<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.fhw.komponentenarchitekturen.knauf.kuchenzutat.KuchenZutatWorkerLocal</local>
			<ejb-class>de.fhw.komponentenarchitekturen.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.fhw.komponentenarchitekturen.knauf.kuchenzutat.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="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
	version="1.0">
	<named-query name="findAllKuchen">
		<query>select o from KuchenBean o</query>
	</named-query>

	<entity class="de.fhw.komponentenarchitekturen.knauf.kuchenzutatt.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.fhw.komponentenarchitekturen.knauf.kuchenzutat.ZutatBean">
				<cascade>
					<cascade-all />
				</cascade>
			</one-to-many>
		</attributes>
	</entity>

	<entity class="de.fhw.komponentenarchitekturen.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.fhw.komponentenarchitekturen.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 ;-)

Stand 04.01.2009
Historie:
07.11.2008: Aus Vorjahresbeispiel erstellt, Eclipse-Warnungen entfernt, Anpassungen an JBoss 5.0, falsche "ejb-jar.xml" korrigiert.
12.11.2008: In SessionBean Fehlerbehandlung für nicht gefundene Entities eingebaut, Code vor allem im Web aufgeräumt. Hinweis auf Unterschiede "find"/"getReference" und Defaults für "fetch". "orm.xml" aufgeräumt.
04.01.2009: Troubleshooting "cannot simultaneously fetch multiple bags" zugefügt.