PersistentObjectException: Getrennte Entität, die von JPA und Hibernate an persist übergeben wurde


236

Ich habe ein JPA-beständiges Objektmodell, das eine Viele-zu-Eins-Beziehung enthält: Ein Accounthat viele Transactions. A Transactionhat einen Account.

Hier ist ein Ausschnitt aus dem Code:

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

Ich kann ein AccountObjekt erstellen , Transaktionen hinzufügen und das AccountObjekt korrekt beibehalten. Wenn ich jedoch eine Transaktion erstelle, ein bereits bestehendes Konto verwende und die Transaktion beibehalten habe , erhalte ich eine Ausnahme:

Auslöser: org.hibernate.PersistentObjectException: Abgetrennte Entität, die an persist übergeben wurde: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Ich kann Accountalso eine Transaktion beibehalten , die Transaktionen enthält, aber keine Transaktion mit einer Account. Ich dachte, das lag daran, dass das Accountmöglicherweise nicht angehängt ist, aber dieser Code gibt mir immer noch die gleiche Ausnahme:

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

Wie kann ich ein Transactionmit einem bereits persistierten AccountObjekt verknüpftes Objekt korrekt speichern ?


15
In meinem Fall habe ich die ID einer Entität festgelegt, die ich mit Entity Manager beibehalten wollte. Als ich den Setter für ID entfernte, funktionierte er einwandfrei.
Rushi Shah

In meinem Fall habe ich die ID nicht festgelegt, aber es gab zwei Benutzer, die dasselbe Konto verwendeten. Einer von ihnen hat eine Entität (korrekt) beibehalten, und der Fehler trat auf, als der zweite versuchte, dieselbe Entität beizubehalten, die bereits vorhanden war beharrte.
SergioFC

Antworten:


130

Dies ist ein typisches bidirektionales Konsistenzproblem. Es ist in besprochen diesem Link sowie .

Gemäß den Artikeln in den vorherigen 2 Links müssen Sie Ihre Setter auf beiden Seiten der bidirektionalen Beziehung reparieren. Ein Beispiel für die One-Seite befindet sich in diesem Link.

Ein Beispiel für die Seite "Viele" finden Sie in diesem Link.

Nachdem Sie Ihre Setter korrigiert haben, möchten Sie den Entity-Zugriffstyp als "Eigenschaft" deklarieren. Die bewährte Methode zum Deklarieren des Zugriffstyps "Eigenschaft" besteht darin, ALLE Anmerkungen aus den Elementeigenschaften in die entsprechenden Getter zu verschieben. Ein großes Wort der Vorsicht ist, keine Zugriffstypen "Feld" und "Eigenschaft" innerhalb der Entitätsklasse zu mischen, da sonst das Verhalten in den JSR-317-Spezifikationen nicht definiert ist.


2
ps: Die Annotation @Id wird im Ruhezustand verwendet, um den Zugriffstyp zu identifizieren.
Diego Plentz

2
Die Ausnahme ist: detached entity passed to persistWarum funktioniert die Verbesserung der Konsistenz? Ok, die Konsistenz wurde repariert, aber das Objekt ist immer noch getrennt.
Gilgamesch

1
@ Sam, vielen Dank für Ihre Erklärung. Aber ich verstehe immer noch nicht. Ich sehe, dass die bidirektionale Beziehung ohne 'speziellen' Setter nicht zufriedenstellend ist. Aber ich verstehe nicht, warum das Objekt abgetrennt wurde.
Gilgamesch

26
Bitte posten Sie keine Antwort, die nur mit Links antwortet.
El Mac

6
Können Sie nicht sehen, wie diese Antwort überhaupt mit der Frage zusammenhängt?
Eugen Labun

270

Die Lösung ist einfach, verwenden Sie einfach die CascadeType.MERGEanstelle von CascadeType.PERSISToder CascadeType.ALL.

Ich hatte das gleiche Problem und CascadeType.MERGEhabe für mich gearbeitet.

Ich hoffe du bist sortiert.


5
Überraschenderweise hat das auch bei mir funktioniert. Es macht keinen Sinn, da CascadeType.ALL alle anderen Kaskadentypen enthält ... WTF? Ich habe Spring 4.0.4, Spring Data JPA 1.8.0 und Hibernate 4.X .. Hat jemand irgendwelche Gedanken, warum ALLES nicht funktioniert, aber MERGE?
Vadim Kirilchuk

22
@VadimKirilchuk Das hat auch bei mir funktioniert und es macht total Sinn. Da die Transaktion PERSISTED ist, wird auch versucht, das Konto zu PERSISTIEREN. Dies funktioniert nicht, da sich das Konto bereits in der Datenbank befindet. Bei CascadeType.MERGE wird das Konto jedoch automatisch zusammengeführt.
Revolverheld

4
Dies kann passieren, wenn Sie keine Transaktionen verwenden.
Lanoxx

1
Eine andere Lösung: Versuchen Sie, kein bereits bestehendes Objekt einzufügen :)
Andrii Plotnikov

3
Danke mann. Das Einfügen eines persistenten Objekts kann nicht vermieden werden, wenn der Referenzschlüssel NICHT NULL ist. Das ist also die einzige Lösung. Danke nochmal.
Makkasi

22

Entfernen Sie die Kaskadierung aus der untergeordneten EntitätTransaction . Es sollte nur Folgendes sein:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

( FetchType.EAGERkann entfernt werden, ebenso wie die Standardeinstellung für @ManyToOne)

Das ist alles!

Warum? Wenn TransactionSie für die untergeordnete Entität "cascade ALL" sagen, müssen Sie sicherstellen, dass jede DB-Operation an die übergeordnete Entität weitergegeben wird Account. Wenn Sie dies dann tun persist(transaction), persist(account)wird auch aufgerufen.

Es dürfen jedoch nur vorübergehende (neue) Entitäten übergeben werden persist( Transactionin diesem Fall). Die getrennten (oder anderen nicht vorübergehenden) Zustände können dies nicht ( Accountin diesem Fall, da es sich bereits in der Datenbank befindet).

Daher erhalten Sie die Ausnahme "getrennte Entität übergeben, um zu bestehen" . Die AccountEntität ist gemeint! Nicht das, was Transactiondu anrufst persist.


Sie möchten im Allgemeinen nicht vom Kind zum Elternteil weitergeben. Leider gibt es viele Codebeispiele in Büchern (auch in guten) und im Internet, die genau das tun. Ich weiß nicht warum ... Vielleicht manchmal einfach immer und immer wieder kopiert, ohne viel nachzudenken ...

remove(transaction)Ratet mal, was passiert, wenn Sie immer noch "Cascade ALL" in diesem @ManyToOne haben? Die account(übrigens mit allen anderen Transaktionen!) Werden ebenfalls aus der DB gelöscht. Aber das war nicht deine Absicht, oder?


Ich möchte nur hinzufügen, wenn Sie wirklich beabsichtigen, das Kind zusammen mit dem Elternteil zu speichern und auch das Elternteil zusammen mit dem Kind zu löschen, wie z. B. Person (Elternteil) und Adresse (Kind) zusammen mit der von der DB automatisch generierten Adress-ID Ein Aufruf zum Speichern der Adresse in Ihrer Transaktionsmethode. Auf diese Weise wird es zusammen mit der ebenfalls von der DB generierten ID gespeichert. Keine Auswirkung auf die Leistung, da Hibenate immer noch zwei Abfragen durchführt. Wir ändern lediglich die Reihenfolge der Abfragen.
Vikky

Wenn wir nichts übergeben können, welchen Standardwert es dann für alle Fälle annimmt.
Dhwanil Patel

15

Die Verwendung von Merge ist riskant und schwierig, daher ist es in Ihrem Fall eine schmutzige Problemumgehung. Sie müssen zumindest daran zu erinnern , dass , wenn Sie eine Entität Objekt zu verschmelzen passieren, ist es nicht mehr auf die Transaktion befestigt ist und stattdessen eine neue, jetzt-gebundene Einheit zurückgeführt wird. Dies bedeutet, dass, wenn jemand das alte Entitätsobjekt noch in seinem Besitz hat, Änderungen daran stillschweigend ignoriert und beim Festschreiben weggeworfen werden.

Sie zeigen hier nicht den vollständigen Code an, daher kann ich Ihr Transaktionsmuster nicht überprüfen. Eine Möglichkeit, zu einer solchen Situation zu gelangen, besteht darin, dass beim Ausführen der Zusammenführung und Fortbestehen keine Transaktion aktiv ist. In diesem Fall wird erwartet, dass der Persistenzanbieter für jede von Ihnen ausgeführte JPA-Operation eine neue Transaktion öffnet und diese sofort festschreibt und schließt, bevor der Anruf zurückkehrt. In diesem Fall wird die Zusammenführung in einer ersten Transaktion ausgeführt. Nachdem die Zusammenführungsmethode zurückgegeben wurde, wird die Transaktion abgeschlossen und geschlossen, und die zurückgegebene Entität wird nun getrennt. Das darunter liegende Persist würde dann eine zweite Transaktion öffnen und versuchen, auf eine Entität zu verweisen, die getrennt ist, was eine Ausnahme darstellt. Wickeln Sie Ihren Code immer in eine Transaktion ein, es sei denn, Sie wissen genau, was Sie tun.

Bei Verwendung einer von einem Container verwalteten Transaktion würde dies ungefähr so ​​aussehen. Beachten Sie: Dies setzt voraus, dass sich die Methode in einer Session-Bean befindet und über die lokale oder Remote-Schnittstelle aufgerufen wird.

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}

Ich stehe vor dem gleichen Problem. Ich habe das @Transaction(readonly=false)auf der Service-Ebene verwendet. Ich
bekomme

Ich kann nicht sagen, dass ich vollständig verstehe, warum die Dinge so funktionieren, aber das Zusammenfügen der Persist-Methode und der Ansicht zur Entitätszuordnung in einer Transaktionsanmerkung hat mein Problem behoben. Vielen Dank.
Deadron

Dies ist tatsächlich eine bessere Lösung als die am besten bewertete.
Aleksei Maide

13

Wahrscheinlich haben Sie in diesem Fall Ihr accountObjekt mithilfe der Zusammenführungslogik erhalten und persistwerden verwendet, um neue Objekte beizubehalten. Es wird sich beschweren, wenn die Hierarchie ein bereits bestehendes Objekt enthält. Sie sollten saveOrUpdatein solchen Fällen anstelle von verwenden persist.


3
Es ist JPA, also denke ich, dass die analoge Methode .merge () ist, aber das gibt mir die gleiche Ausnahme. Um klar zu sein, Transaktion ist ein neues Objekt, Konto nicht.
Paul Sanwald

Mit @PaulSanwald mergeauf transactionObjekt , das Sie die gleichen Fehler?
Dan

Nein, ich habe falsch gesprochen. Wenn ich .merge (Transaktion), wird die Transaktion überhaupt nicht beibehalten.
Paul Sanwald

@ PaulSanwald Hmm, bist du sicher, dass transactiondas nicht bestanden wurde? Wie hast du das überprüft? Beachten Sie, dass mergeein Verweis auf das persistierte Objekt zurückgegeben wird.
Dan

Das von .merge () zurückgegebene Objekt hat eine Null-ID. Außerdem mache ich danach ein .findAll () und mein Objekt ist nicht da.
Paul Sanwald

12

Übergeben Sie id (pk) nicht an persist method oder versuchen Sie save () method anstelle von persist ().


2
Guter Rat! Aber nur wenn id generiert wird. Falls es zugewiesen ist, ist es normal, die ID festzulegen.
Aldian

das hat bei mir funktioniert. Sie können TestEntityManager.persistAndFlush()auch eine Instanz verwalten und persistent machen und dann den Persistenzkontext mit der zugrunde liegenden Datenbank synchronisieren. Gibt die ursprüngliche Quellentität zurück
Anyul Rivas

7

Da dies eine sehr häufige Frage ist, habe ich diesen Artikel geschrieben , auf dem diese Antwort basiert.

Um das Problem zu beheben, müssen Sie die folgenden Schritte ausführen:

1. Entfernen der untergeordneten Kaskadierung

Sie müssen also die @CascadeType.ALLaus der @ManyToOneZuordnung entfernen . Untergeordnete Entitäten sollten nicht zu übergeordneten Zuordnungen kaskadieren. Nur übergeordnete Entitäten sollten zu untergeordneten Entitäten kaskadieren.

@ManyToOne(fetch= FetchType.LAZY)

Beachten Sie, dass ich das fetchAttribut auf gesetzt habe, FetchType.LAZYda eifriges Abrufen die Leistung sehr beeinträchtigt .

2. Legen Sie beide Seiten der Zuordnung fest

Wenn Sie eine bidirektionale Zuordnung haben, müssen Sie beide Seiten mit addChildund removeChildMethoden in der übergeordneten Entität synchronisieren :

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

Weitere Informationen dazu, warum es wichtig ist, beide Enden einer bidirektionalen Zuordnung zu synchronisieren, finden Sie in diesem Artikel .


4

In Ihrer Entitätsdefinition geben Sie nicht die @ JoinColumn für die VerknüpfungAccount mit a an Transaction. Du wirst so etwas wollen:

@Entity
public class Transaction {
    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    @JoinColumn(name = "accountId", referencedColumnName = "id")
    private Account fromAccount;
}

EDIT: Nun, ich denke, das wäre nützlich, wenn Sie die @TableAnmerkung für Ihre Klasse verwenden würden. Heh. :) :)


2
Ja, ich glaube nicht, dass es das ist. Trotzdem habe ich @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") hinzugefügt und es hat nicht funktioniert :).
Paul Sanwald

Ja, ich verwende normalerweise keine XML-Zuordnungsdatei zum Zuordnen von Entitäten zu Tabellen, daher gehe ich normalerweise davon aus, dass sie auf Anmerkungen basiert. Aber wenn ich raten müsste, verwenden Sie eine hibernate.xml, um Entitäten Tabellen zuzuordnen, oder?
NemesisX00

Nein, ich verwende Spring Data JPA, also basiert alles auf Anmerkungen. Ich habe eine "mappedBy" -Anmerkung auf der anderen Seite: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald

4

Selbst wenn Ihre Anmerkungen korrekt deklariert sind, um die Eins-zu-Viele-Beziehung ordnungsgemäß zu verwalten, kann diese genaue Ausnahme auftreten. Wenn TransactionSie einem angehängten Datenmodell ein neues untergeordnetes Objekt hinzufügen, müssen Sie den Primärschlüsselwert verwalten - es sei denn, Sie sollten dies nicht tun . Wenn Sie vor dem Aufruf einen Primärschlüsselwert für eine untergeordnete Entität angeben, die wie folgt deklariert ist, persist(T)tritt diese Ausnahme auf.

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
....

In diesem Fall wird in den Anmerkungen angegeben, dass die Datenbank die Generierung der Primärschlüsselwerte der Entität beim Einfügen verwaltet. Wenn Sie selbst einen bereitstellen (z. B. über den ID-Setter), wird diese Ausnahme verursacht.

Alternativ, aber effektiv gleich, führt diese Anmerkungsdeklaration zu derselben Ausnahme:

@Entity
public class Transaction {
    @Id
    @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
    @GeneratedValue(generator="system-uuid")
    private Long id;
....

Legen Sie den idWert in Ihrem Anwendungscode also nicht fest, wenn er bereits verwaltet wird.


4

Wenn nichts hilft und Sie immer noch diese Ausnahme erhalten, überprüfen Sie Ihre equals()Methoden - und fügen Sie keine untergeordnete Sammlung hinzu. Besonders wenn Sie eine tiefe Struktur eingebetteter Sammlungen haben (z. B. A enthält Bs, B enthält Cs usw.).

Im Beispiel von Account -> Transactions:

  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

Entfernen Sie im obigen Beispiel Transaktionen aus equals()Schecks. Dies liegt daran, dass der Ruhezustand bedeutet, dass Sie nicht versuchen, ein altes Objekt zu aktualisieren, sondern ein neues Objekt übergeben, um es beizubehalten, wenn Sie ein Element in der untergeordneten Sammlung ändern.
Natürlich passen diese Lösungen nicht zu allen Anwendungen, und Sie sollten sorgfältig entwerfen, was Sie in die equalsund hashCode-Methoden aufnehmen möchten .


1

Vielleicht ist es der Fehler von OpenJPA. Beim Rollback wird das @ Versionsfeld zurückgesetzt, aber das pcVersionInit bleibt wahr. Ich habe eine AbstraceEntity, die das Feld @Version deklariert hat. Ich kann es umgehen, indem ich das Feld pcVersionInit zurücksetze. Aber es ist keine gute Idee. Ich denke, es funktioniert nicht, wenn die Kaskade bestehen bleibt.

    private static Field PC_VERSION_INIT = null;
    static {
        try {
            PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
            PC_VERSION_INIT.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
        }
    }

    public T call(final EntityManager em) {
                if (PC_VERSION_INIT != null && isDetached(entity)) {
                    try {
                        PC_VERSION_INIT.set(entity, false);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                    }
                }
                em.persist(entity);
                return entity;
            }

            /**
             * @param entity
             * @param detached
             * @return
             */
            private boolean isDetached(final Object entity) {
                if (entity instanceof PersistenceCapable) {
                    PersistenceCapable pc = (PersistenceCapable) entity;
                    if (pc.pcIsDetached() == Boolean.TRUE) {
                        return true;
                    }
                }
                return false;
            }

1

Sie müssen die Transaktion für jedes Konto festlegen.

foreach(Account account : accounts){
    account.setTransaction(transactionObj);
}

Oder es reicht aus (falls zutreffend), IDs auf vielen Seiten auf null zu setzen.

// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());

foreach(Account account : accounts){
    account.setId(null);
}

transactionObj.setAccounts(accounts);

// just persist transactionObj using EntityManager merge() method.

1
cascadeType.MERGE,fetch= FetchType.LAZY

1
Hallo James und willkommen, du solltest versuchen, nur Code-Antworten zu vermeiden. Bitte geben Sie an, wie dies das in der Frage angegebene Problem löst (und wann es anwendbar ist oder nicht, API-Ebene usw.).
Maarten Bodewes

VLQ- Prüfer : siehe meta.stackoverflow.com/questions/260411/… . Nur-Code-Antworten sollten nicht gelöscht werden. Die entsprechende Aktion besteht darin, "Sieht OK aus" auszuwählen.
Nathan Hughes

0

In meinem Fall habe ich eine Transaktion festgeschrieben, als die Persist- Methode verwendet wurde. Beim Ändern von persist to save wurde die Methode behoben.


0

Wenn die oben genannten Lösungen nicht nur einmal funktionieren, kommentieren Sie die Getter- und Setter-Methoden der Entitätsklasse und setzen Sie nicht den Wert von id (Primärschlüssel). Dies funktioniert dann.


0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) hat für mich gearbeitet.


0

Meine JPA-basierte Antwort auf Spring Data: Ich @Transactionalhabe meiner äußeren Methode einfach eine Anmerkung hinzugefügt .

Warum es funktioniert

Die untergeordnete Entität wurde sofort getrennt, da kein aktiver Sitzungskontext für den Ruhezustand vorhanden war. Durch das Bereitstellen einer Spring-Transaktion (Data JPA) wird sichergestellt, dass eine Ruhezustandssitzung vorhanden ist.

Referenz:

https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/


0

Wird behoben, indem abhängige Objekte vor dem nächsten gespeichert werden.

Dies ist mir passiert, weil ich keine ID festgelegt habe (die nicht automatisch generiert wurde). und versuchen, mit der Beziehung @ManytoOne zu speichern

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.