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:
- Es wurden explizite Spaltennamen durch eine
@Column
-Annoation deklariert. Zur Begründung siehe unten.
- Die getter und setter sind private. Grund hierfür: ein Verwender soll nie direkt mit den IDs arbeiten, sondern immer nur über Kuchen- und Zutat-Objekte
mit der Relation arbeiten. Da eine ungenutzte private Methode eine Warnung verursacht, wurden sie mit der Annotation
@SuppressWarnings("unused")
versehen.
Warum brauch man diese Properties überhaupt, da sie doch sowieso ungenutzt sind? Eigentlich wäre es auch möglich, die Felddeklarationen direkt auf den
Membervariablen vorzunehmen. Dies führte allerdings dazu, dass auch das Feld "Menge" nicht mehr als Property sondern als Attribut behandelt wurde und
deshalb die Annotations auf Property-Ebene ignoriert wurden. Außerdem ist es mir nicht gelungen, Spaltennamen für die ID-Felder anzugeben, dadurch funktionierte
das gesamte Beispiel nicht mehr.
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:
- Im Setter
setKuchen
und setZutat
wird die ID des übergebenen Kuchens/Zutat in die jeweilige ID-Membervariable gepackt. Dieses ID-setzen
ist NULL-sicher, obwohl es in meiner Beispielanwendung nicht nötig ist.
- Wir müssen dem Server sagen, dass er für die Relationsfelder keine eigenen Spalten in der Datenbank erzeugen soll.´
Dies geschieht über die Annotation
javax.persistence.JoinColumn
. Hier wird der Name der ID-Spalte angegeben, wodurch sichergestellt ist, dass beim Initialisieren
der Entity der Wert dieser ID-Spalte auch für das Laden der Relation verwendet wird.
Außerdem soll der Server die Werte nicht in Update-Statements packen. Dies geschieht durch die Attribute insertable
und updateable
.
Anmerkung: lassen wir diese Attribute weg, so führt das zu folgender Exception:
java.sql.SQLException: Column count does not match in statement [insert into KuchenZutatJoinTableBean (KUCHENID, menge, ZUTATID, kuchenId, zutatId) values (?, ?, ?, ?, ?)]
at org.hsqldb.jdbc.Util.throwError(Unknown Source)
at org.hsqldb.jdbc.jdbcPreparedStatement.(Unknown Source)
at org.hsqldb.jdbc.jdbcConnection.prepareStatement(Unknown Source)
...
Man sieht, dass die Spalten "KUCHENID" und "kuchenId" doppelt auftauchen und dies zu einem Datenbankfehler führt.
Das Attribut nullable
ist nur der Vollständigkeit halber definiert und gibt an, dass kein NULL kommen darf (da es sich um eine ID-Spalte handelt).
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:
- Das Speichern einer Verknüpfung schlägt fehl mit folgender Fehlermeldung:
14:32:44,281 WARN [JDBCExceptionReporter] SQL Error: 0, SQLState: null
14:32:44,281 ERROR [JDBCExceptionReporter] failed batch
14:32:44,281 ERROR [AbstractFlushingEventListener] Could not synchronize database state with session
org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:126)
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:114)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:275)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:266)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:167)
...
Caused by: java.sql.BatchUpdateException: failed batch
at org.hsqldb.jdbc.jdbcStatement.executeBatch(Unknown Source)
at org.hsqldb.jdbc.jdbcPreparedStatement.executeBatch(Unknown Source)
at org.jboss.resource.adapter.jdbc.CachedPreparedStatement.executeBatch(CachedPreparedStatement.java:476)
at org.jboss.resource.adapter.jdbc.WrappedStatement.executeBatch(WrappedStatement.java:774)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
... 65 more
Die Erklärung findet sich, wenn man SQL-Logging angeschaltet hat. Es wird nämlich folgendes Statement abgefeuert:
insert into KuchenZutatJoinTableBean (menge, KUCHENID) values (?, ?)
Die zweite ID-Spalte "ZUTATID" fehlt komplett.
- Das Laden einer Entity anhand der ID ist nicht möglich (Methode
removeZutatFromKuchen
der Session Bean, siehe unten).
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):
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 Primary-Key-Klasse
KuchenZutatPK
erhält die Annotation @javax.persistence.Embeddable
:
import javax.persistence.Column;
import javax.persistence.Embeddable;
@Embeddable
public class KuchenZutatPK implements Serializable
{
...
@Column(name="KUCHENID")
public Integer getKuchenId()
{
return this.intKuchenId;
}
...
@Column(name="ZUTATID")
public Integer getZutatId()
{
return this.intZutatId;
}
Außerdem müssen die @javax.persistence.Column
-Annotations in diese Klasse geschoben werden!
- In der Entity
KuchenZutatJoinTableBean
entfallen die beiden Felder "intKuchenId" und "intZutatId", stattdessen gibt es eine Membervariable
vom Typ der Primary-Key-Klasse. Ihre Property wird mit der Annotation javax.persistence.EmbeddedId
versehen:
import javax.persistence.EmbeddedId;
...
private KuchenZutatPK pk;
@EmbeddedId
@SuppressWarnings("unused")
private KuchenZutatPK getPk()
{
return this.pk;
}
@SuppressWarnings("unused")
private void setPk(KuchenZutatPK pk)
{
this.pk = pk;
}
Auch diese Property hat nur private getter/setter. Dadurch ergibt sich der angenehme Zustand, dass kein fremder Code darauf zugreifen konnte und deshalb keine Änderungen außerhalb
der Entity Bean nötig sind (sprich: die Klasse kapselt ihre interne Implementierung des PK sauber).
Nur im Setter von Kuchen und Zutat, wo im vorherigen Beispiel direkt die ID-Membervariable gesetzt werden konnte, muss der Code angepaßt werden: eventuell wird die PK-Klasse
erzeugt, und die ID wird danach in diese geschrieben:
public void setKuchen (KuchenJoinTableBean kuchen)
{
this.kuchen = kuchen;
//Kuchen-ID außerdem in Membervariable übernehmen.
//Eventuell wird PK-Klasse erzeugt:
if (this.pk == null)
{
this.pk = new KuchenZutatPK();
}
//Für den Fall "NULL" wird die ID auf "NULL" gesetzt.
if (kuchen != null)
{
this.pk.setKuchenId(kuchen.getId());
}
else
{
this.pk.setKuchenId(null);
}
}
public void setZutat (ZutatJoinTableBean zutat)
{
this.zutat = zutat;
//Zutat-ID außerdem in Membervariable übernehmen.
//Eventuell wird PK-Klasse erzeugt:
if (this.pk == null)
{
this.pk = new KuchenZutatPK();
}
//Für den Fall "NULL" wird die ID auf "NULL" gesetzt.
if (zutat != null)
{
this.pk.setZutatId(zutat.getId());
}
else
{
this.pk.setZutatId(null);
}
}
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:
- Deklaration der
<id-class>
- Deklaration der Spaltennamen der ID-Spalten
- Deklaration der
<join-column>
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