Blog

JPA-Implementierungsmuster: Speichern von (abgetrennten) Entitäten

Vincent Partington

Aktualisiert Oktober 23, 2025
7 Minuten

Wir haben unsere Suche nach JPA-Implementierungsmustern mit dem Data Access Object-Muster begonnen und mit der Diskussion über die Verwaltung bidirektionaler Assoziationen fortgesetzt. Diese Woche befassen wir uns mit einem Thema, das auf den ersten Blick trivial erscheinen mag: wie man eine Entität speichert. Das Speichern einer Entität in JPA ist einfach, oder? Wir übergeben einfach das Objekt, das wir persistieren möchten, an EntityManager.persist. Das scheint alles ganz gut zu funktionieren, bis wir auf die gefürchtete Meldung "detached entity passed to persist" stoßen. Oder eine ähnliche Meldung, wenn wir einen anderen JPA-Anbieter als den Hibernate EntityManager verwenden.

Was ist also die losgelöste Entität, von der in der Nachricht die Rede ist? Eine abgetrennte Entität (auch als abgetrenntes Objekt bezeichnet) ist ein Objekt, das dieselbe ID wie eine Entität im Persistenzspeicher hat, aber nicht mehr Teil eines Persistenzkontexts ist (der Bereich einer EntityManager-Sitzung ). Die beiden häufigsten Ursachen hierfür sind:

  • Der EntityManager, aus dem das Objekt abgerufen wurde, wurde geschlossen.
  • Das Objekt wurde von außerhalb unserer Anwendung empfangen, z.B. als Teil einer Formularübermittlung, eines Remoting-Protokolls wie Hessian oder über einen BlazeDS AMF Channel von einem Flex-Client.

Der Vertrag für persist (siehe Abschnitt 3.2.1 der JPA 1.0-Spezifikation) besagt ausdrücklich, dass die persist-Methode eine EntityExistsException auslöst, wenn das übergebene Objekt eine abgetrennte Entität ist. Oder jede andere PersistenceException ausgelöst wird, wenn der Persistenzkontext geleert oder die Transaktion übertragen wird. Beachten Sie, dass es kein Problem ist, dasselbe Objekt zweimal innerhalb einer Transaktion zu persistieren. Der zweite Aufruf wird einfach ignoriert, obwohl die Persist-Operation möglicherweise auf alle Assoziationen der Entität kaskadiert wird, die seit dem ersten Aufruf hinzugefügt wurden. Abgesehen von der letztgenannten Überlegung besteht keine Notwendigkeit, EntityManager.persist für eine bereits persistierte Entität aufzurufen, da alle Änderungen beim Flush oder Commit automatisch gespeichert werden.

saveOrUpdate vs. Zusammenführen

Diejenigen unter Ihnen, die mit einfachem Hibernate gearbeitet haben, werden sich wahrscheinlich schon an die Verwendung der Session.saveOrUpdate Methode zu verwenden, um Entitäten zu speichern. Die saveOrUpdate-Methode stellt fest, ob das Objekt neu ist oder schon einmal gespeichert wurde. Im ersten Fall wird die Entität gespeichert, im letzteren Fall wird sie aktualisiert.Beim Wechsel von Hibernate zu JPA sind viele Leute bestürzt, dass diese Methode fehlt. Die naheliegendste Alternative scheint die Methode EntityManager.merge Methode, aber es gibt einen großen Unterschied, der wichtige Auswirkungen hat. Die Methode Session.saveOrUpdate und ihr Cousin Session.update fügen die übergebene Entität an den Persistenzkontext an, während die Methode EntityManager.merge den Zustand des übergebenen Objekts in die persistente Entität mit demselben Bezeichner kopiert und dann eine Referenz auf diese persistente Entität zurückgibt. Das übergebene Objekt ist nicht mit dem Persistenzkontext verbunden. Das bedeutet, dass wir nach dem Aufruf von EntityManager.merge die von dieser Methode zurückgegebene Entitätsreferenz anstelle des ursprünglich übergebenen Objekts verwenden müssen. Dies steht im Gegensatz zu der Art und Weise, wie man einfach EntityManager.persist für ein Objekt aufrufen kann (sogar mehrfach, wie oben erwähnt!), um es zu speichern und das ursprüngliche Objekt weiter zu verwenden. Hibernate's Session.saveOrUpdate teilt dieses nette Verhalten mit EntityManager.persist (oder eher Session.save) auch bei der Aktualisierung, hat aber einen großen Nachteil: Wenn eine Entität mit der gleichen ID wie diejenige, die wir zu aktualisieren, d.h. neu anzuhängen versuchen, bereits Teil des Persistenzkontexts ist, wird eine NonUniqueObjectException ausgelöst. Und herauszufinden, welcher Teil des Codes diese andere Entität persistiert (oder zusammengeführt oder abgerufen) hat, ist schwieriger als herauszufinden, warum wir die Meldung "detached entity passed to persist" erhalten.

Alles zusammenfügen

Lassen Sie uns also die drei möglichen Fälle untersuchen und was die verschiedenen Methoden bewirken:

SzenarioEntityManager.persistEntityManager.mergeSessionManager.saveOrUpdate
Übergebenes Objekt wurde nie persistiert1. Objekt wurde dem Persistenzkontext als neue Entität hinzugefügt 2. Neue Entität wird beim Flush/Commit in die Datenbank eingefügt1. Zustand wird in die neue Entität kopiert. 2. Neue Entität wird zum Persistenzkontext hinzugefügt 3. Neue Entität beim Flush/Commit in die Datenbank eingefügt 4. Neue Entität zurückgegeben1. Objekt wird dem Persistenzkontext als neue Entität hinzugefügt 2. Neue Entität bei Flush/Commit in die Datenbank eingefügt
Objekt wurde zuvor persistiert, aber nicht in diesen Persistenzkontext geladen1. EntityExistsException ausgelöst (oder eine PersistenceException beim Flush/Commit)2. Vorhandene Entität geladen. 2. Status von Objekt zu geladener Entität kopiert 3. Geladene Entität wird beim Flush/Commit in der Datenbank aktualisiert 4. Geladene Entität zurückgegeben1. Objekt zum Persistenzkontext hinzugefügt 2. Geladene Entität wird beim Flush/Commit in der Datenbank aktualisiert
Objekt wurde zuvor persistiert und bereits in diesen Persistenzkontext geladen1. EntityExistsException ausgelöst (oder eine PersistenceException zum Zeitpunkt des Flush oder Commit)1. Zustand des Objekts wird in die geladene Entität kopiert 2. Geladene Entität wird beim Flush/Commit in der Datenbank aktualisiert 3. Geladene Entität zurückgegeben1. NonUniqueObjectException ausgelöst

Wenn man sich diese Tabelle ansieht, versteht man vielleicht, warum die saveOrUpdate-Methode nie Teil der JPA-Spezifikation wurde und warum sich die JSR-Mitglieder stattdessen für die Merge-Methode entschieden haben. Übrigens finden Sie im Blog von Stevi Deter einen anderen Blickwinkel auf das saveOrUpdate vs. merge Problem.

Das Problem mit der Zusammenführung

Bevor wir fortfahren, müssen wir einen Nachteil der Funktionsweise von EntityManager.merge erörtern: Bidirektionale Verknüpfungen können leicht unterbrochen werden. Betrachten Sie das Beispiel mit den Klassen Order und OrderLine aus dem vorigen Blog in dieser Serie. Wenn ein aktualisiertes OrderLine-Objekt von einem Web-Frontend (oder von einem Hessian-Client oder einer Flex-Anwendung usw.) empfangen wird, wird das Order-Feld möglicherweise auf Null gesetzt. Wenn dieses Objekt dann mit einer bereits geladenen Entität zusammengeführt wird, wird das Auftragsfeld dieser Entität auf Null gesetzt. Es wird jedoch nicht aus der orderLines Satz des Bestellung Dadurch wird die Invariante gebrochen, dass das Ordnungsfeld jedes Elements in der orderLines-Menge einer Order so eingestellt ist, dass es auf diese Order zurückweist. . In diesem Fall oder in anderen Fällen, in denen die vereinfachte Art und Weise, wie EntityManager.merge den Objektstatus in die geladene Entität kopiert, Probleme verursacht, können wir auf das DIY-Merge-Muster zurückgreifen. Anstatt EntityManager.merge aufzurufen, rufen wir EntityManager.find auf, um die vorhandene Entität zu finden und den Status selbst zu kopieren. Wenn EntityManager.find null zurückgibt, können wir entscheiden, ob wir das erhaltene Objekt beibehalten oder eine Ausnahme auslösen wollen. Angewandt auf die Klasse Order könnte dieses Muster wie folgt implementiert werden:

  Order existingOrder = dao.findById(receivedOrder.getId());
  if(existingOrder == null) {
  dao.persist(receivedOrder);
  } sonst {
  existingOrder.setCustomerName(receivedOrder.getCustomerName());
  existingOrder.setDate(receivedOrder.getDate());
  }

Das Muster

Was bedeutet das alles für uns? Die Faustregel, an die ich mich halte, lautet wie folgt:

  • Wenn und nur wenn (und vorzugsweise wo) wir eine neue Entität erstellen, rufen Sie EntityManager.persist auf, um sie zu speichern. Das macht durchaus Sinn, wenn wir unsere Domänenzugriffsobjekte als Sammlungen betrachten. Ich nenne dies das persist-on-new Muster.
  • Wenn wir eine bestehende Entität aktualisieren, rufen wir keine EntityManager-Methode auf. Der JPA-Anbieter aktualisiert die Datenbank automatisch zum Zeitpunkt des Flush oder Commit.
  • Wenn wir eine aktualisierte Version einer bestehenden einfachen Entität (eine Entität ohne Verweise auf andere Entitäten) von außerhalb unserer Anwendung erhalten und den neuen Zustand speichern möchten, rufen wir EntityManager.merge auf, um diesen Zustand in den Persistenzkontext zu kopieren. Aufgrund der Art und Weise, wie das Zusammenführen funktioniert, können wir dies auch tun, wenn wir uns nicht sicher sind, ob das Objekt bereits persistiert wurde.
  • Wenn wir mehr Kontrolle über den Zusammenführungsprozess benötigen, verwenden wir das DIY-Zusammenführungsmuster.

Ich hoffe, dieser Blog gibt Ihnen einige Hinweise darauf, wie Sie Entitäten speichern und wie Sie mit abgetrennten Entitäten arbeiten können. Wir werden auf abgetrennte Entitäten zurückkommen, wenn wir in einem späteren Blog über Data Transfer Objects sprechen. In der nächsten Woche werden wir uns jedoch zunächst mit einer Reihe gängiger Abfragemuster für Entitäten befassen. In der Zwischenzeit freuen wir uns über Ihr Feedback. Was sind Ihre JPA-Muster? 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.