Unexpected association fetch triggered by JSR-303 (Bean Validation) integration

Validation of lazy association through PreUpdateEvent by Hibernate and JSR 303 integration might trigger additional fetches.

Full reproducer: GitHub - Kamii0909/test-case-template-hibernate-orm-6
Fail for 6.x up to latest.

Specifically, @Size(max = 3) @ElementCollection private List<Long> lazyCols will be fetched. Is this intended? JPA states that: Attributes that have not been loaded must not be loaded, so I would assume this is a bug. This behavior is not observed with bytecode enhancement, which is at least consistency issue.

This behavior is caused by Hibernate.isPropertyInitialized(Object entity, String propertyName) quickly bailing out (return true) for non-bytecode enhanced entity, and HibernateTraversableResolver reliance on this method to skip validation:

public boolean isReachable(...) {
return Hibernate.isInitialized( traversableObject ) 
    && Hibernate.isPropertyInitialized( traversableObject, traversableProperty.getName() );
}

Turning on byte-code enhancement is a quick hack since bytecode enhanced entities are treated differently by isPropertyInitialized and properly checked, but all bytecode enhancement options are deprecated in recent Hibernate versions, both runtime or through build tool integrations. Entity returned by getReferenceById is a proper HibernateProxy in all configurations, so isPropertyInitialized will work, but since you can’t update that, no validation will be triggered anyway.

A fix could be:

  • Properly implementing Hibernate.isPropertyInitialized even for non-enhanced or proxied entities. This might prompt some reflection and could upset AoT environment, which is backward compatibility concern. And as a side effect, client can’t customize property access strategy.
  • Introduce additional Hibernate.isPropertyInitialized overload that additionally accepts Supplier<Object> property of some sort, which HibernateTraversableResolver could pass the actual uninitialized PersistentCollection to. This left the property access strategy (reflection/getter…) to the caller. I don’t think the current TraversableResolver interface even has enough information for HibernateTraversableResolver to provide such information.
  • Same as above, but let HibernateTraversableResolver additionally check for Hibernate.isInitialized(getProperty(entity, "propertyName")). Still facing the same implementation issue as above, but can keep the wildly used public API org.hibernate.Hibernate the same.

As far as I can see, the reason this is only implemented for bytecode enhanced entities is the fact that no session factory and hence metadata to access entity state is available for non-enhanced entities.
If you want a portable and working solution that also support non-enhanced entities, you will have to use PersistenceUnitUtil, which you can acquire through jakarta.persistence.EntityManagerFactory#getPersistenceUnitUtil. That has methods isLoaded and since JPA 3.2/ORM 7.0 also load methods which you can use for this purpose.

My issue is with HibernateTraversableResolver lax implemention of isReachable triggering unneccessary fetches. Should I make a PR to address it with your suggestion? Replacing Hibernate.xxx with corresponding PersistenceUnitUtil should work.

Yeah, that sounds good to me.