PersistentObjectException: detached entity passed to persist thrown by JPA and Hibernate

前端 未结 18 2346
醉酒成梦
醉酒成梦 2020-11-22 05:10

I have a JPA-persisted object model that contains a many-to-one relationship: an Account has many Transactions. A Transaction has one

相关标签:
18条回答
  • 2020-11-22 05:27

    Remove cascading from the child entity Transaction, it should be just:

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

    (FetchType.EAGER can be removed as well as it's the default for @ManyToOne)

    That's all!

    Why? By saying "cascade ALL" on the child entity Transaction you require that every DB operation gets propagated to the parent entity Account. If you then do persist(transaction), persist(account) will be invoked as well.

    But only transient (new) entities may be passed to persist (Transaction in this case). The detached (or other non-transient state) ones may not (Account in this case, as it's already in DB).

    Therefore you get the exception "detached entity passed to persist". The Account entity is meant! Not the Transaction you call persist on.


    You generally don't want to propagate from child to parent. Unfortunately there are many code examples in books (even in good ones) and through the net, which do exactly that. I don't know, why... Perhaps sometimes simply copied over and over without much thinking...

    Guess what happens if you call remove(transaction) still having "cascade ALL" in that @ManyToOne? The account (btw, with all other transactions!) will be deleted from the DB as well. But that wasn't your intention, was it?

    0 讨论(0)
  • 2020-11-22 05:31

    Maybe It is OpenJPA's bug, When rollback it reset the @Version field, but the pcVersionInit keep true. I have a AbstraceEntity which declared the @Version field. I can workaround it by reset the pcVersionInit field. But It is not a good idea. I think it not work when have cascade persist entity.

        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;
                }
    
    0 讨论(0)
  • 2020-11-22 05:32

    Even if your annotations are declared correctly to properly manage the one-to-many relationship you may still encounter this precise exception. When adding a new child object, Transaction, to an attached data model you'll need to manage the primary key value - unless you're not supposed to. If you supply a primary key value for a child entity declared as follows before calling persist(T), you'll encounter this exception.

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

    In this case, the annotations are declaring that the database will manage the generation of the entity's primary key values upon insertion. Providing one yourself (such as through the Id's setter) causes this exception.

    Alternatively, but effectively the same, this annotation declaration results in the same exception:

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

    So, don't set the id value in your application code when it's already being managed.

    0 讨论(0)
  • 2020-11-22 05:34

    Using merge is risky and tricky, so it's a dirty workaround in your case. You need to remember at least that when you pass an entity object to merge, it stops being attached to the transaction and instead a new, now-attached entity is returned. This means that if anyone has the old entity object still in their possession, changes to it are silently ignored and thrown away on commit.

    You are not showing the complete code here, so I cannot double-check your transaction pattern. One way to get to a situation like this is if you don't have a transaction active when executing the merge and persist. In that case persistence provider is expected to open a new transaction for every JPA operation you perform and immediately commit and close it before the call returns. If this is the case, the merge would be run in a first transaction and then after the merge method returns, the transaction is completed and closed and the returned entity is now detached. The persist below it would then open a second transaction, and trying to refer to an entity that is detached, giving an exception. Always wrap your code inside a transaction unless you know very well what you are doing.

    Using container-managed transaction it would look something like this. Do note: this assumes the method is inside a session bean and called via Local or Remote interface.

    @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);
    }
    
    0 讨论(0)
  • 2020-11-22 05:37

    @OneToMany(mappedBy = "xxxx", cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) worked for me.

    0 讨论(0)
  • 2020-11-22 05:39

    The solution is simple, just use the CascadeType.MERGE instead of CascadeType.PERSIST or CascadeType.ALL.

    I have had the same problem and CascadeType.MERGE has worked for me.

    I hope you are sorted.

    0 讨论(0)
提交回复
热议问题