I'm using Hibernate 5.1.1. The setup is as follows:
- Two @Entity classes X and Y, with a 1-Many relationship from X to Y
- A @PreUpdate callback method wired against X
- A registered HibernateInterceptor
My failing test case simply creates a new instance of X, sets X.y to an appropriate Java collection value (e.g. new HashSet(), or new ArrayList()).
It then commits the instance via Hibernate. The error encountered is as follows:
Code:
java.lang.ClassCastException: java.util.HashSet cannot be cast to org.hibernate.collection.spi.PersistentCollection
at org.hibernate.event.internal.FlushVisitor.processCollection(FlushVisitor.java:39)
at org.hibernate.event.internal.AbstractVisitor.processValue(AbstractVisitor.java:104)
at org.hibernate.event.internal.AbstractVisitor.processValue(AbstractVisitor.java:65)
at org.hibernate.event.internal.AbstractVisitor.processEntityPropertyValues(AbstractVisitor.java:59)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:155)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:216)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:85)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:38)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1295)
After tracing through the code, here is my take on what is going wrong:
- When the Session is flushed, DefaultFlushEntityListener.onFlushEntity() invokes the wrapCollections() method to turn my Java collection value into e.g. a PersistentBag. At this point the entity still contains an e.g. HashSet, but the Object[] values contains a PersistentBag.
- Control is passed to DefaultFlushEntityListener.scheduleUpdate() and thence to handleInterception()
- Finally we end up in JpaFlushEntityEventListener.invokeInterceptor() where, because there is a @PreUpdate registered for X, then calls copyState().
- The copyState() overwrites the Object[] values using what is in the entity; hence we lose our PersistentBag, and it reverts to a plain Java collection e.g. HashSet.
- Back in DefaultFlushEntityListener.onFlushEntity(), we call persister.setPropertyValues() to use the Object[] values array to update the entity. Because that array was overwritten above, we end up with the entity still containing a value for X.y of a HashSet.
- Finally, when we reach FlushVisitor.processCollection(), an attempt is made to case the collection value (still a HashSet) to a PersistentCollection. At which point the ClassCastException occurs.
If I remove the @PreUpdate annotation, then the save completes without error. Hence it appears that the presence of the annotation, along with a registered Interceptor, causes any wrapped collections to be "unwrapped" before the flush executes.
I'd be grateful if someone could verify the above to confirm whether this is a bug or I am doing something wrong. In any case, if I wanted to both preserve my Interceptor and @PreUpdate callback, is there anything I can do to work around this?
Thanks
Alan