Beispiel: N:M-Relationship mit Join-Table


Inhalt:

Definition der Relation als eigene Entity
Anlegen der Session Bean "KuchenZutatJoinTableWorkerBean"
Anlegen des Webclients
Blick in die Datenbank
Alternative: @javax.persistence.EmbeddedId
Ohne Annotations

Dieses Beispiel erweitert das Many-To-Many-Beispiel von Kuchen und Zutat: für die Abbildung der Relation wird eine eigene Join-Tabelle definiert, die ein zusätzliches Feld "Menge" enthält.

Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel): KuchenZutatJoinTable.ear

Aufbau des Beispieles

a) Entity Bean für Kuchen
b) Entity Bean für Zutat
c) Entity Bean für die Verknüpfung von Kuchen und Zutat
d) Session Bean für das Arbeiten mit Zutaten und Kuchen (mit Local-Interface).
e) Webclient


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


Definition der Relation als eigene Entity

Unser Ziel ist es, eine Verknüpfungstabelle mit Feldern "KuchenID" und "ZutatID" sowie "Menge" zu erzeugen. "KuchenID" und "ZutatID" sollen den Primärschlüssel bilden. Eine besondere Schwierigkeit ergibt sich, weil der Primary Key im Prinzip aus zwei Relationsfeldern gebildet wird. Leider können Relationen nicht Teil eines Primary Key sein. Deshalb muss getrickst werden: es wird ein Primary Key aus KuchenID und ZutatID definiert, und außerdem wird eine Relation zu Zutat und Kuchen definiert. Jeglicher Zugriff auf die Relation soll NUR über die Relationsfelder passieren, ein Verwender soll NIE direkt auf eine ID zugreifen.

Entity KuchenZutatJoinTableBean: ID-Felder
Die ID-Felder der Entity sehen so aus:
import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity()
public class KuchenZutatJoinTableBean implements Serializable
{
  private Integer intKuchenId = null;
  private Integer intZutatId = null;
  
  @Id
  @Column(name="KUCHENID")
  @SuppressWarnings("unused")
  private Integer getKuchenId()
  {
    return this.intKuchenId;
  }

  @Deprecated
  @SuppressWarnings("unused")
  private void setKuchenId (Integer int_KuchenId)
  {
    this.intKuchenId = int_KuchenId;
  }
 
  @SuppressWarnings("unused")
  @Id
  @Column(name="ZUTATID")
  private Integer getZutatId()
  {
    return this.intZutatId;
  }

  @SuppressWarnings("unused")
  @Deprecated()
  private void setZutatId (Integer int_ZutatId)
  {
    this.intZutatId = int_ZutatId;
  }
}
Hier ergeben sich zwei Besonderheiten: Entity KuchenZutatJoinTableBean: Relationsfelder
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

  ...
  private KuchenJoinTableBean kuchen = null;
  private ZutatJoinTableBean zutat = null;
  ...
  
  @ManyToOne ()
  @JoinColumn(name="KUCHENID", insertable=false, updatable=false, nullable=false)
  public KuchenJoinTableBean getKuchen()
  {
    return this.kuchen;
  }

  public void setKuchen (KuchenJoinTableBean kuchen)
  {
    this.kuchen = kuchen;
    
    if (kuchen != null)
    {
      this.intKuchenId = kuchen.getId();
    }
    else
    {
      this.intKuchenId = null;
    }
  }
  
  @ManyToOne ()
  @JoinColumn(name="ZUTATID", insertable=false, updatable=false, nullable=false)
  public ZutatJoinTableBean getZutat()
  {
    return this.zutat;
  }

  public void setZutat (ZutatJoinTableBean zutat)
  {
    this.zutat = zutat;
    
    if (zutat != null)
    {
      this.intZutatId = zutat.getId();
    }
    else
    {
      this.intZutatId = null;
    }
  }
  ...
Auch hier gibt es zwei große Besonderheiten:

ID-Klasse:
Da wir einen Primary Key aus zwei Spalten haben, benötigen wir eine eigene ID-Klasse. Diese ist eine simple Java-Klasse, die eine Kopie der ID-Spalten der Entity enthält.
import java.io.Serializable;

public class KuchenZutatPK implements Serializable
{
  private static final long serialVersionUID = 1L;
  
  private Integer intKuchenId = null;  
  private Integer intZutatId = null;
  
  public Integer getKuchenId()
  {
    return this.intKuchenId;
  }

  public void setKuchenId (Integer int_KuchenId)
  {
    this.intKuchenId = int_KuchenId;
  }
  
  public Integer getZutatId()
  {
    return this.intZutatId;
  }

  public void setZutatId (Integer int_ZutatId)
  {
    this.intZutatId = int_ZutatId;
  }
}
Wichtig ist, dass die Properties exakt gleiche Namen wie die Primary-Key-Felder der Entity haben müssen.

In der Entity KuchenZutatJoinTableBean wird diese ID-Klasse deklariert.
import javax.persistence.IdClass;

@Entity()
@IdClass(value=KuchenZutatPK.class)
public class KuchenZutatJoinTableBean implements Serializable
{
  ...
Für diese IDClass gibt es zwei Gründe:

KuchenJoinTableBean
In der KuchenJoinTableBean sieht die Relation so aus:
@Entity()
public class KuchenJoinTableBean implements Serializable
{
  ...
  private Collection<KuchenZutatJoinTableBean> collKuchenZutaten = new ArrayList<KuchenZutatJoinTableBean>();
  ...
  @OneToMany(mappedBy="pk.kuchen", cascade={CascadeType.ALL}, fetch=FetchType.LAZY)
  public Collection<KuchenZutatJoinTableBean> getZutaten()
  {
    return this.collKuchenZutaten;
  }
  
  public void setZutaten (Collection<KuchenZutatJoinTableBean> coll_KuchenZutaten)
  {
    this.collKuchenZutaten = coll_KuchenZutaten;
  }
  ...
}
Die Entity hat eine Property "zutaten", die eine Liste von Entities KuchenZutatJoinTableBean ist. "Cascade" steht auf CascadeType.ALL (also wird auch kaskadierend gelöscht!), "fetch" steht wie auch im n:m-Beispiel auf "LAZY". Die @OneToMany-Relation in der Verknüpfungs-Entity kaskadiert übrigens nicht weiter zur Zutat.


ZutatJoinTableBean
In der ZutatJoinTableBean sieht die Relation so aus:
@Entity()
public class ZutatJoinTableBean implements Serializable
{
  ...
  private Collection<KuchenZutatJoinTableBean> collKuchenVerknuepfungen = new ArrayList<KuchenZutatJoinTableBean>();
  ...
  @OneToMany(mappedBy="pk.zutat", cascade={CascadeType.ALL}, fetch=FetchType.LAZY)
  public Collection<KuchenZutatJoinTableBean> getKuchen()
  {
    return this.collKuchenVerknuepfungen;
  }
  
  public void setKuchen (Collection<KuchenZutatJoinTableBean> coll_KuchenVerknuepfungen)
  {
    this.collKuchenVerknuepfungen = coll_KuchenVerknuepfungen;
  }
  ...
}



Anlegen der Session Bean "KuchenZutatJoinTableWorkerBean"

Es wird eine SessionBean "KuchenZutatJoinTableWorkerBean" (mit Local Interface "KuchenZutatJoinTableWorkerLocal") zugefügt. Sie ist weitgehend identisch mit der KuchenZutatNMWorkerBean des n:m-Beispiels, nur bei der Verwaltung der Relation gibt es jetzt Unterschiede. Die Relation wird allerdings weitgehend weggekapselt, so dass Erstellen oder Löschen einer Verknüpfung (bis auf eine Ausnahme) API-kompatibel zum n:m-Beispiel ist.

Zu beachten ist, dass der Use-Case "Ändern der Menge einer bestehenden Verknüpfung" nicht umgesetzt wurde, um das Beispiel einfach zu halten!

Beim Verknüpfen eines Kuchens mit einer Zutat ist die einzige API-Änderung zu vermerken: der Parameter "menge" wird zusätzlich übergeben.
Beim Erstellen einer Verknüpfung wird eine neue Entity KuchenZutatJoinTableBean sowie deren Primary Key KuchenZutatPK erzeugt. Auch hier gilt wie in allen bisherigen Beispielen, dass die Mapping-Entity auch der Zutatenliste des Kuchens und der Kuchenliste der Zutat zugefügt werden muss!
   public void addZutatToKuchen (Integer intKuchenId, Integer intZutatId, String strMenge)
  {
    //Kuchen und Zutat laden:
    //Es wird "getReference" verwendet, um eine Exception zu provozieren falls kein Datensatz gefunden wird.
    KuchenJoinTableBean kuchen = this.entityManager.getReference(KuchenJoinTableBean.class, intKuchenId );
    ZutatJoinTableBean zutat = this.entityManager.getReference(ZutatJoinTableBean.class, intZutatId );
    
    //Neues Mapping erzeugen:
    KuchenZutatJoinTableBean kuchen2Zutat = new KuchenZutatJoinTableBean();
    kuchen2Zutat.setZutat(zutat);
    kuchen2Zutat.setKuchen(kuchen);
    kuchen2Zutat.setMenge(strMenge);
    
    //Jetzt BEIDEN Seiten des Mappings zufügen!
    //Würde man das nicht tun, gäbe es zwar keinen Fehler, aber es würde auch nix
    //in die Datenbank gespeichert!
    kuchen.getZutaten().add(kuchen2Zutat);
    zutat.getKuchen().add(kuchen2Zutat);
    
    this.entityManager.persist(kuchen);
  }
Das Löschen ist sogar einfacher als im letzten Beispiel geworden: anhand von Kuchen- und Zutat-ID wird eine Primary-Key-Klasse erzeugt und mit dieser die Verknüpfungs-Entity geladen (mittels "getReference", um eine Exception im nicht-gefunden-Fall zu provozieren). Anschließend wird diese Entity einfach gelöscht. Hier ist kein beiderseitiges Update der verknüpften Relationship-Seiten nötig (vermutlich, weil keine Kaskadierungen zu Kuchen/Zutat definiert sind).
 
  public void removeZutatFromKuchen (Integer intKuchenId, Integer intZutatId)
  {
    //Aus Kuchen und Zutat einen PK zusammenbasteln:
    KuchenZutatPK kuchenZutatPK = new KuchenZutatPK();
    kuchenZutatPK.setKuchenId(intKuchenId);
    kuchenZutatPK.setZutatId(intZutatId);
    
    //Mapping laden:
    KuchenZutatJoinTableBean kuchen2Zutat = this.entityManager.getReference(KuchenZutatJoinTableBean.class, kuchenZutatPK);
    
    //Killen:
    this.entityManager.remove (kuchen2Zutat);

  }



Anlegen des Webclients

Hier gibt es keine großen Unterschiede zum letzten Beispiel.

An allen Stellen, wo z.B. auf die Zutatenliste des Kuchens zugegriffen wurde, erhält man jetzt eine Liste von Verknüpfungs-Entities, diese enthalten allerdings alle eine Property, um auf die verknüpfte Zutat zuzugreifen. Hier kommt es also nur zu kleinen Änderungen.
Einzige Besonderheit ist die Seite "KuchenZutaten.jsp", auf der beim Hinzufügen einer Zutat zum Kuchen ein weiteres Eingabefeld für die Menge eingebaut wurde. Dieses enthält im Namen die Kuchen-ID ("menge_123"), um beim Hinzufügen mehrere Zutaten jeweils die Mengen eingegeben zu können.
Zu beachten ist, dass der Use-Case "Ändern der Menge einer bestehenden Verknüpfung" nicht umgesetzt wurde, um das Beispiel einfach zu halten!

Die Anwendung ist unter
http://localhost:8080/KuchenZutatJoinTableWeb/index.jsp zu erreichen.


Blick in die Datenbank

In der Datenbank sieht das so aus (mit rotem Rahmen markiert ist die Tabelle der Relation):
Mapping-Tabelle
Im rechten Teil des Screenshots sind die Inhalte aller drei beteiligten Tabellen dargestellt.


Alternative: @javax.persistence.EmbeddedId

Obiger Ansatz hat einen Nachteil: die ID-Kombination muss doppelt gehalten werden, einmal in der Entity selbst, und außerdem in der Primary-Key-Klasse. Das kann bei nachträglichen Änderungen zu Problemen führen.
Aus diesem Grund wäre hier die Verwendung einer @javax.persistence.EmbeddedId eleganter. Die Codeänderungen sind minimal: Die modifizierte Version des Projekts gibt es hier:
KuchenZutatJoinTableEmbeddedId.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenZutatJoinTable-Beispiel existieren !

Ohne Annotations

In "ejb-jar.xml" gibt es 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 KuchenJoinTableBean o</query>
	</named-query>
	<named-query name="findAllZutaten">
		<query>select o from ZutatJoinTableBean o</query>
	</named-query>

	<entity class="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.KuchenJoinTableBean" 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.kuchenzutatjointable.KuchenZutatJoinTableBean">
				<cascade>
					<cascade-all/>
				</cascade>
			</many-to-many>
		</attributes>
	</entity>

	<entity class="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.ZutatJoinTableBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<id name="id">
				<generated-value />
			</id>
			<basic name="zutatName">
			</basic>
			<many-to-many name="kuchen" mapped-by="zutat" fetch="LAZY"
				target-entity="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.KuchenZutatJoinTableBean">
				<cascade>
					<cascade-all/>
				</cascade>
			</many-to-many>
		</attributes>
	</entity>
	
	<entity class="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.KuchenZutatJoinTableBean" access="PROPERTY"
		metadata-complete="true">
		<id-class class="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.KuchenZutatPK"/>
		<attributes>
			<id name="kuchenId">
				<column name="KUCHENID"/>
			</id>
			<id name="zutatId">
				<column name="ZUTATID"/>
			</id>
			<basic name="menge">
			</basic>
			
			<many-to-one name="kuchen" 
				target-entity="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.KuchenJoinTableBean">
				<join-column name="KUCHENID" insertable="false" updatable="false" nullable="false"/>
			</many-to-one>
			
			<many-to-one name="zutat"
				target-entity="de.fhw.komponentenarchitekturen.knauf.kuchenzutatjointable.ZutatJoinTableBean">
				<join-column name="ZUTATID" insertable="false" updatable="false" nullable="false"/>
			</many-to-one>
		</attributes>
	</entity>
</entity-mappings> 
Neue Elemente in diesem Beispiel sind in der Deklaration der KuchenZutatJoinTableBean zu finden:
Die modifizierte Version des Projekts gibt es hier:
KuchenZutatJoinTableNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenZutatJoinTable-Beispiel existieren !



Stand 02.06.2009
Historie:
02.06.2009: Beispiel erstellt