Blog

JPA-Implementierungsmuster: Entitäten entfernen

Vincent Partington

Aktualisiert Oktober 23, 2025
6 Minuten

In den letzten Wochen habe ich mich mit den Implementierungsmustern beschäftigt, die ich beim Schreiben von JPA-Anwendungen entdeckt habe. In den letzten beiden Blogs ging es um das Speichern von Entitäten und das Abrufen von Entitäten. Aber wenn Sie mit Ihren Entitäten wirklich fertig sind, werden Sie sie wohl auch wieder entfernen wollen. ;-) Das ist also das Thema dieses Blogs. Genau wie das Abrufen einer Entität ist auch das Entfernen einer Entität ziemlich einfach. Alles, was Sie tun müssen, ist, die Entität an die Funktion EntityManager.remove Methode zu übergeben, um die Entität aus der Datenbank zu entfernen, wenn die Transaktion abgeschlossen ist (natürlich würden Sie eine remove-Methode in Ihrer DAO aufrufen, die wiederum EntityManager.remote aufruft). Das ist alles, was Sie tun müssen. Normalerweise. Denn wenn Sie Assoziationen verwenden (ob sie nun bidirektional sind oder nicht), wird es noch interessanter.

Entfernen von Entitäten, die Teil einer Assoziation sind

Betrachten Sie das Beispiel mit dem Bestellung und OrderLine Klassen, die wir zuvor besprochen haben. Nehmen wir an, wir möchten eine OrderLine aus einer Order entfernen und gehen dabei auf diese einfache Weise vor:

orderLineDao.remove(lineToDelete);

Es gibt ein Problem mit diesem Code. Wenn Sie den Entity Manager anweisen, die Entität zu entfernen, wird sie nicht automatisch aus allen Assoziationen, die auf sie verweisen, entfernt. Genauso wie JPA nicht automatisch bidirektionale Assoziationen verwaltet. In diesem Fall wären das die orderLines, die in dem Order-Objekt festgelegt sind, auf das die Eigenschaft OrderLine.order verweist. Wenn ich diese Aussage als einen fehlgeschlagenen JUnit-Testfall formulieren müsste, wäre es dieser:

OrderLine orderLineToRemove = orderLineDao.findById(someOrderLineId);
Order parentOrder = orderLineToRemove.getOrder();
int sizeBeforeRemoval = parentOrder.getOrderLines().size();
orderLineDao.remove(orderLineToRemove);
assertEquals(sizeBeforeRemoval - 1, parentOrder.getOrderLines().size());

Auswirkungen

Das Scheitern dieses Testfalls hat zwei subtile und daher unangenehme Auswirkungen:

  • Jeder Code, der das Order-Objekt verwendet , nachdem wir die OrderLine entfernt haben, wird die entfernte OrderLine weiterhin sehen. Erst wenn Sie die Transaktion bestätigen, eine neue Transaktion starten und die Order in einer neuen Transaktion erneut laden, wird sie nicht mehr im Order.orderLines-Satz auftauchen. In einfachen Szenarien werden wir dieses Problem nicht haben, aber wenn die Dinge komplexer werden, können wir vom Auftauchen dieser "Zombie" -Bestellungenüberrascht werden.
  • Wenn die Operation PERSIST von der Klasse Order auf die Assoziation Order.orderLines kaskadiert wird und das enthaltende Order-Objekt nicht in derselben Transaktion entfernt wird, erhalten wir einen Fehler wie "deleted entity passed to persist". Anders als der Fehler "detached entity passed to persist", über den wir in einem früheren Blog gesprochen haben, wird dieser Fehler dadurch verursacht, dass das Order-Objekt eine Referenz auf ein bereits gelöschtes OrderLine-Objekt hat. Dieser Verweis wird dann entdeckt, wenn der JPA-Anbieter die Entitäten im Persistenzkontext in die Datenbank spült und versucht, die bereits gelöschte Entität zu persistieren. Und daher erscheint der Fehler.

Die einfache Lösung

Um dieses Problem zu beheben, müssen wir auch die OrderLine aus der Menge Order.orderLines entfernen. Das kommt mir schrecklich bekannt vor... In der Tat mussten wir bei der Verwaltung bidirektionaler Assoziationen auch sicherstellen, dass sich beide Seiten der Assoziation in einem konsistenten Zustand befinden. Und das bedeutet, dass wir das Muster, das wir dort verwendet haben, wiederverwenden können. Wenn Sie dem Test einen Aufruf von orderLineToRemove.setOrder(null); hinzufügen, wird er erfolgreich sein:

OrderLine orderLineToRemove = orderLineDao.findById(someOrderLineId);
Order parentOrder = orderLineToRemove.getOrder();
int sizeBeforeRemoval = parentOrder.getOrderLines().size();
orderLineToRemove.setOrder(null);
orderLineDao.remove(orderLineToRemove);
assertEquals(sizeBeforeRemoval - 1, parentOrder.getOrderLines().size());

Das Muster

Aber wenn wir so vorgehen, wird unser Code brüchig, da er von den Benutzern unserer Domänenobjekte abhängt, die richtigen Methoden aufzurufen. Dafür sollte die DAO zuständig sein. Eine sehr gute Möglichkeit, dieses Problem zu lösen, ist die Verwendung des @PreRemove Entity Lifecycle Hook wie folgt:

@Entität
public class OrderLine {
  [...]
  @PreRemove
  public void preRemove() {
  setOrder(null);
  }
}

Jetzt können wir einfach OrderLineDao.remove() aufrufen, um ein unerwünschtes OrderLine-Objekt loszuwerden. Ursprünglich wurde in diesem Blog vorgeschlagen, eine HasPreRemove-Schnittstelle mit einer preRemove-Methode einzuführen, die von der DAO aufgerufen werden würde. Aber Sakuraba hat unten kommentiert, dass die tt>@PreRemove Annotation genau das ist, was wir hier brauchen. Nochmals vielen Dank, Sakuraba!

Waisenkinder entfernen

Aber was, so fragen Sie sich, würde passieren, wenn wir die OrderLine einfach aus dem Order.orderLines-Satz entfernen würden, etwa so:

Order parentOrder = orderLineToRemove.getOrder();
parentOrder.removeOrderLine(orderLineToRemove);

? Die OrderLine würde in der Tat aus der Menge Order.orderLines entfernt werden. Und zwar nicht nur in dieser Transaktion. Wenn wir das Order-Objekt in einer neuen Transaktion erneut abrufen, würde die entfernte OrderLine immer noch nicht auftauchen. Wenn wir jedoch einen Blick in die Datenbank werfen, sehen wir, dass die OrderLine immer noch vorhanden ist. Nur das Feld OrderLine.order ist auf null gesetzt. Was wir hier sehen, ist ein "verwaistes" Set-Element. Es gibt zwei Möglichkeiten, dieses Problem zu lösen:

Die zweite Lösung ist zwar anbieterspezifisch, hat aber den Vorteil, dass Ihr Code nicht jedes Mal eine DAO-Entfernungsmethode aufrufen muss, wenn Sie eine Entität aus einem Set entfernen. Um jedoch deutlich zu machen, dass Sie eine anbieterspezifische Erweiterung verwenden, sollten Sie auf diese Annotationen mit dem vollständigen Paketnamen verweisen (wie es auch Java Persistence with Hibernate vorschlägt):

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
private Set orderLines = new HashSet();

Bitte beachten Sie, dass KaskadenTyp.ALL den Kaskadentyp DELETE_ORPHAN nicht enthält. Deshalb wird er im Beispiel hier explizit festgelegt. Wir können also feststellen, dass das Entfernen von Entitäten einfach ist, es sei denn, Sie haben es mit Assoziationen zu tun. In diesem Fall müssen Sie zusätzliche Vorkehrungen treffen, um sicherzustellen, dass die Entität von allen Objekten, die auf sie verweisen, und gleichzeitig aus der Datenbank entfernt wird. Wir sehen uns im nächsten Blog! In der Zwischenzeit können Sie gerne Ihre Anmerkungen im Kommentarbereich unten hinterlassen. P.S. Wenn Sie nächste Woche an der J-Spring-Konferenz teilnehmen, besuchen Sie meinen Vortrag über dieses Thema von 14:25 bis 15:15 Uhr (er wird allerdings auf Niederländisch sein). Oder finden Sie mich irgendwo auf der Konferenz, um über JPA zu sprechen. Wahrscheinlich werde ich mich am Stand von Xebia herumtreiben. :-) 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.