Blog

JPA-Implementierungsmuster: Bidirektionale Assoziationen

Vincent Partington

Aktualisiert Oktober 23, 2025
8 Minuten

Letzte Woche haben wir unsere Suche nach JPA-Implementierungsmustern mit dem Data Access Object-Muster begonnen. Diese Woche machen wir mit einem anderen haarigen Thema weiter. JPA bietet die Annotationen @OneToMany, @ManyToOne, @OneToOne und @ManyToMany, um Assoziationen zwischen Objekten abzubilden. Während EJB 2.x containerverwaltete Beziehungen zur Verwaltung dieser Assoziationen und insbesondere zur Synchronisierung bidirektionaler Assoziationen anbot, überlässt JPA mehr dem Entwickler.

Die Einrichtung

Beginnen wir mit der Erweiterung des Order-Beispiels aus dem vorherigen Blog um ein OrderLine-Objekt. Es hat eine ID, eine Beschreibung, einen Preis und einen Verweis auf die Bestellung, die es enthält:

@Entität
public class OrderLine {
  @Id
  @GeneratedValue
  private int id;
  private String description;
  private int Preis;
  @ManyToOne
  private Order bestellen;
  public int getId() { return id; }
  public void setId(int id) { this.id = id; }
  public String getDescription() { return description; }
  public void setDescription(String description) { this.description = description; }
  public int getPreis() { return preis; }
  public void setPrice(int price) { this.price = price; }
  public Order getOrder() { return order; }
  public void setOrder(Order order) { this.order = order; }
}

Mithilfe des generischen DAO-Musters erhalten wir schnell eine sehr einfache OrderLineDao-Schnittstelle und eine Implementierung:

public interface OrderLineDao extends Dao {
  public List findOrderLinesByOrder(Order o);
}
public class JpaOrderLineDao extends JpaDao implements
  OrderLineDao {
  public Liste findOrderLinesByOrder(Order o) {
  Abfrage q = entityManager.createQuery("SELECT e FROM "
  + entityClass.getName() + " e WHERE order = :entity");
  q.setParameter("entity", o);
  return (Liste) q.getResultList();
  }
}

Wir können diese DAO verwenden, um eine Bestellzeile zu einer Bestellung hinzuzufügen oder um alle Bestellzeilen für eine Bestellung zu finden:

  OrderLine line = new OrderLine();
  line.setDescription("Java Persistence mit Hibernate");
  line.setPrice(5999);
  line.setOrder(o);
  orderLineDao.persist(line);
  Collection lines = orderLineDao.findOrderLinesByOrder(o);

Mehr Vereine, mehr Probleme

All dies ist ziemlich einfach, aber interessant wird es, wenn wir diese Assoziation bidirektional machen. Fügen wir unserem Order-Objekt ein orderLines-Feld hinzu und fügen eine naive Implementierung des Getter/Setter-Paares hinzu:

  @OneToMany(mappedBy = "Bestellung")
  private Set orderLines = new HashSet();
  public Set getOrderLines() { return orderLines; }
  public void setOrderLines(Set orderLines) { this.orderLines = orderLines; }

Die mappedBy Feld auf der tt>@OneToMany Annotation teilt JPA mit, dass dies die Rückseite einer Assoziation ist. Anstatt dieses Feld direkt einer Datenbankspalte zuzuordnen, kann es das Order-Feld eines OrderLine-Objekts betrachten, um zu wissen, zu welchem Order-Objekt es gehört. Ohne die zugrundeliegende Datenbank zu ändern, können wir nun die Auftragszeilen für einen Auftrag wie diesen abrufen:

  Collection lines = o.getOrderLines();

Sie müssen nicht mehr auf den OrderLineDao zugreifen :-)Aber es gibt einen Haken! Während die von EJB 2.x definierten container managed relationships (CMR) dafür sorgen, dass das Hinzufügen eines OrderLine-Objekts zur orderLines-Eigenschaft einer Order auch die order-Eigenschaft dieser OrderLine setzt (und umgekehrt), führt JPA (als POJO-Framework) keine solche Magie aus. Das ist eigentlich eine gute Sache, denn es macht unsere Domänenobjekte auch außerhalb eines JPA-Containers nutzbar, so dass Sie sie leichter testen und verwenden können, wenn sie (noch) nicht persistiert wurden. Aber es kann auch verwirrend sein für Leute, die das CMR-Verhalten von EJB 2.x gewohnt sind. Wenn Sie die obigen Beispiele in separaten Transaktionen ausführen, werden Sie feststellen, dass sie korrekt laufen. Wenn Sie sie jedoch innerhalb einer Transaktion ausführen, wie es der nachstehende Code tut, werden Sie feststellen, dass die Artikelliste zwar leer ist:

  Bestellung o = new Bestellung();
  o.setCustomerName("Mary Jackson");
  o.setDate(new Datum());
  OrderLine line = new OrderLine();
  line.setDescription("Java Persistence mit Hibernate");
  line.setPrice(5999);
  line.setOrder(o);
  System.out.println("Artikel bestellt von " + o.getCustomerName() + ": ");
  Collection lines = o.getOrderLines();
  for (OrderLine each : lines) {
  System.out.println(jede.getId() + ": " + jede.getDescription()
  + " zu $" + each.getPrice());
  }

Dies kann behoben werden, indem Sie die folgende Zeile vor der ersten System.out.println-Anweisung hinzufügen:

  o.getOrderLines().add(line);

Reparieren und reparieren...

Es funktioniert, aber es ist nicht sehr schön. Es bricht die Abstraktion auf und ist brüchig, da es vom Benutzer unserer Domänenobjekte abhängt, diese Setter und Addierer korrekt aufzurufen. Wir können dies beheben, indem wir diesen Aufruf in die Definition von OrderLine.setOrder(Order) verschieben:

  public void setOrder(Order order) {
  this.order = order;
  order.getOrderLines().add(this);
  }

Wenn Sie die Eigenschaft orderLines des Objekts Order noch besser kapseln können:

  public Set getOrderLines() { return orderLines; }
  public void addOrderLine(OrderLine line) { orderLines.add(line); }

Und dann können wir OrderLine.setOrder(Order) wie folgt umdefinieren:

  public void setOrder(Order order) {
  this.order = order;
  order.addOrderLine(this);
  }

Sind Sie immer noch bei mir? Ich hoffe es, aber wenn nicht, probieren Sie es bitte aus und überzeugen Sie sich selbst.Jetzt taucht ein weiteres Problem auf. Was passiert, wenn jemand direkt die Methode Order.addOrderLine(OrderLine) aufruft? Die OrderLine wird der orderLines-Sammlung hinzugefügt, aber ihre order-Eigenschaft zeigt nicht auf die dazugehörige Order. Das Ändern von Order.addOrderLine(OrderLine) wie unten funktioniert nicht, da es zu einer Endlosschleife führt, in der addOrderLine setOrder aufruft und addOrderLine setOrder aufruft usw.:

  public void addOrderLine(OrderLine line) {
  orderLines.add(line);
  line.setOrder(this);
  }

Dieses Problem lässt sich lösen, indem Sie eine Methode Order.internalAddOrderLine(OrderLine) einführen, die die Zeile nur zur Sammlung hinzufügt, aber line.setOrder(this) nicht aufruft. Diese Methode wird dann von OrderLine.setOrder(Order) aufgerufen und führt nicht zu einer Endlosschleife. Benutzer der Klasse Order sollten Order.addOrderLine(OrderLine) aufrufen.

Das Muster

Wenn wir diese Idee zu Ende denken, kommen wir zu diesen Methoden für die Klasse OrderLine:

  public Order getOrder() { return order; }
  public void setOrder(Order order) {
  if (this.order != null) { this.order.internalRemoveOrderLine(this); }
  this.order = order;
  if (order != null) { order.internalAddOrderLine(this); }
  }

Und diese Methoden für die Klasse Order:

  public Set getOrderLines() { return Collections.unmodifiableSet(orderLines); }
  public void addOrderLine(OrderLine line) { line.setOrder(this); }
  public void removeOrderLine(OrderLine line) { line.setOrder(null); }
  public void internalAddOrderLine(OrderLine line) { orderLines.add(line); }
  public void internalRemoveOrderLine(OrderLine line) { orderLines.remove(line); }

Diese Methoden bieten eine POJO-basierte Implementierung der CMR-Logik, die in EJB 2.x eingebaut wurde. Mit den typischen POJO-Vorteilen, dass sie leichter zu verstehen, zu testen und zu warten sind. Natürlich gibt es eine Reihe von Variationen zu diesem Thema:

  • Wenn sich Order und OrderLine im selben Paket befinden, können Sie den internen... Methoden Paketumfang geben, um zu verhindern, dass sie versehentlich aufgerufen werden. (An dieser Stelle wäre das Konzept der Freundesklassen von C++ sehr nützlich. Aber lassen wir das lieber. ;-) ).
  • Sie können auf die Methoden removeOrderLine und internalRemoveOrderLine verzichten, wenn die Bestellzeilen nie aus einer Bestellung entfernt werden sollen.
  • Sie können die Verantwortung für die Verwaltung der bidirektionalen Assoziation von der Methode OrderLine.setOrder(Order) in die Klasse Order verlagern und damit die Idee im Grunde umdrehen. Aber das würde bedeuten, dass Sie die Logik auf die Methoden addOrderLine und removeOrderLine verteilen müssten.
  • Anstatt oder zusätzlich zur Verwendung von Collections.singletonSet, um den OrderLine-Satz zur Laufzeit schreibgeschützt zu machen, können Sie auch generische Typen verwenden, um ihn zur Kompilierungszeit schreibgeschützt zu machen:
    public Set getOrderLines() { return Collections.unmodifiableSet(orderLines); }
    Dies macht es jedoch schwieriger, diese Objekte mit einem Mocking-Framework wie EasyMock zu spotten.

Es gibt auch einige Dinge zu beachten, wenn Sie dieses Muster verwenden:

  • Wenn Sie eine OrderLine zu einer Order hinzufügen, wird sie nicht automatisch persistiert. Dazu müssen Sie auch die persist-Methode für die DAO (oder den EntityManager) aufrufen. Oder Sie können die Kaskadeneigenschaft der @OneToMany-Annotation der Eigenschaft Order.orderLines auf KaskadenTyp.PERSIST setzen (mindestens), um dies zu erreichen. Mehr dazu erfahren Sie, wenn wir die EntityManager.persist Methode besprechen.
  • Bidirektionale Assoziationen funktionieren nicht gut mit der Funktion EntityManager.merge Methode. Wir werden dies besprechen, wenn wir zum Thema abgetrennte Objekte kommen.
  • Wenn eine Entität, die Teil einer bidirektionalen Assoziation ist, (demnächst) entfernt wird, sollte sie auch vom anderen Ende der Assoziation entfernt werden. Dies wird auch zur Sprache kommen, wenn wir über die Funktion EntityManager.remove Methode sprechen.
  • Das obige Muster funktioniert nur, wenn Sie den Feldzugriff (anstelle des Zugriffs auf Eigenschaften/Methoden) verwenden, damit Ihr JPA-Anbieter Ihre Entitäten auffüllen kann. Feldzugriff wird verwendet, wenn die @Id Annotation Ihrer Entität auf das entsprechende Feld und nicht auf den entsprechenden Getter gesetzt wird. Ob Sie den Feldzugriff oder den Eigenschafts-/Methodenzugriff bevorzugen, ist eine umstrittene Frage, auf die ich in einem späteren Blog zurückkommen werde.
  • Und zu guter Letzt: Auch wenn es sich bei diesem Muster um eine technisch solide POJO-basierte Implementierung von verwalteten Assoziationen handelt, können Sie darüber streiten, warum Sie all diese Getter und Setter benötigen. Warum sollten Sie in der Lage sein, sowohl Order.addOrderLine(OrderLine) als auch OrderLine.setOrder(Order) zu verwenden, um das gleiche Ergebnis zu erzielen? Der Verzicht auf eines dieser Elemente könnte unseren Code einfacher machen. Siehe zum Beispiel den Artikel von James Holub über Getter und Setter. Andererseits haben wir festgestellt, dass dieses Muster Entwicklern, die diese Domänenobjekte verwenden, die Flexibilität gibt, sie nach Belieben zuzuordnen.

Nun, das war's für heute. Ich bin sehr gespannt auf Ihr Feedback zu diesem Thema und darauf, wie Sie bidirektionale Assoziationen in Ihren JPA-Domänenobjekten verwalten. Und natürlich: Wir sehen uns beim nächsten Pattern! Eine Liste aller Blogs zu JPA-Implementierungsmustern finden Sie in der Zusammenfassung der JPA-Implementierungsmuster.

Verfasst von

Vincent Partington

Contact

Let’s discuss how we can support your journey.