Beispiel: N:M-Relationship zweiter Entity Beans


Inhalt:

Anlegen der Application
Anlegen der Entity Bean "KuchenNM"
Anlegen der Entity-Bean "ZutatNM"
Hinzufügen der Relationship
persistence.xml
Anlegen der Session Bean "KuchenZutatNMWorker"
Anlegen des Webclients
Blick in die Datenbank
Ohne Annotations

Beispiel für zwei Entity Bean, auf die per Webclient zugegriffen wird. Zwischen den beiden Beans besteht eine Relationship, der Primary Key ist Container-erzeugt.
Änderung im Vergleich zu den vorherigen Beispielen: Kuchen und Zutat werden im Webclient separat eingegeben, die n:m-Verknüpfung ist eine Zuordnung von Kuchen zu Zutat. Die Information "Menge" entfällt, da wir ansonsten die Relationship um weitere Fehler erweitern müßten und dafür wahrscheinlich eine eigene Zuordnungs-Bean nötig wäre.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel): KuchenZutatNM.ear

Aufbau des Beispieles

a) Entity Bean-Klasse für Kuchen mit Local-Interfaces.
b) Entity Bean-Klasse für Zutat mit Local-Interfaces.
c) Session Bean für das Arbeiten mit Zutaten und Kuchen.
d) Webclient


Anlegen der Application

Ein "EAR Application Project" mit dem Namen "KuchenZutatNM" erstellen.
Zu erzeugende Module definieren. Dieses Beispiel benötigt ein EJB-Projekt und ein Anwendungsclientprojekt.
Erzeugen einer Application (Module)
Anschließend die Deployment-Deskriptoren "application.xml", "ejb-jar.xml" und "web.xml" auf JavaEE5 umstellen.


Anlegen der Entity Bean "KuchenNM"

Wir legen eine Klasse namens "KuchenNMBean" im Package "de.fhw.swtvertiefung.knauf.kuchenzutatnm" an, die das Interface java.io.Serializable implementiert.
KuchenNM Bean (1)


Der Code der Klasse mit Annotations (noch ohne Relationship-Felder) sieht so aus:
  @Entity()
  @NamedQuery(name="findAllKuchen", query="select o from KuchenNMBean o")
  public class KuchenNMBean implements Serializable
  {
    private static final long serialVersionUID = 1L;
  
    private Integer intId;
    private String strName;
  
    public KuchenNMBean()
    { 
    }
    
	@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;
    }
  }


Anlegen der Entity-Bean "ZutatNM"

Die ZutatNM-Bean sieht fast genauso aus wie die KuchenNM-Bean. Einziger Unterschied: Die Property "name" heißt hier "zutatName".
  @Entity()
  @NamedQuery(name="findAllZutaten", query="select o from ZutatNMBean o")
  public class ZutatNMBean implements Serializable
  {
    private static final long serialVersionUID = 1L;
  
    private Integer intId;
    private String strZutatName;
	
    public ZutatNMBean()
    { 
    }
    
	
    @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;
    }
  }


Hinzufügen der Relationship

In "KuchenBean" fügen wir eine Membervariable und zwei Methoden zu:
  /**Die Collection der Zutaten */
  private Collection<ZutatBean> collZutaten = new ArrayList<ZutatBean>();

  @ManyToMany(mappedBy="kuchen", cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.LAZY)
  public Collection<ZutatBean> getZutaten()
  {
    return this.collZutaten;
  }
  
  public void setZutaten (Collection<ZutatBean> coll_Zutaten)
  {
    this.collZutaten = coll_Zutaten;
  } 

Über den CascadeType müssen wir uns diesmal mehr Gedanken machen: ein kaskadierendes Löschen soll verboten sein, denn eine Zutat kann auch ohne Kuchen existieren. Deshalb kaskadieren wir alle Operationen außer dem Löschen weiter (Merge, Persist und Refresh). Der FetchType sollte hier nicht "Eager" sondern "Lazy" sein (denn wir wollen möglichst wenig Daten zusammen mit dem angeforderten Objekt mitladen).

In der Zutat-Bean wird die andere Seite der Relation deklariert:
  private Collection<KuchenBean> collKuchen = new ArrayList<KuchenBean>();
  @ManyToMany(cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.LAZY)
  public Collection<KuchenBean> getKuchen()
  {
    return this.collKuchen;
  }
  
  public void setKuchen (Collection<KuchenBean> coll_Kuchen)
  {
    this.collKuchen = coll_Kuchen;
  } 
Hier müssen wir kein Attribut "mappedBy" in der Annotation angegeben.
Den FetchType habe ich auf "Lazy" gesetzt, denn wenn wir den auf beiden Seite der Relation auf "Eager" setzen würden, dann würde beim Abrufen eines Kuchens die Liste seiner Zutaten geholt, pro Zutat wiederum die Liste der Kuchen für die die Zutat verwendet wird, und für jeden dieser Kuchen wiederum die Zutaten. Dadurch würde im schlimmsten Fall die gesamte Datenbank bei einem Zugriff eingelesen werden.


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>
		<persistence-unit name="kuchenZutatNMPersistenceUnit">
			<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 "KuchenZutatNMWorker"

Es wird eine SessionBean "KuchenZutatNMWorkerBean" zugefügt, die diese Methoden enthält:
  @Stateless
  public class KuchenZutatNMWorkerBean implements KuchenZutatNMWorkerLocal
  {
    @PersistenceContext(unitName="kuchenZutatNMPersistenceUnit")
    EntityManager entityManager = null;
Die folgenden Methoden sind mit denen des KuchenZutat-Beispiel weitgehend identisch.
Besonderheit ist in "findKuchenById" bzw. "findZutatById" das explizite Abrufen der Kuchen-/Zutaten-Liste. Grund ist dass der FetchType auf "LAZY" steht, die Relation also nicht direkt beim Laden des Objekts eingelesen wird. Deshalb MUSS die Liste der Kuchen/Zutaten abgerufen werden solange die Bean noch nicht detached ist. Beim Abrufen reicht es nicht die Collection über "getZutaten"/"getKuchen" zu holen, es muss irgendeine Operation ausgeführt werden die explizit die Collection einliest. Im Beispiel ist das das Abrufen der Anzahl der Datensätze für eine Logger-Ausgabe. Ohne dieses Stück Code wird ein Zugriff im Webclient eine Exception auslösen.
    public void saveKuchen (KuchenNMBean kuchen)
    {
      kuchen = this.entityManager.merge(kuchen);
    }
    
    public List getKuchen()
    {
      Query query = this.entityManager.createNamedQuery("findAllKuchen");
      List<KuchenNMBean> listKuchen = query.getResultList();
      return listKuchen;
    }
    
    public KuchenNMBean findKuchenById(Integer int_Id)
    {
      KuchenNMBean kuchen = this.entityManager.find(KuchenNMBean.class, int_Id);
      
      //Da der FetchType der ManyToMany-Relation auf LAZY gesetzt ist müssen die Zutaten 
      //hier explizit abgerufen werden solange die KuchenNMBean noch nicht
      //detached ist. Später würde ansonsten ein Zugriff auf die Zutatenliste eine
      //Exception auslösen.
      Collection<ZutatNMBean> collZutaten = kuchen.getZutaten();
      //Wir müssen einen Zugriff auf die Collection selbst ausführen, Abrufen der Property alleine reicht nicht !
      logger.info ("Anzahl Zutaten: " + collZutaten.size());
      
      return kuchen;
    }
    
    public void deleteKuchen (KuchenNMBean kuchen)
    {
      //Zuerst den Kuchen aus der Datenbank holen:
      kuchen = this.entityManager.find (KuchenNMBean.class, kuchen.getId() );
	  
	  //Jetzt wird es knifflig: wir müssen die Zutaten des Kuchens holen,
      //und aus den Kuchen-Collections der Zutaten den Kuchen entfernen.
      //Grund scheint zu sein dass der EntityManager ansonsten noch den
      //Kuchen in Zutaten hält und dabei in einen Fehlerzustand läuft.
      Collection<ZutatNMBean> listeZutaten = kuchen.getZutaten();
      Iterator<ZutatNMBean> iteratorZutat = listeZutaten.iterator();
      while (iteratorZutat.hasNext() == true) 
      {
        //Zutat wegwerfen:
        ZutatNMBean zutatVonKuchen = iteratorZutat.next();
        zutatVonKuchen.getKuchen().remove(kuchen);
      }
	  
      //Jetzt löschen:
      this.entityManager.remove(kuchen);
    }
    
    public ZutatNMBean findZutatById(Integer int_Id)
    {
      ZutatNMBean zutat = this.entityManager.find(ZutatNMBean.class, int_Id);
      //Die Kuchen einmal abrufen solange die Bean unter
      //Container-Verwaltung ist.
      Collection<KuchenNMBean> collKuchen = zutat.getKuchen();
      //Wir müssen einen Zugriff auf die Collection selbst ausführen, Abrufen der Property alleine reicht nicht !
      logger.info ("Anzahl Kuchen: " + collKuchen.size());
      
      return zutat;
    }
    
    public void deleteZutat (ZutatNMBean zutat)
    {
      //Zuerst einmal die Zutat im EntityManager holen: 
      zutat = this.entityManager.find (ZutatNMBean.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.
      Collection<KuchenNMBean> listeKuchen = zutat.getKuchen();
      Iterator<KuchenNMBean> iteratorKuchen = listeKuchen.iterator();
      while (iteratorKuchen.hasNext() == true) 
      {
        //Zutat wegwerfen:
        KuchenNMBean kuchenMitZutat = iteratorKuchen.next();
        kuchenMitZutat.getZutaten().remove(zutat);
        //Speichern des Kuchens ist nicht nötig.
      }
  
      //Jetzt endlich dürfen wir die Zutat löschen.
      this.entityManager.remove(zutat);
    }
    
    public void saveZutat (ZutatNMBean zutat)
    {
      this.entityManager.merge(zutat);
    }
    
    public List getZutaten()
    {
      Query query = this.entityManager.createNamedQuery("findAllZutaten");
      
      List<ZutatNMBean> listZutaten = query.getResultList();
      return listZutaten;
    }
  } 
Wie im Kuchen-Zutat-Beispiel müssen wir hier die Zutat aufwändig löschen da kein CascadeType.REMOVE gesetzt ist. Auch beim Löschen eines Kuchens ist das manuelle Anpassen der Zutaten-Seite nötig, da wir auch hier kein kaskadierendes Löschen aktiviert haben.

Jetzt kommen zwei neue Methoden ins Spiel die die Zuordnung von Zutaten zu Kuchen verwalten. Grund dieser Methoden: wenn eine Zutat nur zu der Zutatenliste eines Kuchens zugefügt wird dann passiert beim Speichern rein garnichts (es wird kein Datensatz in der Datenbank erzeugt). Es müssen immer beide Seiten des Mappings angepaßt werden:
Die gleiche Logik ist für das Entfernen einer Zutat aus dem Kuchen nötig !

So etwas funktioniert auch mit den detached Entities z.B. im Webclient (wahrscheinlich weil das Speichern dann vom Kuchen über die Zutatenliste weiterkaskadiert), allerdings habe ich es zur Sicherheit in die SessionBean verlegt, wo wir den Entity Manager zur Verfügung haben.
    public void addZutatToKuchen (KuchenNMBean kuchen, ZutatNMBean zutat)
    {
      //Zuerst einmal beide Datensätze unter Container-Verwaltung stellen:
      kuchen = this.entityManager.find(KuchenNMBean.class, kuchen.getId() );
      zutat = this.entityManager.find(ZutatNMBean.class, zutat.getId() );
      
      //Jetzt BEIDEN Seiten des Mappings zufügen !
      kuchen.getZutaten().add(zutat);
      zutat.getKuchen().add(kuchen);
      
      //Und eine Seite des Mappings speichern (hier dürfen wir "Persist" nehmen).
      this.entityManager.persist(kuchen);
    }
    
    public void removeZutatFromKuchen (KuchenNMBean kuchen, ZutatNMBean zutat)
    {
      //Zuerst einmal beide Datensätze unter Container-Verwaltung stellen:
      kuchen = this.entityManager.find(KuchenNMBean.class, kuchen.getId() );
      zutat = this.entityManager.find(ZutatNMBean.class, zutat.getId() );
      
      //Jetzt in BEIDEN Seiten des Mappings entfernen !
      kuchen.getZutaten().remove(zutat);
      zutat.getKuchen().remove(kuchen);
      
      //Und eine Seite des Mappings speichern (hier dürfen wir "Persist" nehmen).
      this.entityManager.persist(kuchen);
    } 


Die Bussiness-Methoden werden in ein local Interface "KuchenZutatNMWorkerLocal" extrahiert, die Bean muss dieses Interface implementieren.
Das Interface erhält die Annotation "@Local".

Anlegen des Webclients

Der Webclient muss die EJB-JARs referenzieren. Dazu in die Eigenschaften des Webmoduls "KuchenZutatNMWeb" wechseln und unter "J2EE Module Depdencies" das EJB-JAR wählen.
EJB-Dependencies
EJB-Verweise in "web.xml" festlegen:
<ejb-local-ref>
	<ejb-ref-name>ejb/KuchenZutatNMWorkerLocal</ejb-ref-name>
	<ejb-ref-type>Entity</ejb-ref-type>
	<local-home>java.lang.Object</local-home>
	<local>de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenZutatNMWorkerLocal</local>
</ejb-local-ref> 
"jboss-web.xml" sollte so aussehen:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web PUBLIC
	"-//JBoss//DTD Web Application 4.2//EN"
	"http://www.jboss.org/j2ee/dtd/jboss-web_4_2.dtd">
<jboss-web>
	<context-root>KuchenZutatNMWeb</context-root>
	<ejb-local-ref>
		<ejb-ref-name>ejb/KuchenZutatNMWorkerLocal</ejb-ref-name>
		<local-jndi-name>KuchenZutatNM/KuchenZutatNMWorkerBean/local</local-jndi-name>
	</ejb-local-ref>
</jboss-web>

Es müssen vier JSP-Seiten "index.jsp", "KuchenEdit.jsp", "ZutatEdit.jsp" und "KuchenZutaten.jsp" zugefügt werden.

Jetzt die Anwendung nur noch deployen. Sie ist unter
http://localhost:8080/KuchenZutatNMWeb/index.jsp zu erreichen.


Blick in die Datenbank

In der Datenbank sieht das so aus:
Mapping-Tabelle
Der Name der Mapping-Datenbank wurde erzeugt aus den Namen der beiden beteiligten Beans. Im rechten unteren Teil des Screenshots sind die Inhalte der KuchenNMBean-Tabelle (oben) und der ZutatNMBean-Tabelle (darunter) dargestellt.


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>
	KuchenZutatNMEJB</display-name>
	
	<enterprise-beans>
		<session>
			<description>
				<![CDATA[Stateless Session Bean für das Arbeiten mit Kuchen und Zutaten.]]>
			</description>
			<display-name>KuchenZutatNMWorkerBean</display-name>
			<ejb-name>KuchenZutatNMWorkerBean</ejb-name>
			<local>de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenZutatNMWorkerLocal</local>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenZutatNMWorkerBean</ejb-class>
			<session-type>Stateless</session-type>
			<!--EntityManager-Injection -->
			<persistence-context-ref>
				<persistence-context-ref-name>KuchenZutatNMPersistenceUnitRef</persistence-context-ref-name>
				<persistence-unit-name>kuchenZutatNMPersistenceUnit</persistence-unit-name>
				<injection-target>
					<injection-target-class>
						de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenZutatNMWorkerBean
					</injection-target-class>
					<injection-target-name>entityManager</injection-target-name>
				</injection-target>
			</persistence-context-ref>
		</session>
		<entity>
			<description>
				<![CDATA[Entity Bean für einen einzelnen Kuchen.]]>
			</description>
			<display-name>KuchenNMBean</display-name>
			<ejb-name>KuchenNMBean</ejb-name>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenNMBean</ejb-class>
			<persistence-type>Container</persistence-type>
			<prim-key-class>java.lang.Integer</prim-key-class>
			<reentrant>false</reentrant>
		</entity>
		<entity>
			<description>
				<![CDATA[Entity Bean für eine einzelne Zutat.]]>
			</description>
			<display-name>ZutatNMBean</display-name>
			<ejb-name>ZutatNMBean</ejb-name>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchenzutatnm.ZutatNMBean</ejb-class>
			<persistence-type>Container</persistence-type>
			<prim-key-class>java.lang.Integer</prim-key-class>
			<reentrant>false</reentrant>
		</entity>
	</enterprise-beans>
	
</ejb-jar>
Es gibt keine Neuerungen im Vergleich zum KuchenZutatNM-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 KuchenNMBean o</query>
	</named-query>
	<named-query name="findAllZutaten">
		<query>select o from ZutatNMBean o</query>
	</named-query>

	<entity class="de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenNMBean" access="PROPERTY"
		metadata-complete="true">
		<table name="KUCHENNMBEAN"></table>
		<attributes>
			<id name="id">
				<column name="ID" />
				<generated-value />
			</id>
			<basic name="name">
				<column name="NAME" />
			</basic>
			<many-to-many name="zutaten" mapped-by="kuchen" fetch="LAZY"
				target-entity="de.fhw.swtvertiefung.knauf.kuchenzutatnm.ZutatNMBean">
				<cascade>
					<cascade-persist />
					<cascade-merge />
					<cascade-refresh />
				</cascade>
			</many-to-many>
		</attributes>
	</entity>

	<entity class="de.fhw.swtvertiefung.knauf.kuchenzutatnm.ZutatNMBean" access="PROPERTY"
		metadata-complete="true">
		<table name="ZUTATNMBEAN"></table>
		<attributes>
			<id name="id">
				<column name="ID" />
				<generated-value />
			</id>
			<basic name="zutatName">
				<column name="ZUTATNAME" />
			</basic>
			<many-to-many name="kuchen" fetch="LAZY"
				target-entity="de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenNMBean">
				<cascade>
					<cascade-persist />
					<cascade-merge />
					<cascade-refresh />
				</cascade>
			</many-to-many>
		</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:
KucheZutatNMNoAnnotation.ear.
ACHTUNG 1: Dieses Projekt kann nicht neben dem obigen KuchenZutatNM-Beispiel existieren !

ACHTUNG 2: Ein Re-Import der EAR-Datei klappt nur wenn in ejb-jar.xml das Element "persistence-context-ref" auskommentiert ist ! Ansonsten zeigt Eclipse eine Fehlermeldung, ohne sich allerdings über die Details auszulassen. Dieses Element muss nach dem Import wieder einkommentiert werden !



Stand 14.07.2007
Historie:
23.04.2007: Aus Vorjahresbeispiel erstellt, JBoss-Deploymentdeskriptoren auf 4.2 aktualisiert, Javadoc-Warnungen entfernt
08.05.2007: Bug in "KuchenZutat.jsp" behoben, Code aufgeräumt.
14.07.2007: Falsche Interfaceklasse in web.xml korrigiert.