diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java index 2cab523720b2..f3bfee79ca16 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java @@ -55,13 +55,17 @@ public class BeanValidationEventListener BeanValidationEventListener.class.getName() ); - private HibernateTraversableResolver traversableResolver; - private Validator validator; - private GroupsPerOperation groupsPerOperation; + private final HibernateTraversableResolver traversableResolver; + private final Validator validator; + private final GroupsPerOperation groupsPerOperation; public BeanValidationEventListener( - ValidatorFactory factory, Map<String, Object> settings, ClassLoaderService classLoaderService) { - traversableResolver = new HibernateTraversableResolver(); + ValidatorFactory factory, + Map<String, Object> settings, + ClassLoaderService classLoaderService, + SessionFactoryImplementor sessionFactory) { + + traversableResolver = new HibernateTraversableResolver( sessionFactory.getPersistenceUnitUtil() ); validator = factory.usingContext() .traversableResolver( traversableResolver ) .getValidator(); @@ -80,7 +84,6 @@ public boolean onPreInsert(PreInsertEvent event) { validate( event.getEntity(), event.getPersister(), - event.getFactory(), GroupsPerOperation.Operation.INSERT ); return false; @@ -90,7 +93,6 @@ public boolean onPreUpdate(PreUpdateEvent event) { validate( event.getEntity(), event.getPersister(), - event.getFactory(), GroupsPerOperation.Operation.UPDATE ); return false; @@ -100,7 +102,6 @@ public boolean onPreDelete(PreDeleteEvent event) { validate( event.getEntity(), event.getPersister(), - event.getFactory(), GroupsPerOperation.Operation.DELETE ); return false; @@ -111,7 +112,6 @@ public boolean onPreUpsert(PreUpsertEvent event) { validate( event.getEntity(), event.getPersister(), - event.getFactory(), GroupsPerOperation.Operation.UPSERT ); return false; @@ -123,7 +123,6 @@ public void onPreUpdateCollection(PreCollectionUpdateEvent event) { validate( entity, event.getSession().getEntityPersister( event.getAffectedOwnerEntityName(), entity ), - event.getFactory(), GroupsPerOperation.Operation.UPDATE ); } @@ -131,7 +130,6 @@ public void onPreUpdateCollection(PreCollectionUpdateEvent event) { private <T> void validate( T object, EntityPersister persister, - SessionFactoryImplementor sessionFactory, GroupsPerOperation.Operation operation) { if ( object == null || persister.getRepresentationStrategy().getMode() != RepresentationMode.POJO ) { return; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java index 333819b5d056..299cae47a8c0 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java @@ -20,6 +20,7 @@ import org.hibernate.type.EntityType; import org.hibernate.type.Type; +import jakarta.persistence.PersistenceUnitUtil; import jakarta.validation.Path; import jakarta.validation.TraversableResolver; @@ -32,6 +33,11 @@ */ public class HibernateTraversableResolver implements TraversableResolver { private final Map<Class<?>, Set<String>> associationsPerEntityClass = new HashMap<>(); + private final PersistenceUnitUtil persistenceUnitUtil; + + public HibernateTraversableResolver(PersistenceUnitUtil persistenceUnitUtil) { + this.persistenceUnitUtil = persistenceUnitUtil; + } public void addPersister(EntityPersister persister, SessionFactoryImplementor factory) { Class<?> javaTypeClass = persister.getEntityMappingType().getMappedJavaType().getJavaTypeClass(); @@ -91,7 +97,7 @@ public boolean isReachable(Object traversableObject, ElementType elementType) { //lazy, don't load return Hibernate.isInitialized( traversableObject ) - && Hibernate.isPropertyInitialized( traversableObject, traversableProperty.getName() ); + && persistenceUnitUtil.isLoaded( traversableObject, traversableProperty.getName() ); } public boolean isCascadable(Object traversableObject, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java index 9a220f8c7df1..5fd0708dc553 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java @@ -157,7 +157,12 @@ private static void setupListener(ValidatorFactory validatorFactory, SessionFact final ClassLoaderService classLoaderService = serviceRegistry.requireService( ClassLoaderService.class ); final ConfigurationService cfgService = serviceRegistry.requireService( ConfigurationService.class ); final BeanValidationEventListener listener = - new BeanValidationEventListener( validatorFactory, cfgService.getSettings(), classLoaderService ); + new BeanValidationEventListener( + validatorFactory, + cfgService.getSettings(), + classLoaderService, + sessionFactory + ); final EventListenerRegistry listenerRegistry = serviceRegistry.requireService( EventListenerRegistry.class ); listenerRegistry.addDuplicationStrategy( DuplicationStrategyImpl.INSTANCE ); listenerRegistry.appendListeners( EventType.PRE_INSERT, listener ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/LazyPropertiesFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/LazyPropertiesFetchTest.java new file mode 100644 index 000000000000..0efe4eabaaeb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/LazyPropertiesFetchTest.java @@ -0,0 +1,123 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.beanvalidation; + +import java.util.List; + +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.Size; + +@DomainModel(annotatedClasses = { + LazyPropertiesFetchTest.Association.class, + LazyPropertiesFetchTest.MutableEntity.class +}) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.JAKARTA_VALIDATION_MODE, value = "CALLBACK")) +@SessionFactory(useCollectingStatementInspector = true) +@JiraKey("HHH-19203") +class LazyPropertiesFetchTest { + + @AfterAll + static void cleanup(SessionFactoryScope scope) { + scope.dropData(); + } + + @Test + void testLazyCollectionNotFetched(SessionFactoryScope scope) { + scope.inTransaction( session -> { + MutableEntity mutableEntity = new MutableEntity(); + mutableEntity.id = 1L; + mutableEntity.lazyCollection = List.of( 1, 2 ); + session.persist( mutableEntity ); + } ); + + SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( session -> { + MutableEntity fetched = session.find( MutableEntity.class, 1L ); + inspector.assertExecutedCount( 1 ); + fetched.mutableField = 1; + } ); + + inspector.assertExecutedCount( 2 ); + } + + @Test + void testLazyCollectionFetchDoesntDependOnEachOther(SessionFactoryScope scope) { + scope.inTransaction( session -> { + MutableEntity mutableEntity = new MutableEntity(); + mutableEntity.id = 2L; + mutableEntity.lazyCollection = List.of( 1, 2 ); + + Association asso = new Association(); + asso.id = 1L; + asso.lazyCollection = List.of( 2, 3 ); + + mutableEntity.lazyAssociation = List.of( asso ); + + session.persist( mutableEntity ); + } ); + + SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( session -> { + MutableEntity fetched = session.find( MutableEntity.class, 2L ); + inspector.assertExecutedCount( 1 ); + + Association asso = fetched.lazyAssociation.get( 0 ); + inspector.assertExecutedCount( 2 ); + + asso.mutableField = 5; + } ); + inspector.assertExecutedCount( 3 ); + } + + @Entity(name = "MutableEntity") + static class MutableEntity { + @Id + private Long id; + + private int mutableField = 0; + + @Size(max = 10) + @ElementCollection + @CollectionTable(name = "LazyPropertiesCollection1") + private List<Integer> lazyCollection; + + @OneToMany(cascade = CascadeType.PERSIST) + private List<Association> lazyAssociation; + } + + @Entity(name = "Association") + static class Association { + @Id + private Long id; + + private int mutableField = 0; + + @Size(max = 10) + @ElementCollection + @CollectionTable(name = "LazyPropertiesCollection2") + private List<Integer> lazyCollection; + } +}