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 ?

Paul Sanwald
quelle
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.

Sym-Sym
quelle
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.

ochiWlad
quelle
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?

Eugen Labun
quelle
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);
}
Zds
quelle
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.

Dan
quelle
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 ().

Angad Bansode
quelle
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 .

Vlad Mihalcea
quelle
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. :) :)

NemesisX00
quelle
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.

Dan
quelle
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 .

FazoM
quelle
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;
            }
Yaocl
quelle
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.
stakahop
quelle
1
cascadeType.MERGE,fetch= FetchType.LAZY
James Siva
quelle
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.

H. Ostwal
quelle
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.

Shubham
quelle
0

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

SateeshKasaboina
quelle
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/

JJ Zabkar
quelle
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

Shahid Hussain Abbasi
quelle