PersistentObjectException: entità distaccata passata a persistente generata da JPA e Hibernate


237

Ho un modello a oggetti persistente JPA che contiene una relazione molti-a-uno: uno ne Accountha molti Transactions. A ne Transactionha uno Account.

Ecco uno snippet del codice:

@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;

Sono in grado di creare un Accountoggetto, aggiungere transazioni ad esso e persistere Accountcorrettamente l' oggetto. Ma, quando creo una transazione, usando un Conto già persistente esistente e persistendo la Transazione , ottengo un'eccezione:

Causato da: org.hibernate.PersistentObjectException: entità distaccata passata a persistere: com.paulsanwald.Account su org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Quindi, sono in grado di persistere in un file Accountcontenente transazioni, ma non in una Transazione con un Account. Pensavo che ciò fosse dovuto al fatto che Accountpotrebbe non essere allegato, ma questo codice mi dà ancora la stessa eccezione:

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);

Come posso salvare correttamente un Transaction, associato a un Accountoggetto già persistente ?


15
Nel mio caso, stavo impostando l'id di un'entità che stavo cercando di mantenere usando Entity Manager. Quando, ho rimosso il setter per id, ha iniziato a funzionare bene.
Rushi Shah,

Nel mio caso, non stavo impostando l'id, ma c'erano due utenti che utilizzavano lo stesso account, uno di loro ha persistito un'entità (correttamente) e l'errore si è verificato quando il secondo ha provato a mantenere la stessa entità, che era già persisteva.
sergioFC,

Risposte:


129

Questo è un tipico problema di coerenza bidirezionale. È ben discusso in questo link così come in questo link.

Come da articoli nei precedenti 2 collegamenti, è necessario correggere i setter in entrambi i lati della relazione bidirezionale. Un setter di esempio per il lato One è in questo link.

Un setter di esempio per il lato Many è in questo link.

Dopo aver corretto i setter, si desidera dichiarare il tipo di accesso Entity come "Proprietà". La migliore pratica per dichiarare il tipo di accesso "Proprietà" è spostare TUTTE le annotazioni dalle proprietà del membro ai getter corrispondenti. Un grande avvertimento non è quello di mescolare i tipi di accesso "Campo" e "Proprietà" all'interno della classe di entità, altrimenti il ​​comportamento non è definito dalle specifiche JSR-317.


2
ps: l'annotazione @Id è quella utilizzata dall'ibernazione per identificare il tipo di accesso.
Diego Plentz,

2
L'eccezione è: detached entity passed to persistperché migliorare la coerenza fa sì che funzioni? Ok, la coerenza è stata riparata ma l'oggetto è ancora staccato.
Gilgamesz,

1
@Sam, grazie mille per la tua spiegazione. Ma non capisco ancora. Vedo che senza setter "speciale" la relazione bidirezionale non è soddisfatta. Ma non vedo perché l'oggetto sia stato staccato.
Gilgamesz,

28
Si prega di non pubblicare una risposta che risponda esclusivamente ai collegamenti.
El Mac,

6
Non riesci a vedere come questa risposta sia correlata alla domanda?
Eugen Labun,

271

La soluzione è semplice, basta usare CascadeType.MERGEinvece di CascadeType.PERSISTo CascadeType.ALL.

Ho avuto lo stesso problema e CascadeType.MERGEha funzionato per me.

Spero che tu sia ordinato.


5
Sorprendentemente quello ha funzionato anche per me. Non ha senso poiché CascadeType.ALL include tutti gli altri tipi di cascata ... WTF? Ho spring 4.0.4, spring data jpa 1.8.0 e hibernate 4.X .. Qualcuno ha qualche idea sul perché TUTTI non funzionano, ma MERGE funziona?
Vadim Kirilchuk,

22
@VadimKirilchuk Questo ha funzionato anche per me e ha perfettamente senso. Poiché Transaction è PERSISTED, prova anche a PERSIST Account e non funziona poiché Account è già nel db. Ma con CascadeType.MERGE l'account viene invece unito automaticamente.
Pistolero

4
Questo può accadere se non si utilizzano le transazioni.
lanoxx,

1
un'altra soluzione: cerca di non inserire oggetti già persistenti :)
Andrii Plotnikov,

3
Grazie amico. Non è possibile evitare l'inserimento di oggetti persistenti, se si ha una limitazione per la chiave di riferimento NON essere NULL. Quindi questa è l'unica soluzione. Grazie ancora.
Makkasi,

22

Rimuovere il collegamento in cascata dall'entità figlioTransaction , dovrebbe essere solo:

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

( FetchType.EAGERpuò essere rimosso e predefinito per @ManyToOne)

È tutto!

Perché? Dicendo "ALL cascata" sull'entità figlio Transactionè necessario che ogni operazione DB venga propagata all'entità padre Account. Se lo fai persist(transaction), persist(account)verrà anche invocato.

Ma solo (nuove) entità transitorie possono essere passate a persist( Transactionin questo caso). Quelli staccati (o altri stati non transitori) potrebbero non ( Accountin questo caso, poiché è già nel DB).

Pertanto si ottiene l'eccezione "entità staccata passata a persistere" . L' Accountentità è pensata! Non il Transactionrichiamo persist.


In genere non si desidera propagare da figlio a genitore. Sfortunatamente ci sono molti esempi di codice nei libri (anche in quelli buoni) e attraverso la rete, che fanno esattamente questo. Non lo so, perché ... Forse a volte semplicemente copiato ripetutamente senza pensarci troppo ...

Indovina cosa succede se chiami remove(transaction)ancora avere "cascata TUTTO" in quel @ManyToOne? Il account(tra l'altro, con tutte le altre transazioni!) Verrà eliminato anche dal DB. Ma non era questa la tua intenzione, vero?


Voglio solo aggiungere, se la tua intenzione è davvero quella di salvare il figlio insieme al genitore ed anche eliminare il genitore insieme al figlio, come persona (genitore) e indirizzo (figlio) insieme all'indirizzoId autogenerato da DB, quindi prima di chiamare salva su Persona, basta fare una chiamata per salvare l'indirizzo nel metodo di transazione. In questo modo verrebbe salvato insieme a id generato anche da DB. Nessun impatto sulle prestazioni poiché Hibenate effettua ancora 2 query, stiamo solo cambiando l'ordine delle query.
Vikky,

Se non riusciamo a trasmettere nulla, quale valore predefinito è necessario per tutti i casi.
Dhwanil Patel,

15

L'uso della fusione è rischioso e complicato, quindi è una sporca soluzione nel tuo caso. È necessario ricordare almeno che quando si passa un oggetto entità da unire, smette di essere collegato alla transazione e invece viene restituita una nuova entità ora collegata. Ciò significa che se qualcuno ha ancora il vecchio oggetto entità in suo possesso, le modifiche ad esso vengono silenziosamente ignorate e eliminate durante il commit.

Non stai mostrando il codice completo qui, quindi non posso ricontrollare il tuo modello di transazione. Un modo per arrivare a una situazione come questa è se non hai una transazione attiva quando esegui l'unione e la persistenza. In tal caso, il fornitore di persistenza dovrebbe aprire una nuova transazione per ogni operazione JPA eseguita, impegnarla e chiuderla immediatamente prima che la chiamata ritorni. In questo caso, l'unione verrebbe eseguita in una prima transazione e quindi dopo che il metodo di unione ritorna, la transazione viene completata e chiusa e l'entità restituita viene ora staccata. Il persistente sottostante aprirà quindi una seconda transazione e tenterà di fare riferimento a un'entità che è staccata, dando un'eccezione. Avvolgi sempre il codice all'interno di una transazione a meno che tu non sappia molto bene cosa stai facendo.

Usando una transazione gestita dal container sarebbe simile a questa. Nota: questo presuppone che il metodo sia all'interno di un bean di sessione e chiamato tramite interfaccia locale o remota.

@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);
}

Sto affrontando lo stesso problema., Ho usato il @Transaction(readonly=false)livello di servizio.,
Sto

Non posso dire di comprendere appieno il perché le cose funzionino in questo modo, ma mettere insieme il metodo persist e visualizzare la mappatura delle entità all'interno di un'annotazione Transazionale ha risolto il mio problema, quindi grazie.
Deadron,

Questa è in realtà una soluzione migliore di quella più votata.
Aleksei Maide,

13

Probabilmente in questo caso hai ottenuto il tuo accountoggetto usando la logica di unione, e persistviene usato per persistere nuovi oggetti e si lamenterà se la gerarchia ha un oggetto già persistente. In saveOrUpdatequesti casi è necessario utilizzare , anziché persist.


3
è JPA, quindi penso che il metodo analogo sia .merge (), ma questo mi dà la stessa eccezione. Per essere chiari, Transaction è un nuovo oggetto, Account no.
Paul Sanwald,

@PaulSanwald Usando mergesul transactionoggetto si ottiene lo stesso errore?
dan

in realtà no, ho parlato male. se io .merge (transazione), allora la transazione non è affatto persistente.
Paul Sanwald,

@PaulSanwald Hmm, sei sicuro che transactionnon sia stato persistito? Come hai controllato. Si noti che mergesta restituendo un riferimento all'oggetto persistente.
dan

l'oggetto restituito da .merge () ha un ID null. inoltre, sto facendo un .findAll () in seguito, e il mio oggetto non è lì.
Paul Sanwald,

12

Non passare id (pk) al metodo persist o prova il metodo save () invece di persist ().


2
Buon Consiglio! Ma solo se viene generato id. Nel caso in cui sia assegnato, è normale impostare l'id.
Aldian,

questo ha funzionato per me. Inoltre, è possibile utilizzare TestEntityManager.persistAndFlush()per rendere un'istanza gestita e persistente, quindi sincronizzare il contesto di persistenza con il database sottostante. Restituisce l'entità di origine originale
Anyul Rivas il

7

Poiché questa è una domanda molto comune, ho scritto questo articolo , su cui si basa questa risposta.

Per risolvere il problema è necessario seguire questi passaggi:

1. Rimozione dell'associazione figlio a cascata

Quindi, è necessario rimuovere @CascadeType.ALLil @ManyToOnedall'associazione. Le entità figlio non devono essere collegate alle associazioni parentali. Solo le entità padre devono essere collegate a entità figlio.

@ManyToOne(fetch= FetchType.LAZY)

Si noti che ho impostato l' fetchattributo su FetchType.LAZYperché il recupero desideroso è molto negativo per le prestazioni .

2. Impostare entrambi i lati dell'associazione

Ogni volta che si dispone di un'associazione bidirezionale, è necessario sincronizzare entrambi i lati utilizzando addChilde removeChildmetodi nell'entità padre:

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

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

Per maggiori dettagli sul perché è importante sincronizzare entrambe le estremità di un'associazione bidirezionale, consulta questo articolo .


4

Nella definizione della tua entità, non stai specificando @JoinColumn per l' unioneAccount a a Transaction. Ti consigliamo qualcosa del genere:

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

EDIT: Beh, immagino che sarebbe utile se stessi usando l' @Tableannotazione sulla tua classe. Eh. :)


2
sì, non penso che sia così, ho aggiunto @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") e non ha funzionato :).
Paul Sanwald,

Sì, di solito non uso un file xml di mappatura per mappare entità su tabelle, quindi di solito presumo sia basato su annotazioni. Ma se dovessi indovinare, stai usando un hibernate.xml per mappare le entità alle tabelle, giusto?
NemesisX00

no, sto usando JPA di dati di primavera, quindi è tutto basato su annotazioni. Ho un'annotazione "mappedBy" sull'altro lato: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald,

4

Anche se le tue annotazioni vengono dichiarate correttamente per gestire correttamente la relazione uno-a-molti, potresti comunque riscontrare questa precisa eccezione. Quando si aggiunge un nuovo oggetto figlio Transaction, a un modello di dati collegato è necessario gestire il valore della chiave primaria, a meno che non sia previsto . Se fornisci un valore chiave primaria per un'entità figlio dichiarata come segue prima di chiamare persist(T), riscontrerai questa eccezione.

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

In questo caso, le annotazioni dichiarano che il database gestirà la generazione dei valori della chiave primaria dell'entità al momento dell'inserimento. Fornire te stesso (ad esempio tramite il setter dell'ID) provoca questa eccezione.

In alternativa, ma effettivamente uguale, questa dichiarazione di annotazione comporta la stessa eccezione:

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

Quindi, non impostare il idvalore nel codice dell'applicazione quando è già gestito.


4

Se nulla aiuta e stai ancora ottenendo questa eccezione, rivedi i tuoi equals()metodi e non includere la raccolta figlio in essa. Soprattutto se si dispone di una struttura profonda delle raccolte incorporate (ad es. A contiene Bs, B contiene Cs, ecc.).

Nell'esempio di 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!
    }
  }

Nell'esempio precedente rimuovere le transazioni dagli equals()assegni. Questo perché l'ibernazione implica che non si sta tentando di aggiornare il vecchio oggetto, ma si passa un nuovo oggetto per persistere, ogni volta che si cambia elemento nella raccolta figlio.
Naturalmente queste soluzioni non si adattano a tutte le applicazioni e dovresti progettare attentamente ciò che vuoi includere nei metodi equalse hashCode.


1

Forse è il bug di OpenJPA, quando il rollback reimposta il campo @Version, ma pcVersionInit rimane vero. Ho un AbstraceEntity che ha dichiarato il campo @Version. Posso risolvere il problema ripristinando il campo pcVersionInit. Ma non è una buona idea. Penso che non funzioni quando sono presenti entità a cascata.

    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

Devi impostare la Transazione per ogni Conto.

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

Oppure potrebbe essere sufficiente (se appropriato) impostare gli ID su null su molti lati.

// 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
Ciao James e benvenuto, dovresti cercare di evitare le risposte solo al codice. Indica in che modo risolve il problema indicato nella domanda (e quando è applicabile o meno, livello API ecc.).
Maarten Bodewes,

Revisori VLQ: consultare meta.stackoverflow.com/questions/260411/… . le risposte di solo codice non meritano la cancellazione, l'azione appropriata è selezionare "Sembra OK".
Nathan Hughes,


0

Nel mio caso stavo commettendo una transazione quando è stato usato il metodo persist . Modificando persist per salvare il metodo, è stato risolto.


0

Se le soluzioni di cui sopra non funzionano solo una volta commentare i metodi getter e setter della classe di entità e non impostare il valore di id. (Chiave primaria) Quindi funzionerà.


0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) ha funzionato per me.


0

Risolto salvando l'oggetto dipendente prima del successivo.

Questo mi è successo perché non stavo impostando Id (che non è stato generato automaticamente). e cercando di salvare con relazione @ManytoOne

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.