PersistentObjectException: entité détachée passée pour persister levée par JPA et Hibernate

237

J'ai un modèle d'objet persistant JPA qui contient une relation plusieurs-à-un: an Accounta plusieurs Transactions. A en Transactiona un Account.

Voici un extrait du 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;

Je peux créer un Accountobjet, y ajouter des transactions et conserver Accountcorrectement l' objet. Mais, lorsque je crée une transaction, en utilisant un compte existant déjà persistant et en persistant la transaction , j'obtiens une exception:

Causée par: org.hibernate.PersistentObjectException: entité détachée passée pour persister: com.paulsanwald.Account à org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Donc, je suis capable de persister un Accountqui contient des transactions, mais pas une transaction qui a un Account. Je pensais que c'était parce queAccount peut ne pas être attaché, mais ce code me donne toujours la même exception:

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

Comment puis-je enregistrer correctement un Transaction, associé à un Accountobjet déjà persistant ?

Paul Sanwald
la source
15
Dans mon cas, je définissais l'ID d'une entité que j'essayais de conserver en utilisant Entity Manager. Quand j'ai retiré le setter pour id, il a bien fonctionné.
Rushi Shah
Dans mon cas, je ne définissais pas l'ID, mais il y avait deux utilisateurs utilisant le même compte, l'un d'eux a persisté (correctement), et l'erreur s'est produite lorsque le second a essayé de persister la même entité, qui était déjà a persisté.
sergioFC

Réponses:

129

Il s'agit d'un problème de cohérence bidirectionnel typique. Il est bien discuté dans ce lien ainsi que ce lien.

Selon les articles des 2 liens précédents, vous devez corriger vos setters des deux côtés de la relation bidirectionnelle. Un exemple de setter pour le côté One est dans ce lien.

Un exemple de setter pour le côté Many se trouve dans ce lien.

Après avoir corrigé vos setters, vous voulez déclarer le type d'accès Entity comme "Property". La meilleure pratique pour déclarer le type d'accès "Propriété" consiste à déplacer TOUTES les annotations des propriétés de membre vers les getters correspondants. Un grand mot de prudence est de ne pas mélanger les types d'accès "Champ" et "Propriété" au sein de la classe d'entité sinon le comportement n'est pas défini par les spécifications JSR-317.

Sym-Sym
la source
2
ps: l'annotation @Id est celle que l'hibernation utilise pour identifier le type d'accès.
Diego Plentz
2
L'exception est la suivante: detached entity passed to persistpourquoi améliorer la cohérence fait que cela fonctionne? Ok, la cohérence a été réparée mais l'objet est toujours détaché.
Gilgamesz
1
@Sam, merci beaucoup pour votre explication. Mais je ne comprends toujours pas. Je vois que sans poseur «spécial» la relation bidirectionnelle n'est pas satisfaite. Mais, je ne vois pas pourquoi l'objet a été détaché.
Gilgamesz
28
Veuillez ne pas poster une réponse qui ne répond qu'avec des liens.
El Mac
6
Vous ne voyez pas du tout comment cette réponse est liée à la question?
Eugen Labun
271

La solution est simple, utilisez simplement le CascadeType.MERGEau lieu de CascadeType.PERSISTou CascadeType.ALL.

J'ai eu le même problème et CascadeType.MERGEj'ai travaillé pour moi.

J'espère que vous êtes trié.

ochiWlad
la source
5
Étonnamment, celui-ci a également fonctionné pour moi. Cela n'a aucun sens puisque CascadeType.ALL inclut tous les autres types de cascade ... WTF? J'ai le printemps 4.0.4, les données du printemps jpa 1.8.0 et l'hibernation 4.X.
Vadim Kirilchuk du
22
@VadimKirilchuk Cela a également fonctionné pour moi et cela a un sens total. Puisque la transaction est PERSISTEE, elle essaie également de PERSISTER le compte et cela ne fonctionne pas car le compte est déjà dans la base de données. Mais avec CascadeType.MERGE, le compte est automatiquement fusionné à la place.
Gunslinger
4
Cela peut se produire si vous n'utilisez pas de transactions.
lanoxx
1
une autre solution: essayez de ne pas insérer d'objet déjà persistant :)
Andrii Plotnikov
3
Merci mec. Il n'est pas possible d'éviter l'insertion d'un objet persistant, si vous avez une restriction pour que la clé de référence ne soit pas NULL. C'est donc la seule solution. Merci encore.
makkasi
22

Supprimez la cascade de l'entité enfantTransaction , cela devrait être juste:

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

( FetchType.EAGERpeut être supprimé ainsi que sa valeur par défaut @ManyToOne)

C'est tout!

Pourquoi? En disant "cascade ALL" sur l'entité enfant, Transactionvous exigez que chaque opération de base de données soit propagée à l'entité parent Account. Si vous le faites persist(transaction), persist(account)sera également invoqué.

Mais seules les (nouvelles) entités transitoires peuvent être transmises à persist(Transaction dans ce cas). Les éléments détachés (ou autres états non transitoires) peuvent ne pas ( Accountdans ce cas, car ils sont déjà dans DB).

Par conséquent, vous obtenez l'exception "entité détachée passée pour persister" . L' Accountentité est destinée! Pas celui que Transactionvous appelez persist.


Vous ne voulez généralement pas vous propager de l'enfant au parent. Malheureusement, il existe de nombreux exemples de code dans les livres (même dans les bons) et sur le net, qui font exactement cela. Je ne sais pas, pourquoi ... Peut-être parfois simplement copié encore et encore sans trop réfléchir ...

Devinez ce qui se passe si vous appelez remove(transaction)toujours "TOUT en cascade" dans ce @ManyToOne? Le account(btw, avec toutes les autres transactions!) Sera également supprimé de la base de données. Mais ce n'était pas votre intention, n'est-ce pas?

Eugen Labun
la source
Je veux juste ajouter, si votre intention est vraiment de sauver l'enfant avec le parent et aussi de supprimer le parent avec l'enfant, comme la personne (parent) et l'adresse (enfant) avec addressId généré automatiquement par DB puis avant d'appeler save on Person, faites simplement un appel pour économiser sur l'adresse dans votre méthode de transaction. De cette façon, il serait enregistré avec id également généré par DB. Aucun impact sur les performances car Hibenate fait toujours 2 requêtes, nous ne faisons que changer l'ordre des requêtes.
Vikky
Si nous ne pouvons rien transmettre, alors quelle valeur par défaut cela prend pour tous les cas.
Dhwanil Patel
15

L'utilisation de la fusion est risquée et délicate, c'est donc une solution de contournement sale dans votre cas. Vous devez vous rappeler au moins que lorsque vous passez un objet entité à fusionner, il cesse d' être attaché à la transaction et à la place une nouvelle entité désormais attachée est renvoyée. Cela signifie que si quelqu'un a toujours l'ancien objet entité en sa possession, les modifications apportées sont silencieusement ignorées et jetées lors de la validation.

Vous ne montrez pas le code complet ici, donc je ne peux pas revérifier votre modèle de transaction. Une façon d'arriver à une situation comme celle-ci est de ne pas avoir de transaction active lors de l'exécution de la fusion et de persister. Dans ce cas, le fournisseur de persistance doit ouvrir une nouvelle transaction pour chaque opération JPA que vous effectuez et la valider et la fermer immédiatement avant le retour de l'appel. Si tel est le cas, la fusion sera exécutée dans une première transaction, puis après le retour de la méthode de fusion, la transaction est terminée et fermée et l'entité retournée est maintenant détachée. La persistance en dessous ouvrirait alors une deuxième transaction et tenterait de faire référence à une entité qui est détachée, donnant une exception. Enveloppez toujours votre code dans une transaction, sauf si vous savez très bien ce que vous faites.

En utilisant une transaction gérée par conteneur, cela ressemblerait à quelque chose comme ceci. Remarque: cela suppose que la méthode se trouve dans un bean session et appelée via une interface locale ou distante.

@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
la source
Je suis confronté au même problème., J'ai utilisé la @Transaction(readonly=false)couche de service ., Je reçois toujours le même problème,
Senthil Arumugam SP
Je ne peux pas dire que je comprends parfaitement pourquoi les choses fonctionnent de cette façon, mais le fait de placer la méthode persist et la vue sur le mappage d'entité ensemble dans une annotation transactionnelle a résolu mon problème, merci.
Deadron
C'est en fait une meilleure solution que la plus votée.
Aleksei Maide
13

Dans ce cas, vous avez probablement obtenu votre accountobjet en utilisant la logique de fusion, et persistest utilisé pour conserver de nouveaux objets et il se plaindra si la hiérarchie a un objet déjà persistant. Vous devez utiliser saveOrUpdatedans de tels cas, au lieu de persist.

dan
la source
3
c'est JPA, donc je pense que la méthode analogue est .merge (), mais cela me donne la même exception. Pour être clair, la transaction est un nouvel objet, le compte ne l'est pas.
Paul Sanwald
@PaulSanwald En utilisant mergesur l' transactionobjet que vous obtenez la même erreur?
dan
en fait, non, j'ai mal parlé. si je .merge (transaction), alors la transaction n'est pas persistée du tout.
Paul Sanwald
@PaulSanwald Hmm, êtes-vous sûr que cela transactionn'a pas persisté? Comment avez-vous vérifié. Notez que mergerenvoie une référence à l'objet persistant.
dan
l'objet retourné par .merge () a un id nul. aussi, je fais un .findAll () après, et mon objet n'est pas là.
Paul Sanwald
12

Ne passez pas id (pk) à la méthode persist ou essayez la méthode save () au lieu de persist ().

Angad Bansode
la source
2
Bon conseil! Mais seulement si id est généré. Dans le cas où il est attribué, il est normal de définir l'identifiant.
Aldian
cela a fonctionné pour moi. Vous pouvez également utiliser TestEntityManager.persistAndFlush()pour rendre une instance gérée et persistante, puis synchroniser le contexte de persistance avec la base de données sous-jacente. Renvoie l'entité source d'origine
Anyul Rivas
7

Puisque c'est une question très courante, j'ai écrit cet article , sur lequel cette réponse est basée.

Afin de résoudre le problème, vous devez suivre ces étapes:

1. Suppression de l'association d'enfants en cascade

Vous devez donc supprimer le @CascadeType.ALLde l' @ManyToOneassociation. Les entités enfants ne doivent pas se connecter en cascade aux associations de parents. Seules les entités parentes doivent se connecter en cascade aux entités enfants.

@ManyToOne(fetch= FetchType.LAZY)

Notez que j'ai défini l' fetchattribut sur, FetchType.LAZYcar une récupération rapide est très mauvaise pour les performances .

2. Définir les deux côtés de l'association

Chaque fois que vous avez une association bidirectionnelle, vous devez synchroniser les deux côtés à l'aide des méthodes addChildet removeChilddans l'entité parent:

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

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

Pour plus de détails sur les raisons pour lesquelles il est important de synchroniser les deux extrémités d'une association bidirectionnelle, consultez cet article .

Vlad Mihalcea
la source
4

Dans votre définition d'entité, vous ne spécifiez pas la @JoinColumn pour le Accountjoint à a Transaction. Vous voudrez quelque chose comme ça:

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

EDIT: Eh bien, je suppose que ce serait utile si vous utilisiez l' @Tableannotation sur votre classe. Il h. :)

NemesisX00
la source
2
oui je ne pense pas que ce soit tout de même, j'ai ajouté @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") et ça n'a pas marché :).
Paul Sanwald
Ouais, je n'utilise généralement pas de fichier xml de mappage pour mapper des entités à des tables, donc je suppose généralement qu'il est basé sur des annotations. Mais si je devais deviner, vous utilisez un hibernate.xml pour mapper des entités aux tables, non?
NemesisX00
non, j'utilise JPA de données de printemps, donc tout est basé sur des annotations. J'ai une annotation "mappedBy" de l'autre côté: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald
4

Même si vos annotations sont déclarées correctement pour gérer correctement la relation un-à-plusieurs, vous pouvez toujours rencontrer cette exception précise. Lors de l'ajout d'un nouvel objet enfant Transaction, à un modèle de données attaché, vous devrez gérer la valeur de la clé primaire - sauf si vous n'êtes pas censé le faire . Si vous fournissez une valeur de clé primaire pour une entité enfant déclarée comme suit avant d'appeler persist(T), vous rencontrerez cette exception.

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

Dans ce cas, les annotations déclarent que la base de données gérera la génération des valeurs de clé primaire de l'entité lors de l'insertion. Fournir un vous-même (par exemple via le setter de l'ID) provoque cette exception.

Alternativement, mais en fait la même chose, cette déclaration d'annotation entraîne la même exception:

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

Donc, ne définissez pas la idvaleur dans votre code d'application lorsqu'elle est déjà gérée.

dan
la source
4

Si rien ne vous aide et que vous obtenez toujours cette exception, passez en revue votre equals() méthodes - et n'incluez pas de collection d'enfant dans celle-ci. Surtout si vous avez une structure profonde de collections intégrées (par exemple, A contient Bs, B contient Cs, etc.).

En exemple de 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!
    }
  }

Dans l'exemple ci-dessus, supprimez les transactions des equals()chèques. En effet, hibernate impliquera que vous n'essayez pas de mettre à jour l'ancien objet, mais que vous passez un nouvel objet pour qu'il persiste, chaque fois que vous changez d'élément sur la collection enfant.
Bien sûr, ces solutions ne conviennent pas à toutes les applications et vous devez soigneusement concevoir ce que vous souhaitez inclure dans les méthodes equalset hashCode.

FazoM
la source
1

C'est peut-être le bogue d'OpenJPA, lors de la restauration, il réinitialise le champ @Version, mais le pcVersionInit reste vrai. J'ai une AbstraceEntity qui a déclaré le champ @Version. Je peux le contourner en réinitialisant le champ pcVersionInit. Mais ce n'est pas une bonne idée. Je pense que cela ne fonctionne pas lorsque la cascade persiste.

    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
la source
1

Vous devez définir la transaction pour chaque compte.

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

Ou il peut suffire (le cas échéant) de définir les identifiants sur null de plusieurs côtés.

// 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
la source
1
cascadeType.MERGE,fetch= FetchType.LAZY
James Siva
la source
1
Salut James et bienvenue, vous devriez essayer d'éviter les réponses en code uniquement. Veuillez indiquer comment cela résout le problème énoncé dans la question (et quand c'est applicable ou non, niveau API, etc.).
Maarten Bodewes
Réviseurs VLQ: voir meta.stackoverflow.com/questions/260411/… . les réponses de code uniquement ne méritent pas d'être supprimées, l'action appropriée consiste à sélectionner "Looks OK".
Nathan Hughes
1

Ma réponse basée sur Spring Data JPA: j'ai simplement ajouté une @Transactionalannotation à ma méthode externe.

Pourquoi ça marche

L'entité enfant se détachait immédiatement car il n'y avait pas de contexte de session Hibernate actif. La fourniture d'une transaction Spring (Data JPA) garantit la présence d'une session Hibernate.

Référence:

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

JJ Zabkar
la source
0

Dans mon cas, je commettais une transaction lorsque la méthode persist était utilisée. En changeant la méthode persist to save , elle a été résolue.

H.Ostwal
la source
0

Si les solutions ci-dessus ne fonctionnent pas une seule fois, commentez les méthodes getter et setter de la classe d'entité et ne définissez pas la valeur de id. (Clé primaire) Ensuite, cela fonctionnera.

Shubham
la source
0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) a fonctionné pour moi.

SateeshKasaboina
la source
0

Résolu en enregistrant l'objet dépendant avant le suivant.

Cela m'est arrivé parce que je ne définissais pas l'ID (qui n'était pas généré automatiquement). et essayer d'économiser avec la relation @ManytoOne

Shahid Hussain Abbasi
la source