From 793a30bf59843403a2120267f357e229405f7e18 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Mon, 15 Sep 2025 14:54:00 +0200 Subject: [PATCH 1/2] [#1770] Fix Better error message when transparently loading lazy fields --- gradle/libs.versions.toml | 1 + hibernate-reactive-core/build.gradle | 1 + .../spi/ReactiveBootstrapContextAdapter.java | 187 ++++++++++++++++++ ...EnhancementAsProxyLazinessInterceptor.java | 42 ++++ ...activeLazyAttributeLoadingInterceptor.java | 40 ++++ ...odeEnhancementMetadataPojoImplAdapter.java | 107 ++++++++++ .../hibernate/reactive/logging/impl/Log.java | 3 + ...activeEntityInstantiatorPojoOptimized.java | 42 ++++ ...eactiveEntityInstantiatorPojoStandard.java | 41 ++++ ...ityRepresentationStrategyPojoStandard.java | 47 +++++ .../ReactiveRuntimeModelCreationContext.java | 5 +- ...tiveManagedTypeRepresentationResolver.java | 40 ++++ .../session/impl/ReactiveSessionImpl.java | 17 +- .../tuple/entity/ReactiveEntityMetamodel.java | 16 ++ 14 files changed, 586 insertions(+), 3 deletions(-) create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea2af6f9d..af8cab01b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,7 @@ org-hibernate-orm-hibernate-core = { group = "org.hibernate.orm", name = "hibern org-hibernate-orm-hibernate-jcache = { group = "org.hibernate.orm", name = "hibernate-jcache", version.ref = "hibernateOrmVersion" } org-hibernate-orm-hibernate-jpamodelgen = { group = "org.hibernate.orm", name = "hibernate-jpamodelgen", version.ref = "hibernateOrmVersion" } org-hibernate-validator-hibernate-validator = { group = "org.hibernate.validator", name = "hibernate-validator", version = "8.0.3.Final" } +org-hibernate-models = { group = "org.hibernate.models", name = "hibernate-models", version = "1.0.1" } org-jboss-logging-jboss-logging = { group = "org.jboss.logging", name = "jboss-logging", version.ref = "jbossLoggingVersion" } org-jboss-logging-jboss-logging-annotations = { group = "org.jboss.logging", name = "jboss-logging-annotations", version.ref = "jbossLoggingAnnotationVersion" } org-jboss-logging-jboss-logging-processor = { group = "org.jboss.logging", name = "jboss-logging-processor", version.ref = "jbossLoggingAnnotationVersion" } diff --git a/hibernate-reactive-core/build.gradle b/hibernate-reactive-core/build.gradle index dee6afd59..15f30d7f9 100644 --- a/hibernate-reactive-core/build.gradle +++ b/hibernate-reactive-core/build.gradle @@ -15,6 +15,7 @@ apply from: publishScript dependencies { api(libs.org.hibernate.orm.hibernate.core) + compileOnly(libs.org.hibernate.models) api(libs.io.smallrye.reactive.mutiny) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java new file mode 100644 index 000000000..8134378ee --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java @@ -0,0 +1,187 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.boot.spi; + +import org.hibernate.boot.CacheRegionDefinition; +import org.hibernate.boot.archive.scan.spi.ScanEnvironment; +import org.hibernate.boot.archive.scan.spi.ScanOptions; +import org.hibernate.boot.archive.spi.ArchiveDescriptorFactory; +import org.hibernate.boot.model.convert.spi.ConverterDescriptor; +import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.boot.spi.ClassLoaderAccess; +import org.hibernate.boot.spi.ClassmateContext; +import org.hibernate.boot.spi.MetadataBuildingOptions; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.jpa.spi.MutableJpaCompliance; +import org.hibernate.metamodel.spi.ManagedTypeRepresentationResolver; +import org.hibernate.models.spi.ModelsContext; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.reactive.metamodel.spi.ReactiveManagedTypeRepresentationResolver; +import org.hibernate.resource.beans.spi.BeanInstanceProducer; +import org.hibernate.resource.beans.spi.ManagedBeanRegistry; +import org.hibernate.type.BasicType; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Adapt {@link BootstrapContext#getRepresentationStrategySelector()} to return a {@link ReactiveManagedTypeRepresentationResolver} + */ +public class ReactiveBootstrapContextAdapter implements BootstrapContext { + + private final BootstrapContext delegate; + + public ReactiveBootstrapContextAdapter(BootstrapContext bootstrapContext) { + this.delegate = bootstrapContext; + } + + @Override + public StandardServiceRegistry getServiceRegistry() { + return delegate.getServiceRegistry(); + } + + @Override + public MutableJpaCompliance getJpaCompliance() { + return delegate.getJpaCompliance(); + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return delegate.getTypeConfiguration(); + } + + @Override + public ModelsContext getModelsContext() { + return delegate.getModelsContext(); + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return delegate.getFunctionRegistry(); + } + + @Override + public BeanInstanceProducer getCustomTypeProducer() { + return delegate.getCustomTypeProducer(); + } + + @Override + public MetadataBuildingOptions getMetadataBuildingOptions() { + return delegate.getMetadataBuildingOptions(); + } + + @Override + public ClassLoaderService getClassLoaderService() { + return delegate.getClassLoaderService(); + } + + @Override + public ManagedBeanRegistry getManagedBeanRegistry() { + return delegate.getManagedBeanRegistry(); + } + + @Override + public ConfigurationService getConfigurationService() { + return delegate.getConfigurationService(); + } + + @Override + public boolean isJpaBootstrap() { + return delegate.isJpaBootstrap(); + } + + @Override + public void markAsJpaBootstrap() { + delegate.markAsJpaBootstrap(); + } + + @Override + public ClassLoader getJpaTempClassLoader() { + return delegate.getJpaTempClassLoader(); + } + + @Override + public ClassLoaderAccess getClassLoaderAccess() { + return delegate.getClassLoaderAccess(); + } + + @Override + public ClassmateContext getClassmateContext() { + return delegate.getClassmateContext(); + } + + @Override + public ArchiveDescriptorFactory getArchiveDescriptorFactory() { + return delegate.getArchiveDescriptorFactory(); + } + + @Override + public ScanOptions getScanOptions() { + return delegate.getScanOptions(); + } + + @Override + public ScanEnvironment getScanEnvironment() { + return delegate.getScanEnvironment(); + } + + @Override + public Object getScanner() { + return delegate.getScanner(); + } + + @Override + public Object getJandexView() { + return delegate.getJandexView(); + } + + @Override + public Map getSqlFunctions() { + return delegate.getSqlFunctions(); + } + + @Override + public Collection getAuxiliaryDatabaseObjectList() { + return List.of(); + } + + @Override + public Collection> getAttributeConverters() { + return delegate.getAttributeConverters(); + } + + @Override + public Collection getCacheRegionDefinitions() { + return delegate.getCacheRegionDefinitions(); + } + + @Override + public ManagedTypeRepresentationResolver getRepresentationStrategySelector() { + return ReactiveManagedTypeRepresentationResolver.INSTANCE; + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public void registerAdHocBasicType(BasicType basicType) { + delegate.registerAdHocBasicType( basicType ); + } + + @Override + public BasicType resolveAdHocBasicType(String key) { + return delegate.resolveAdHocBasicType( key ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java new file mode 100644 index 000000000..2b759400b --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java @@ -0,0 +1,42 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.bythecode.enhance.spi.interceptor; + +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; + +import java.lang.invoke.MethodHandles; + +/** + * Reactive version of {@link EnhancementAsProxyLazinessInterceptor}. + * + * It throws a {@link org.hibernate.LazyInitializationException} when a lazy attribute + * is not fetched using {@link org.hibernate.reactive.mutiny.Mutiny#fetch(Object)} + * or {@link org.hibernate.reactive.stage.Stage#fetch(Object)} but transparently + */ +public class ReactiveEnhancementAsProxyLazinessInterceptor extends EnhancementAsProxyLazinessInterceptor { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveEnhancementAsProxyLazinessInterceptor( + EntityRelatedState meta, + EntityKey entityKey, + SharedSessionContractImplementor session) { + super( meta, entityKey, session ); + } + + @Override + protected Object handleRead(Object target, String attributeName, Object value) { + if ( isIdentifier( attributeName ) ) { + return super.handleRead( target, attributeName, value ); + } + else { + throw LOG.lazyFieldInitializationException( attributeName, getEntityName() ); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java new file mode 100644 index 000000000..7db34f6fe --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java @@ -0,0 +1,40 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.bythecode.enhance.spi.internal; + +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; + +import java.lang.invoke.MethodHandles; + +/** + * Reactive version of {@link LazyAttributeLoadingInterceptor}. + * + * It throws a {@link org.hibernate.LazyInitializationException} when a lazy attribute + * is not fetched using {@link org.hibernate.reactive.mutiny.Mutiny#fetch(Object)} + * or {@link org.hibernate.reactive.stage.Stage#fetch(Object)} but transparently + */ +public class ReactiveLazyAttributeLoadingInterceptor extends LazyAttributeLoadingInterceptor { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveLazyAttributeLoadingInterceptor( + EntityRelatedState entityMeta, + Object identifier, + SharedSessionContractImplementor session) { + super( entityMeta, identifier, session ); + } + + @Override + protected Object handleRead(Object target, String attributeName, Object value) { + if ( !isAttributeLoaded( attributeName ) ) { + throw LOG.lazyFieldInitializationException( attributeName, getEntityName() ); + } + return value; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java new file mode 100644 index 000000000..5c8b94b2d --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java @@ -0,0 +1,107 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.bythecode.spi; + +import org.hibernate.boot.Metadata; +import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributesMetadata; +import org.hibernate.bytecode.internal.BytecodeEnhancementMetadataPojoImpl; +import org.hibernate.bytecode.spi.NotInstrumentedException; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.reactive.bythecode.enhance.spi.interceptor.ReactiveEnhancementAsProxyLazinessInterceptor; +import org.hibernate.reactive.bythecode.enhance.spi.internal.ReactiveLazyAttributeLoadingInterceptor; +import org.hibernate.type.CompositeType; + +import java.util.Set; + +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptableType; + +/** + * Extends {@link BytecodeEnhancementMetadataPojoImpl} to inject Reactive versions of {@link BytecodeLazyAttributeInterceptor} + */ +public class ReactiveBytecodeEnhancementMetadataPojoImplAdapter extends BytecodeEnhancementMetadataPojoImpl { + + public static BytecodeEnhancementMetadataPojoImpl from( + PersistentClass persistentClass, + Set identifierAttributeNames, + CompositeType nonAggregatedCidMapper, + boolean collectionsInDefaultFetchGroupEnabled, + Metadata metadata) { + final Class mappedClass = persistentClass.getMappedClass(); + final boolean enhancedForLazyLoading = isPersistentAttributeInterceptableType( mappedClass ); + final LazyAttributesMetadata lazyAttributesMetadata = enhancedForLazyLoading + ? LazyAttributesMetadata.from( persistentClass, true, collectionsInDefaultFetchGroupEnabled, metadata ) + : LazyAttributesMetadata.nonEnhanced( persistentClass.getEntityName() ); + + return new ReactiveBytecodeEnhancementMetadataPojoImplAdapter( + persistentClass.getEntityName(), + mappedClass, + identifierAttributeNames, + nonAggregatedCidMapper, + enhancedForLazyLoading, + lazyAttributesMetadata + ); + } + + ReactiveBytecodeEnhancementMetadataPojoImplAdapter(String entityName, Class mappedClass, Set identifierAttributeNames, CompositeType nonAggregatedCidMapper, boolean enhancedForLazyLoading, LazyAttributesMetadata lazyAttributesMetadata) { + super( + entityName, + mappedClass, + identifierAttributeNames, + nonAggregatedCidMapper, + enhancedForLazyLoading, + lazyAttributesMetadata + ); + } + + @Override + public LazyAttributeLoadingInterceptor injectInterceptor( + Object entity, + Object identifier, + SharedSessionContractImplementor session) throws NotInstrumentedException { + if ( !isEnhancedForLazyLoading() ) { + throw new NotInstrumentedException( "Entity class [" + getEntityClass() + .getName() + "] is not enhanced for lazy loading" ); + } + + if ( !getEntityClass().isInstance( entity ) ) { + throw new IllegalArgumentException( + String.format( + "Passed entity instance [%s] is not of expected type [%s]", + entity, + getEntityName() + ) + ); + } + final LazyAttributeLoadingInterceptor interceptor = new ReactiveLazyAttributeLoadingInterceptor( + getLazyAttributeLoadingInterceptorState(), + identifier, + session + ); + + injectInterceptor( entity, interceptor, session ); + + return interceptor; + } + + @Override + public void injectEnhancedEntityAsProxyInterceptor( + Object entity, + EntityKey entityKey, + SharedSessionContractImplementor session) { + final EnhancementAsProxyLazinessInterceptor.EntityRelatedState meta = + getEnhancementAsProxyLazinessInterceptorMetastate( session ); + injectInterceptor( + entity, + new ReactiveEnhancementAsProxyLazinessInterceptor( meta, entityKey, session ), + session + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java index fa29cd8b5..a932f7b58 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java @@ -270,6 +270,9 @@ public interface Log extends BasicLogger { @Message(id = 84, value = "The application requested a JDBC connection, but Hibernate Reactive doesn't use JDBC. This could be caused by a bug or the use of an unsupported feature in Hibernate Reactive") SQLException notUsingJdbc(); + @Message(id = 85, value = "Reactive sessions do not support transparent lazy fetching - use Session.fetch() (property '%1$S' of entity '%2$s' was not loaded)") + LazyInitializationException lazyFieldInitializationException(String fieldName, String entityName); + // Same method that exists in CoreMessageLogger @LogMessage(level = WARN) @Message(id = 104, value = "firstResult/maxResults specified with collection fetch; applying in memory!" ) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java new file mode 100644 index 000000000..252f39e8c --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java @@ -0,0 +1,42 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.internal; + +import org.hibernate.bytecode.spi.ReflectionOptimizer; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityInstantiatorPojoOptimized; +import org.hibernate.reactive.bythecode.enhance.spi.internal.ReactiveLazyAttributeLoadingInterceptor; +import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.descriptor.java.JavaType; + +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; + +/** + * Extends {@link EntityInstantiatorPojoOptimized} to apply a {@link ReactiveLazyAttributeLoadingInterceptor} + */ +public class ReactiveEntityInstantiatorPojoOptimized extends EntityInstantiatorPojoOptimized { + + public ReactiveEntityInstantiatorPojoOptimized( + EntityMetamodel entityMetamodel, + PersistentClass persistentClass, + JavaType javaType, + ReflectionOptimizer.InstantiationOptimizer instantiationOptimizer) { + super( entityMetamodel, persistentClass, javaType, instantiationOptimizer ); + } + + @Override + protected Object applyInterception(Object entity) { + if ( isApplyBytecodeInterception() ) { + asPersistentAttributeInterceptable( entity ) + .$$_hibernate_setInterceptor( new ReactiveLazyAttributeLoadingInterceptor( + getLoadingInterceptorState(), + null, + null + ) ); + } + return entity; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java new file mode 100644 index 000000000..a30a9fc53 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java @@ -0,0 +1,41 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.internal; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityInstantiatorPojoStandard; +import org.hibernate.reactive.bythecode.enhance.spi.internal.ReactiveLazyAttributeLoadingInterceptor; +import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.descriptor.java.JavaType; + +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; + +/** + * Extends {@link EntityInstantiatorPojoStandard} to apply a {@link ReactiveLazyAttributeLoadingInterceptor} + */ +public class ReactiveEntityInstantiatorPojoStandard extends EntityInstantiatorPojoStandard { + + public ReactiveEntityInstantiatorPojoStandard( + EntityMetamodel entityMetamodel, + PersistentClass persistentClass, + JavaType javaType) { + super( entityMetamodel, persistentClass, javaType ); + } + + @Override + protected Object applyInterception(Object entity) { + if ( isApplyBytecodeInterception() ) { + asPersistentAttributeInterceptable( entity ) + .$$_hibernate_setInterceptor( new ReactiveLazyAttributeLoadingInterceptor( + getLoadingInterceptorState(), + null, + null + ) ); + } + return entity; + + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java new file mode 100644 index 000000000..90d336fb5 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java @@ -0,0 +1,47 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.internal; + +import org.hibernate.bytecode.spi.ReflectionOptimizer; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityRepresentationStrategyPojoStandard; +import org.hibernate.metamodel.spi.EntityInstantiator; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.tuple.entity.EntityMetamodel; + +/** + * Extends {@link EntityRepresentationStrategyPojoStandard} + * to create {@link ReactiveEntityInstantiatorPojoOptimized} and {@link ReactiveEntityInstantiatorPojoStandard} + */ +public class ReactiveEntityRepresentationStrategyPojoStandard extends EntityRepresentationStrategyPojoStandard { + + public ReactiveEntityRepresentationStrategyPojoStandard( + PersistentClass bootDescriptor, + EntityPersister runtimeDescriptor, + RuntimeModelCreationContext creationContext) { + super( bootDescriptor, runtimeDescriptor, creationContext ); + } + + @Override + protected EntityInstantiator determineInstantiator( + PersistentClass bootDescriptor, + EntityMetamodel entityMetamodel) { + final ReflectionOptimizer reflectionOptimizer = getReflectionOptimizer(); + if ( reflectionOptimizer != null && reflectionOptimizer.getInstantiationOptimizer() != null ) { + return new ReactiveEntityInstantiatorPojoOptimized( + entityMetamodel, + bootDescriptor, + getMappedJavaType(), + reflectionOptimizer.getInstantiationOptimizer() + ); + } + else { + return new ReactiveEntityInstantiatorPojoStandard( entityMetamodel, bootDescriptor, getMappedJavaType() ); + } + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java index 876312b47..b3d89ebd9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java @@ -22,6 +22,7 @@ import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.reactive.boot.spi.ReactiveBootstrapContextAdapter; import org.hibernate.reactive.tuple.entity.ReactiveEntityMetamodel; import org.hibernate.service.ServiceRegistry; import org.hibernate.tuple.entity.EntityMetamodel; @@ -31,9 +32,11 @@ public class ReactiveRuntimeModelCreationContext implements RuntimeModelCreationContext { private final RuntimeModelCreationContext delegate; + private final BootstrapContext bootstrapContext; public ReactiveRuntimeModelCreationContext(RuntimeModelCreationContext delegate) { this.delegate = delegate; + bootstrapContext = new ReactiveBootstrapContextAdapter( delegate.getBootstrapContext() ); } @Override @@ -48,7 +51,7 @@ public SessionFactoryImplementor getSessionFactory() { @Override public BootstrapContext getBootstrapContext() { - return delegate.getBootstrapContext(); + return bootstrapContext; } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java new file mode 100644 index 000000000..8641cf728 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java @@ -0,0 +1,40 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.spi; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityRepresentationStrategyMap; +import org.hibernate.metamodel.internal.ManagedTypeRepresentationResolverStandard; +import org.hibernate.metamodel.spi.EntityRepresentationStrategy; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.metamodel.internal.ReactiveEntityRepresentationStrategyPojoStandard; + +/** + * Extends {@link ManagedTypeRepresentationResolverStandard} to create a {@link ReactiveEntityRepresentationStrategyPojoStandard} + */ +public class ReactiveManagedTypeRepresentationResolver extends ManagedTypeRepresentationResolverStandard { + + public static final ManagedTypeRepresentationResolverStandard INSTANCE = new ReactiveManagedTypeRepresentationResolver(); + + @Override + public EntityRepresentationStrategy resolveStrategy( + PersistentClass bootDescriptor, + EntityPersister runtimeDescriptor, + RuntimeModelCreationContext creationContext) { + if ( bootDescriptor.getMappedClass() == null ) { // i.e. RepresentationMode.MAP; + return new EntityRepresentationStrategyMap( bootDescriptor, creationContext ); + } + else { + return new ReactiveEntityRepresentationStrategyPojoStandard( + bootDescriptor, + runtimeDescriptor, + creationContext + ); + } + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java index 7b0bd112a..acbbc5bde 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java @@ -18,6 +18,7 @@ import org.hibernate.UnknownEntityTypeException; import org.hibernate.UnresolvableObjectException; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.dialect.Dialect; import org.hibernate.engine.internal.ReactivePersistenceContextAdapter; @@ -359,8 +360,20 @@ else if ( isPersistentAttributeInterceptable( association ) ) { @Override public CompletionStage reactiveFetch(E entity, Attribute field) { - return ( (ReactiveEntityPersister) getEntityPersister( null, entity ) ) - .reactiveInitializeLazyProperty( field, entity, this ); + final ReactiveEntityPersister entityPersister = (ReactiveEntityPersister) getEntityPersister( null, entity ); + LazyAttributeLoadingInterceptor lazyAttributeLoadingInterceptor = entityPersister.getBytecodeEnhancementMetadata() + .extractInterceptor( entity ); + final String attributeName = field.getName(); + if ( !lazyAttributeLoadingInterceptor.isAttributeLoaded( attributeName ) ) { + return ( (CompletionStage) lazyAttributeLoadingInterceptor.fetchAttribute( entity, field.getName() ) ) + .thenApply( value -> { + lazyAttributeLoadingInterceptor.attributeInitialized( attributeName ); + return value; + } ); + } + else { + return completedFuture( (T) entityPersister.getPropertyValue( entity, attributeName ) ); + } } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java index 14e227358..763992b2e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java @@ -5,11 +5,13 @@ */ package org.hibernate.reactive.tuple.entity; +import java.util.Set; import java.util.function.Function; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.generator.Generator; import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.CompositeNestedGeneratedValueGenerator; @@ -28,6 +30,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.bythecode.spi.ReactiveBytecodeEnhancementMetadataPojoImplAdapter; import org.hibernate.reactive.id.ReactiveIdentifierGenerator; import org.hibernate.reactive.id.impl.EmulatedSequenceReactiveIdentifierGenerator; import org.hibernate.reactive.id.impl.ReactiveCompositeNestedGeneratedValueGenerator; @@ -37,6 +40,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.service.ServiceRegistry; import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import static java.lang.invoke.MethodHandles.lookup; @@ -196,4 +200,16 @@ public Type getType() { return identifier.getType(); } } + + @Override + protected BytecodeEnhancementMetadata getBytecodeEnhancementMetadataPojo(PersistentClass persistentClass, RuntimeModelCreationContext creationContext, Set idAttributeNames, CompositeType nonAggregatedCidMapper, boolean collectionsInDefaultFetchGroupEnabled) { + return ReactiveBytecodeEnhancementMetadataPojoImplAdapter.from( + persistentClass, + idAttributeNames, + nonAggregatedCidMapper, + collectionsInDefaultFetchGroupEnabled, + creationContext.getMetadata() + ); + } + } From a2419929e97d3c4eec7a1d72130c87c7cf1eb4e4 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Mon, 15 Sep 2025 14:53:17 +0200 Subject: [PATCH 2/2] [#1770] Add test for Better error message when transparently loading lazy fields --- .../hibernate/reactive/LazyPropertyTest.java | 2 +- .../reactive/it/LazyBasicFieldTest.java | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java index 73233b5e9..acff15717 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java @@ -153,7 +153,7 @@ public Book(String isbn, String title, Author author) { } public Book() { - super( new EntityRelatedState( "Book", singleton( "isbn" ) ), 1, null ); + super( new EntityRelatedState( Book.class.getName(), singleton( "isbn" ) ), 1, null ); } @Override diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java index 68f234b26..cb07324cc 100644 --- a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java @@ -5,6 +5,9 @@ */ package org.hibernate.reactive.it; +import org.hibernate.LazyInitializationException; + +import io.smallrye.mutiny.Uni; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; @@ -47,4 +50,46 @@ public void testFetchBasicField(VertxTestContext context) { ) ) ) ); } + + @Test + public void testTransparentLazyFetching(VertxTestContext context) { + final Crew emily = new Crew(); + emily.setId( 21L ); + emily.setName( "Emily Jackson" ); + emily.setRole( "Passenger" ); + emily.setFate( "Unknown" ); + + test( context, assertThrown( LazyInitializationException.class, getMutinySessionFactory() + .withTransaction( session -> session.persist( emily ) ) + .call( () -> getMutinySessionFactory().withSession( session -> session.find( Crew.class, emily.getId() ) + .invoke( Crew::getRole ) ) ) + ).invoke( exception -> assertThat( exception.getMessage() ).contains( "Reactive sessions do not support transparent lazy fetching" ) ) + ); + } + + @Test + public void testGetReferenceAndTransparentLazyFetching(VertxTestContext context) { + final Crew emily = new Crew(); + emily.setId( 21L ); + emily.setName( "Emily Jackson" ); + emily.setRole( "Passenger" ); + emily.setFate( "Unknown" ); + + test( context, assertThrown( LazyInitializationException.class, getMutinySessionFactory() + .withTransaction( session -> session.persist( emily ) ) + .chain( () -> getMutinySessionFactory().withSession( session -> { + Crew crew = session.getReference( Crew.class, emily.getId() ); + String role = crew.getRole(); + return session.flush(); + } ) ) + ).invoke( exception -> assertThat( exception.getMessage() ).contains( "Reactive sessions do not support transparent lazy fetching" ) ) + ); + } + + public static Uni assertThrown(Class expectedException, Uni uni) { + return uni.onItemOrFailure().transform( (s, e) -> { + assertThat( e ).isInstanceOf( expectedException ); + return (U) e; + } ); + } }