JPA EventListener method not called on change to many-to-many collection?
Asked Answered
M

2

8

I am using JPA with Hibernate as the implementation in my Spring webapp.

I use EntityListeners for auditing (Spring-data) and other notification purposes. In general it works fine. However, when changes are made to a many-to-many collection/relationship, without any other change to any field of the entities themselves, the @PostUpdate event of the EventListeners are not fired.

To give a concrete example : I have a User and a Role entities, with a many-to-many relationship between them, using an underlying table. If I go to my user management GUI, and add (or remove) roles to a user, without changing anything else, the "postUpdate" event is not triggered, the auditing information is not modified.

There is some logic to it (at a low level) since neither the user or the role tables are updated, only the relationship table is. However the relationship itself is not modeled as an entity class and so it is impossible to apply auditing to it, at least not at the JPA level, so it would make sense to trigger the change event for the entity itself.

Is this a normal JPA behaviour, or is this specific to Hibernate ? Are there any workaround ? What would you do to trigger this event ? Thoughts ?

Note : I found very few mentions of this limitation, and even less solutions : This SO question (without useful answer) and this post on Hibernate's forum also without any answer.

Mcgean answered 12/6, 2013 at 10:31 Comment(6)
At the risk of sounding like Hibernate hater, I suggest NEVER rely on entity listener implementations provided by hibernate. If you go and visit hibernate bug log you would find many bugs related to these. Implementation is broken since 3.7.x. I had many problems with postpersist, postload.Fenian
Thanks for the suggestion. I'll consider changing my JPA provider in the future, but right now is not a good time. Do you have experience with this particular issue working differently in with other providers such as EclipseLink ?Mcgean
I didnt try any other JPA provider but removed the entity listeners and did them manually. No other option, had to change whole code flow because of this :(Fenian
Welcome. Sorry could not be of more help!Fenian
Where to start Rachit :) Hibernate has never had a 3.7.x release, so how could it be broken since then? As for bugs, which Jira issues?Renee
I think you will find a solution in this fork. #16020320Abatis
R
6

The JPA spec is very unclear about changes to collections triggering pre/post update callbacks on the entity that owns them. And by unclear, I mean completely silent. In fact, IMO the wording in the spec implies that the callback should not happen since pre/post update is supposed to happen before/after SQL UPDATE statements are sent for that entity.

Now, if the owning entity is versioned Hibernate treats changes to any collections (any attributes really) it owns as forcing a version increment on the owning entity which will trigger the update of the entity which will in turn trigger the post-update callback.

Renee answered 12/6, 2013 at 19:6 Comment(5)
What exactly do you mean by "if the owning entity is versioned" ?Mcgean
@PierreHenry, if it has a field annotated with the javax.persistence.Version annotation. This field is used to check for changes on the entity.Dolly
I have a OneToMany(mapedBy) field, add to this collection, the entity's version will not change.Quicken
@steve-ebersole Versioning works well with ElementCollections and ManyToMany. It does work with OneToMany Collection. Any workaround for this?Emeliaemelin
I also figured out that Version is not incremented on changes of OneToMany properties and would be very interested in a valid workaround for this.Enunciate
A
0

You can use special hooks to catch this action. In my case, I created an EventIntegrator with the EntityManagerFactory.

@Service
@RequiredArgsConstructor
public class EventIntegrator {
    private final EntityManagerFactory entityManagerFactory;
    private final CustomPostCollectionUpdateEventListener postCollectionUpdateEventListener;
    @PostConstruct
    private void init() {
        SessionFactoryImpl sessionFactory = entityManagerFactory.unwrap(SessionFactoryImpl.class);
        EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
        registry.getEventListenerGroup(EventType.POST_COLLECTION_UPDATE)
            .appendListener(postCollectionUpdateEventListener);
}

And after that I created an implementation for `PostCollectionUpdateEventListener'.

@Component
@RequiredArgsConstructor
public class CustomPostCollectionUpdateEventListener implements PostCollectionUpdateEventListener {
    @Override
    public void onPostUpdateCollection(PostCollectionUpdateEvent event) {
        if (event.getAffectedOwnerOrNull() != null) {
            var object = (AEntity<?>) event.getAffectedOwnerOrNull();
            Arrays.stream(object.getClass().getMethods())
                .filter(method -> method.isAnnotationPresent(PrePersist.class) && method.getParameterCount() == 0)
                .forEach(method -> {
                    try {
                        method.invoke(object);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        ExceptionUtils.throwUnsupportedOperationException("@PrePersis method was found, but cannot be called");
                    }
                });
            object.log(Event.UPDATE);
        }
    }
}

I also created an implementation for PostUpdateEventListener because sometimes I catch PostUpdateEvent without CollectionUpdateEvent!

And you can see more hooks in org.hibernate.event.spi.EventType

Abatis answered 21/9, 2022 at 6:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.