Beispiel: N:M-Relationship zweiter Entity Beans


Inhalt:

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


Das Beispiel besteht aus einem "EAR Application Project" mit dem Namen "KuchenZutatNM", einem EJB-Projekt und einem Webprojekt.


Anlegen der Entity Bean "KuchenNM"

Wir legen eine Klasse namens "KuchenNMBean" im Package "de.fhw.komponentenarchitekturen.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:
  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.
@ManyToMany und die @JoinTable-Annotation:
Mittels der @JoinTable-Annotation kann man kontrollieren, wie die Mapping-Tabelle sowie deren Spalten heißen sollen. Hierbei ist es bei bidirektionalen Relationships wichtig, wohin man sie setzt! Sie darf auf keinen Fall auf die Seite der Relationship, in der das "mappedBy"-Attribut deklariert ist.

Für obiges Beispiel wäre die korrekte Lösung:

KuchenBean:
  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;
  }
ZutatBean:
  private Collection<KuchenBean> collKuchen = new ArrayList<KuchenBean>();

  @ManyToMany(cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.LAZY)
  @JoinTable (name="KUCHENZUTATMAPPING", 
    joinColumns={@JoinColumn(name="KUCHENID") },
    inverseJoinColumns={@JoinColumn(name="ZUTATID") })
  public Collection<KuchenBean> getKuchen()
  {
    return this.collKuchen;
  }
Setzt man den @JoinTable in die KuchenBean (also dort, wo das "mappedBy" deklariert ist), so wird diese Deklaration einfach ignoriert und der JBoss generiert Default-Namen für die Mappingtabelle!
In einem Nebensatz findet man einen dezenten Hinweis auf dieses Verhalten in der
Hibernate-Doku.


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="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 "kuchenZutatNMPersistenceUnit" 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" (mit Local Interface "KuchenZutatNMWorkerLocal") zugefügt, die diese Methoden enthält:
  @Stateless
  public class KuchenZutatNMWorkerBean implements KuchenZutatNMWorkerLocal
  {
    @PersistenceContext(unitName="kuchenZutatNMPersistenceUnit")
    private 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<KuchenNMBean> getKuchen()
    {
      Query query = this.entityManager.createNamedQuery("findAllKuchen");
      List<KuchenNMBean> listKuchen = query.getResultList();
      return listKuchen;
    }
    
    public KuchenNMBean findKuchenById(Integer int_Id)
    {
      //Die Zutat im EntityManager laden.
      //Hier mit "find" arbeiten, da bei ungültiger ID "null" zurückkommen soll. 
      KuchenNMBean kuchen = this.entityManager.find(KuchenNMBean.class, int_Id);
    
      //Falls etwas gefunden wurde, dann die Relationship einlesen.
      if (kuchen != null)
      {
        //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 (Integer intKuchenId)
    {
      //Den Kuchen im EntityManager laden.
      //Hier mit "getReference" arbeiten, damit eine böse EntityNotFoundException fliegt wenn Zutat nicht gefunden wird.
      KuchenNMBean kuchen = this.entityManager.getReference(KuchenNMBean.class, intKuchenId);
  
      //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 listeZutaten = kuchen.getZutaten();
      Iterator 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);
      //Wenn nichts gefunden wurde, kommt hier "null" zurück.
      //Falls etwas gefunden wurde, dann die Relationship einlesen.
      if (zutat != null)
      {
        //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 (Integer intZutatId)
    {
      //Die Zutat im EntityManager laden.
      //Hier mit "getReference" arbeiten, damit eine böse EntityNotFoundException fliegt wenn Zutat nicht gefunden wird.
      ZutatNMBean zutat = this.entityManager.getReference(ZutatNMBean.class, intZutatId );
      
      //Jetzt wird es knifflig: wir müssen die Kuchen der Zutat holen,
      //und aus den Zutat-Collections der 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);
      }
  
      //Jetzt endlich dürfen wir die Zutat löschen.
      this.entityManager.remove(zutat);
    }
    
    public void saveZutat (ZutatNMBean zutat)
    {
      this.entityManager.merge(zutat);
    }
    
    public List<ZutatNMBean> 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 (Integer intKuchenId, Integer intZutatId)
    {
      //Kuchen und Zutat laden:
      //Es wird "getReference" verwendet, um eine Exception zu provozieren falls kein Datensatz gefunden wird.
      KuchenNMBean kuchen = this.entityManager.getReference(KuchenNMBean.class, intKuchenId );
      ZutatNMBean zutat = this.entityManager.getReference(ZutatNMBean.class, intZutatId );
      
      //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, da alle beteiligten Entities
      //unter Kontrolle des Persistence-Managers sind, "merge" ist nicht nötig).
      this.entityManager.persist(kuchen);
    }
    
    public void removeZutatFromKuchen (Integer intKuchenId, Integer intZutatId)
    {
      //Kuchen und Zutat laden:
      //Es wird "getReference" verwendet, um eine Exception zu provozieren falls kein Datensatz gefunden wird.
      KuchenNMBean kuchen = this.entityManager.getReference(KuchenNMBean.class, intKuchenId );
      ZutatNMBean zutat = this.entityManager.getReference(ZutatNMBean.class, intZutatId);
      
      //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, da alle beteiligten Entities
      //unter Kontrolle des Persistence-Managers sind).
      this.entityManager.persist(kuchen);
    }	

Die Fehlerbehandlung für nicht gefundene Entities sieht so aus:


Anlegen des Webclients

Der Webclient muss die EJB-JARs referenzieren. Dazu in die Eigenschaften des Webmoduls "KuchenZutatNMWeb" wechseln und unter "Java EE Module Dependencies" das EJB-JAR wählen.

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>de.fhw.komponentenarchitekturen.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 5.0//EN"
	"http://www.jboss.org/j2ee/dtd/jboss-web_5_0.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>
			<business-local>de.fhw.komponentenarchitekturen.knauf.kuchenzutatnm.KuchenZutatNMWorkerLocal</business-local>
			<ejb-class>de.fhw.komponentenarchitekturen.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.komponentenarchitekturen.knauf.kuchenzutatnm.KuchenZutatNMWorkerBean
					</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" 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.komponentenarchitekturen.knauf.kuchenzutatnm.KuchenNMBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<id name="id">
				<generated-value />
			</id>
			<basic name="name">
			</basic>
			<many-to-many name="zutaten" mapped-by="kuchen" fetch="LAZY"
				target-entity="de.fhw.komponentenarchitekturen.knauf.kuchenzutatnm.ZutatNMBean">
				<cascade>
					<cascade-persist />
					<cascade-merge />
					<cascade-refresh />
				</cascade>
			</many-to-many>
		</attributes>
	</entity>

	<entity class="de.fhw.komponentenarchitekturen.knauf.kuchenzutatnm.ZutatNMBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<id name="id">
				<generated-value />
			</id>
			<basic name="zutatName">
			</basic>
			<many-to-many name="kuchen" fetch="LAZY"
				target-entity="de.fhw.komponentenarchitekturen.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: Dieses Projekt kann nicht neben dem obigen KuchenZutatNM-Beispiel existieren !



Stand 16.11.2008
Historie:
16.11.2008: Aus Vorjahresbeispiel erstellt und angepaßt an JBoss 5.0