Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring-JPA: updating parent Entity fails to persist new child Entities, interpreting them as Transient instead

I'm new to Spring/JPA/Hibernate, and while it sounds easy reality just hasn't been. I could use some help.

I have a parent Entity that holds a list of child Entities. I'll use these to keep the discussion simple:

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

    @OneToMany(fetch=FetchType.EAGER, cascade = CascadeType.ALL, mappedBy="parent")
    private List<Child> children= new ArrayList<Child>();

    etc...
}

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

    @ManyToOne
    private Parent parent;

    etc...
}

@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {};

Round 1, I create a new parent and a new child, add the child to the parent's list and set the parent on the child. When I save the parent the child is saved as well.

@Transactional(propagation=Propagation.REQUIRES_NEW)
void create() {
    Parent parent = new Parent();
    Child  child  = new Child();
    parent.add(child);
    child.setParent(parent);
    parent = repository.save(parent);
}

Now, Round 2, I add a new child:

@Transactional(propagation=Propagation.REQUIRES_NEW)
void update() {
    Parent parent = repository.findOne(parentID);
    Child newChild = new Child();
    newChild.setParent(parent);
    parent.add(newChild);
    parent = repository.save(parent);
}

However, this time the new child is never persisted!

I've tried most every variation of CascadeType, @GeneratedValue GenerationType, @Transactional Propagation type...

Tracing this through hibernate (painful!), here's what I've found:

  • When saving the second time, the problem is with the second (new) child.
  • The issue seems to be that when it comes time to persist the parent's Child list the new child is not in the EntityManager (yet) and thus in considered to be Transient.
  • As a result, it is effectively being passed down the chain as null, resulting in the following:
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is
javax.persistence.RollbackException: Error while committing thetransaction
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:521)
...

Caused by: javax.persistence.RollbackException: Error while committing the transaction
    at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:92)
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:512)
...

Caused by: org.hibernate.AssertionFailure: collection [null] was not processed by flush()
    at org.hibernate.engine.spi.CollectionEntry.postFlush(CollectionEntry.java:225)
...
  • It might be relevant that in my actual code "Child" also has a map of child Entities. This "value" is what gets passed down as null due to the "Transient" misappropriation.
  • I've been using repository.saveAndFlush() to keep things synchronous for debugging. When I use just .save() my @PreUpdate EntityListener is called but the @PostUpdate listener never is.
  • It seems that there wouldn't be a problem if Child were just persisted or given an Id at least before persisting Parent. But it also seems counter-productive to do that manually. Still, that's the only alternative I can think of.

Thanks for reading. Any help would be much appreciated!

like image 404
Didjit Avatar asked Feb 26 '13 01:02

Didjit


2 Answers

I found the problem, though I don't really have a complete solution yet.

First, some additional background. In addition to Parent and Child, I had a related class I'll call "House" here. House has an EntityListener defined so that when it is saved/updated, the associated Parent & Child objects get created/updated. So it is during House's PostPersist/PostUpdate that Parent and Child objects are created, linked, pointed back to House, and then persisted.

So the problem seems to be this is done before the House transaction completes. By merely pulling out the Parent/Child activity until after the House activity completes, all the problems went away.

The only thing I can figure (I'll dig a little deeper) is that since House hasn't been completely persisted at that moment, it results in the Transient condition described above.

Update:

Chalk one up to ignorance. Apparently EntityCallback methods "should not call EntityMan­ager or Query methods and should not access any other entity objects." Did not know that. This raises the question now of how should I trigger an Entity's creation on another's activity. But I'll start another thread for that if necessary. Thanks all!

like image 186
Didjit Avatar answered Nov 05 '22 19:11

Didjit


Everything in what you've shown seems pretty normal, so the problem might lie in that map you mentioned. I have a working example of a bidirectional one-to-many using Spring Data JPA on Github. You can look through the code or clone and run it with:

git clone git://github.com/zzantozz/testbed tmp
cd tmp/spring-data
mvn -q compile exec:java -D exec.mainClass=rds.springdata.JpaBiDiOneToManyExample
like image 3
Ryan Stewart Avatar answered Nov 05 '22 17:11

Ryan Stewart