From 60f0ba5507c93b62be4471582b674f72e395ded7 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Fri, 27 Jun 2025 11:50:19 +0200 Subject: [PATCH 1/2] HHH-19523 correctly initialize Pre/PostCollectionXxxxEvents from StatelessSession note that this change breaks Hibernate Reactive --- .../event/spi/AbstractCollectionEvent.java | 31 +++- .../spi/PostCollectionRecreateEvent.java | 8 + .../event/spi/PostCollectionRemoveEvent.java | 16 +- .../event/spi/PostCollectionUpdateEvent.java | 9 + .../event/spi/PreCollectionRecreateEvent.java | 9 + .../event/spi/PreCollectionRemoveEvent.java | 8 + .../event/spi/PreCollectionUpdateEvent.java | 9 + .../internal/StatelessSessionImpl.java | 154 ++++++++++-------- 8 files changed, 167 insertions(+), 77 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/AbstractCollectionEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/AbstractCollectionEvent.java index dc706e043b41..ddee54c7d90d 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/AbstractCollectionEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/AbstractCollectionEvent.java @@ -22,13 +22,13 @@ public abstract class AbstractCollectionEvent extends AbstractEvent { private final String affectedOwnerEntityName; /** - * Constructs an AbstractCollectionEvent object. - * @param collection - the collection + * Constructs an instance for a stateful session. + * @param collection - the collection * @param source - the Session source * @param affectedOwner - the owner that is affected by this event; - * can be null if unavailable + * can be null if unavailable * @param affectedOwnerId - the ID for the owner that is affected -* by this event; can be null if unavailable + * by this event; can be null if unavailable */ public AbstractCollectionEvent( CollectionPersister collectionPersister, @@ -44,6 +44,27 @@ public AbstractCollectionEvent( getAffectedOwnerEntityName( collectionPersister, affectedOwner, source ); } + /** + * Constructs an instance for a stateless session. + * @param collection - the collection + * @param entityName - the name of the owning entity + * @param affectedOwner - the owner that is affected by this event; + * can be null if unavailable + * @param affectedOwnerId - the ID for the owner that is affected + * by this event; can be null if unavailable + */ + public AbstractCollectionEvent( + PersistentCollection collection, + String entityName, + Object affectedOwner, + Object affectedOwnerId) { + super( null ); + this.collection = collection; + this.affectedOwner = affectedOwner; + this.affectedOwnerId = affectedOwnerId; + this.affectedOwnerEntityName = entityName; + } + protected static CollectionPersister getLoadedCollectionPersister( PersistentCollection collection, EventSource source ) { CollectionEntry ce = source.getPersistenceContextInternal().getCollectionEntry( collection ); return ce == null ? null : ce.getLoadedPersister(); @@ -58,7 +79,7 @@ protected static Object getLoadedOwnerIdOrNull(PersistentCollection collectio } protected static Object getOwnerIdOrNull(Object owner, EventSource source ) { - EntityEntry ownerEntry = source.getPersistenceContextInternal().getEntry( owner ); + final EntityEntry ownerEntry = source.getPersistenceContextInternal().getEntry( owner ); return ownerEntry == null ? null : ownerEntry.getId(); } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRecreateEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRecreateEvent.java index a62fc589f59a..6a5357a98705 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRecreateEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRecreateEvent.java @@ -26,4 +26,12 @@ public PostCollectionRecreateEvent( getOwnerIdOrNull( collection.getOwner(), source ) ); } + + public PostCollectionRecreateEvent( + PersistentCollection collection, + Object id, + String entityName, + Object loadedOwner) { + super( collection, entityName, loadedOwner, id ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRemoveEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRemoveEvent.java index 2c322d21536b..96061a54fd74 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRemoveEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionRemoveEvent.java @@ -18,6 +18,20 @@ public PostCollectionRemoveEvent( PersistentCollection collection, EventSource source, Object loadedOwner) { - super( collectionPersister, collection, source, loadedOwner, getOwnerIdOrNull( loadedOwner, source ) ); + super( + collectionPersister, + collection, + source, + loadedOwner, + getOwnerIdOrNull( loadedOwner, source ) + ); + } + + public PostCollectionRemoveEvent( + PersistentCollection collection, + Object id, + String entityName, + Object loadedOwner) { + super( collection, entityName, loadedOwner, id ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionUpdateEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionUpdateEvent.java index 0e91f5d89bfe..c0a3569451d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionUpdateEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PostCollectionUpdateEvent.java @@ -25,4 +25,13 @@ public PostCollectionUpdateEvent( getLoadedOwnerIdOrNull( collection, source ) ); } + + + public PostCollectionUpdateEvent( + PersistentCollection collection, + Object id, + String entityName, + Object loadedOwner) { + super( collection, entityName, loadedOwner, id ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRecreateEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRecreateEvent.java index 27bd622682ba..68cd61f9eab0 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRecreateEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRecreateEvent.java @@ -26,4 +26,13 @@ public PreCollectionRecreateEvent( getOwnerIdOrNull( collection.getOwner(), source ) ); } + + + public PreCollectionRecreateEvent( + PersistentCollection collection, + Object id, + String entityName, + Object loadedOwner) { + super( collection, entityName, loadedOwner, id ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRemoveEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRemoveEvent.java index f6b4093dd192..84a458bb9df3 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRemoveEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionRemoveEvent.java @@ -27,4 +27,12 @@ public PreCollectionRemoveEvent( getOwnerIdOrNull( loadedOwner, source ) ); } + + public PreCollectionRemoveEvent( + PersistentCollection collection, + Object id, + String entityName, + Object loadedOwner) { + super( collection, entityName, loadedOwner, id ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionUpdateEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionUpdateEvent.java index 473c28fa1cbe..2aea2a8becad 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionUpdateEvent.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreCollectionUpdateEvent.java @@ -26,4 +26,13 @@ public PreCollectionUpdateEvent( getLoadedOwnerIdOrNull( collection, source ) ); } + + + public PreCollectionUpdateEvent( + PersistentCollection collection, + Object id, + String entityName, + Object loadedOwner) { + super( collection, entityName, loadedOwner, id ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index c018b0f2c358..408451eec3ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -275,25 +275,29 @@ else if ( generator.generatedOnExecution( entity, this ) ) { } private void recreateCollections(Object entity, Object id, EntityPersister persister) { - forEachOwnedCollection( entity, id, persister, - (descriptor, collection) -> { - firePreRecreate( collection, descriptor ); - final EventMonitor eventMonitor = getEventMonitor(); - final DiagnosticEvent event = eventMonitor.beginCollectionRecreateEvent(); - boolean success = false; - try { - descriptor.recreate( collection, id, this ); - success = true; - } - finally { - eventMonitor.completeCollectionRecreateEvent( event, id, descriptor.getRole(), success, this ); - } - final StatisticsImplementor statistics = getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.recreateCollection( descriptor.getRole() ); - } - firePostRecreate( collection, descriptor ); - } ); + if ( persister.hasOwnedCollections() ) { + final String entityName = persister.getEntityName(); + final EventMonitor eventMonitor = getEventMonitor(); + final StatisticsImplementor statistics = getFactory().getStatistics(); + forEachOwnedCollection( entity, id, persister, + (descriptor, collection) -> { + final String role = descriptor.getRole(); + firePreRecreate( collection, id, entityName, entity ); + final DiagnosticEvent event = eventMonitor.beginCollectionRecreateEvent(); + boolean success = false; + try { + descriptor.recreate( collection, id, this ); + success = true; + } + finally { + eventMonitor.completeCollectionRecreateEvent( event, id, role, success, this ); + } + if ( statistics.isStatisticsEnabled() ) { + statistics.recreateCollection( role ); + } + firePostRecreate( collection, id, entityName, entity ); + } ); + } } // deletes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -347,25 +351,29 @@ public void delete(String entityName, Object entity) { } private void removeCollections(Object entity, Object id, EntityPersister persister) { - forEachOwnedCollection( entity, id, persister, - (descriptor, collection) -> { - firePreRemove( collection, entity, descriptor ); - final EventMonitor eventMonitor = getEventMonitor(); - final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); - boolean success = false; - try { - descriptor.remove( id, this ); - success = true; - } - finally { - eventMonitor.completeCollectionRemoveEvent( event, id, descriptor.getRole(), success, this ); - } - firePostRemove( collection, entity, descriptor ); - final StatisticsImplementor statistics = getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.removeCollection( descriptor.getRole() ); - } - } ); + if ( persister.hasOwnedCollections() ) { + final String entityName = persister.getEntityName(); + final EventMonitor eventMonitor = getEventMonitor(); + final StatisticsImplementor statistics = getFactory().getStatistics(); + forEachOwnedCollection( entity, id, persister, + (descriptor, collection) -> { + final String role = descriptor.getRole(); + firePreRemove( collection, id, entityName, entity ); + final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); + boolean success = false; + try { + descriptor.remove( id, this ); + success = true; + } + finally { + eventMonitor.completeCollectionRemoveEvent( event, id, role, success, this ); + } + firePostRemove( collection, id, entityName, entity ); + if ( statistics.isStatisticsEnabled() ) { + statistics.removeCollection( role ); + } + } ); + } } @@ -430,27 +438,31 @@ public void update(String entityName, Object entity) { } private void removeAndRecreateCollections(Object entity, Object id, EntityPersister persister) { - forEachOwnedCollection( entity, id, persister, - (descriptor, collection) -> { - firePreUpdate( collection, descriptor ); - final EventMonitor eventMonitor = getEventMonitor(); - final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); - boolean success = false; - try { - // TODO: can we do better here? - descriptor.remove( id, this ); - descriptor.recreate( collection, id, this ); - success = true; - } - finally { - eventMonitor.completeCollectionRemoveEvent( event, id, descriptor.getRole(), success, this ); - } - firePostUpdate( collection, descriptor ); - final StatisticsImplementor statistics = getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.updateCollection( descriptor.getRole() ); - } - } ); + if ( persister.hasOwnedCollections() ) { + final String entityName = persister.getEntityName(); + final EventMonitor eventMonitor = getEventMonitor(); + final StatisticsImplementor statistics = getFactory().getStatistics(); + forEachOwnedCollection( entity, id, persister, + (descriptor, collection) -> { + final String role = descriptor.getRole(); + firePreUpdate( collection, id, entityName, entity ); + final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); + boolean success = false; + try { + // TODO: can we do better here? + descriptor.remove( id, this ); + descriptor.recreate( collection, id, this ); + success = true; + } + finally { + eventMonitor.completeCollectionRemoveEvent( event, id, role, success, this ); + } + firePostUpdate( collection, id, entityName, entity ); + if ( statistics.isStatisticsEnabled() ) { + statistics.updateCollection( role ); + } + } ); + } } @Override @@ -635,44 +647,44 @@ protected void firePostDelete(Object entity, Object id, EntityPersister persiste } // Hibernate Reactive may need to call this - protected void firePreRecreate(PersistentCollection collection, CollectionPersister persister) { + protected void firePreRecreate(PersistentCollection collection, Object id, String entityName, Object owner) { eventListenerGroups.eventListenerGroup_PRE_COLLECTION_RECREATE.fireLazyEventOnEachListener( - () -> new PreCollectionRecreateEvent( persister, collection, null ), + () -> new PreCollectionRecreateEvent( collection, id, entityName, owner ), PreCollectionRecreateEventListener::onPreRecreateCollection ); } // Hibernate Reactive may need to call this - protected void firePreUpdate(PersistentCollection collection, CollectionPersister persister) { + protected void firePreUpdate(PersistentCollection collection, Object id, String entityName, Object owner) { eventListenerGroups.eventListenerGroup_PRE_COLLECTION_UPDATE.fireLazyEventOnEachListener( - () -> new PreCollectionUpdateEvent( persister, collection, null ), + () -> new PreCollectionUpdateEvent( collection, id, entityName, owner ), PreCollectionUpdateEventListener::onPreUpdateCollection ); } // Hibernate Reactive may need to call this - protected void firePreRemove(PersistentCollection collection, Object owner, CollectionPersister persister) { + protected void firePreRemove(PersistentCollection collection, Object id, String entityName, Object owner) { eventListenerGroups.eventListenerGroup_PRE_COLLECTION_REMOVE.fireLazyEventOnEachListener( - () -> new PreCollectionRemoveEvent( persister, collection, null, owner ), + () -> new PreCollectionRemoveEvent( collection, id, entityName, owner ), PreCollectionRemoveEventListener::onPreRemoveCollection ); } // Hibernate Reactive may need to call this - protected void firePostRecreate(PersistentCollection collection, CollectionPersister persister) { + protected void firePostRecreate(PersistentCollection collection, Object id, String entityName, Object owner) { eventListenerGroups.eventListenerGroup_POST_COLLECTION_RECREATE.fireLazyEventOnEachListener( - () -> new PostCollectionRecreateEvent( persister, collection, null ), + () -> new PostCollectionRecreateEvent( collection, id, entityName, owner ), PostCollectionRecreateEventListener::onPostRecreateCollection ); } // Hibernate Reactive may need to call this - protected void firePostUpdate(PersistentCollection collection, CollectionPersister persister) { + protected void firePostUpdate(PersistentCollection collection, Object id, String entityName, Object owner) { eventListenerGroups.eventListenerGroup_POST_COLLECTION_UPDATE.fireLazyEventOnEachListener( - () -> new PostCollectionUpdateEvent( persister, collection, null ), + () -> new PostCollectionUpdateEvent( collection, id, entityName, owner ), PostCollectionUpdateEventListener::onPostUpdateCollection ); } // Hibernate Reactive may need to call this - protected void firePostRemove(PersistentCollection collection, Object owner, CollectionPersister persister) { + protected void firePostRemove(PersistentCollection collection, Object id, String entityName, Object owner) { eventListenerGroups.eventListenerGroup_POST_COLLECTION_REMOVE.fireLazyEventOnEachListener( - () -> new PostCollectionRemoveEvent( persister, collection, null, owner ), + () -> new PostCollectionRemoveEvent( collection, id, entityName, owner ), PostCollectionRemoveEventListener::onPostRemoveCollection ); } From 2ad0f58640a4a75ee0a994af7cc4a92c471f7979 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Fri, 27 Jun 2025 12:11:20 +0200 Subject: [PATCH 2/2] HHH-19523 test for collection listeners in stateless session --- ...lectionListenerInStatelessSessionTest.java | 107 ++++++++++++++++++ .../orm/test/stateless/events/EntityA.java | 32 ++++++ .../orm/test/stateless/events/EntityB.java | 21 ++++ 3 files changed, 160 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityA.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityB.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java new file mode 100644 index 000000000000..a29443efec85 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/CollectionListenerInStatelessSessionTest.java @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.stateless.events; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.event.spi.PostCollectionRecreateEvent; +import org.hibernate.event.spi.PostCollectionRecreateEventListener; +import org.hibernate.event.spi.PostCollectionRemoveEvent; +import org.hibernate.event.spi.PostCollectionRemoveEventListener; +import org.hibernate.event.spi.PreCollectionRecreateEvent; +import org.hibernate.event.spi.PreCollectionRecreateEventListener; +import org.hibernate.event.spi.PreCollectionRemoveEvent; +import org.hibernate.event.spi.PreCollectionRemoveEventListener; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = {EntityA.class, EntityB.class}) +@SessionFactory +class CollectionListenerInStatelessSessionTest { + + @Test + void statelessInsert(SessionFactoryScope scope) { + EventListenerRegistry registry = + scope.getSessionFactory().unwrap(SessionFactoryImplementor.class) + .getServiceRegistry().getService(EventListenerRegistry.class); + var preRecreate = new MyPreCollectionRecreateEventListener(); + var preRemove = new MyPreCollectionRemoveEventListener(); + var postRecreate = new MyPostCollectionRecreateEventListener(); + var postRemove = new MyPostCollectionRemoveEventListener(); + registry.getEventListenerGroup(EventType.PRE_COLLECTION_RECREATE) + .appendListener( preRecreate ); + registry.getEventListenerGroup(EventType.PRE_COLLECTION_REMOVE) + .appendListener( preRemove ); + registry.getEventListenerGroup(EventType.POST_COLLECTION_RECREATE) + .appendListener( postRecreate ); + registry.getEventListenerGroup(EventType.POST_COLLECTION_REMOVE) + .appendListener( postRemove ); + + scope.inStatelessTransaction(statelessSession -> { + EntityA a = new EntityA(); + EntityB b = new EntityB(); + a.children.add(b); + statelessSession.insert( b ); + statelessSession.insert( a ); + statelessSession.delete( a ); + statelessSession.delete( b ); + }); + + assertEquals(1, preRecreate.called); + assertEquals(1, preRemove.called); + assertEquals(1, postRecreate.called); + assertEquals(1, postRemove.called); + } + +} + +class MyPreCollectionRecreateEventListener implements PreCollectionRecreateEventListener { + + int called = 0; + + @Override + public void onPreRecreateCollection(PreCollectionRecreateEvent event) { + called++; + } + +} + +class MyPreCollectionRemoveEventListener implements PreCollectionRemoveEventListener { + + int called = 0; + + @Override + public void onPreRemoveCollection(PreCollectionRemoveEvent event) { + called++; + } + +} + +class MyPostCollectionRecreateEventListener implements PostCollectionRecreateEventListener { + + int called = 0; + + @Override + public void onPostRecreateCollection(PostCollectionRecreateEvent event) { + called++; + } + +} + +class MyPostCollectionRemoveEventListener implements PostCollectionRemoveEventListener { + + int called = 0; + + @Override + public void onPostRemoveCollection(PostCollectionRemoveEvent event) { + called++; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityA.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityA.java new file mode 100644 index 000000000000..183d38fcc9ef --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityA.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.stateless.events; + +import java.util.ArrayList; +import java.util.Collection; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +@Entity +@Table(name = "ENTITY_A") +public class EntityA { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + Integer id; + + @OneToMany + @JoinColumn(name = "ENTITY_A") + Collection children = new ArrayList<>(); + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityB.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityB.java new file mode 100644 index 000000000000..2ec190c13335 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/events/EntityB.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.stateless.events; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "ENTITY_B") +public class EntityB { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + Integer id; +}