From 2f2dbbe2e62a4e77b86ef64e72c7c01b99c578a8 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 22 Aug 2024 14:04:17 +0200 Subject: [PATCH] HHH-18489 Lazy, unowned one-to-one associations get loaded eagerly in queries - even with bytecode enhancement --- .../LazyAttributeLoadingInterceptor.java | 10 +- .../entity/AbstractEntityPersister.java | 213 +++++++++++++----- .../EntityDelayedFetchInitializer.java | 34 ++- 3 files changed, 191 insertions(+), 66 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java index 92e9da93c5..3555d8d1ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java @@ -34,8 +34,9 @@ public class LazyAttributeLoadingInterceptor extends AbstractLazyLoadInterceptor private final Object identifier; //N.B. this Set needs to be treated as immutable - private final Set lazyFields; + private Set lazyFields; private Set initializedLazyFields; + private Set mutableLazyFields; public LazyAttributeLoadingInterceptor( String entityName, @@ -193,4 +194,11 @@ public class LazyAttributeLoadingInterceptor extends AbstractLazyLoadInterceptor return initializedLazyFields == null ? Collections.emptySet() : initializedLazyFields; } + public void addLazyFieldByGraph(String fieldName) { + if ( mutableLazyFields == null ) { + mutableLazyFields = new HashSet<>( lazyFields ); + lazyFields = mutableLazyFields; + } + mutableLazyFields.add( fieldName ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 03aba6fa1d..608234c137 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -378,6 +378,7 @@ public abstract class AbstractEntityPersister private final String[] lazyPropertyNames; private final int[] lazyPropertyNumbers; private final Type[] lazyPropertyTypes; + private final Set nonLazyPropertyNames; //information about all properties in class hierarchy private final String[] subclassPropertyNameClosure; @@ -469,6 +470,7 @@ public abstract class AbstractEntityPersister private final boolean implementsLifecycle; private List uniqueKeyEntries = null; //lazily initialized + private HashMap nonLazyPropertyLoadPlansByName; public AbstractEntityPersister( final PersistentClass persistentClass, @@ -606,6 +608,7 @@ public abstract class AbstractEntityPersister propertyColumnUpdateable = new boolean[hydrateSpan][]; propertyColumnInsertable = new boolean[hydrateSpan][]; sharedColumnNames = new HashSet<>(); + nonLazyPropertyNames = new HashSet<>(); final HashSet thisClassProperties = new HashSet<>(); final ArrayList lazyNames = new ArrayList<>(); @@ -663,6 +666,9 @@ public abstract class AbstractEntityPersister lazyNumbers.add( i ); lazyTypes.add( prop.getValue().getType() ); } + else { + nonLazyPropertyNames.add( prop.getName() ); + } propertyColumnUpdateable[i] = prop.getValue().getColumnUpdateability(); propertyColumnInsertable[i] = prop.getValue().getColumnInsertability(); @@ -1222,6 +1228,10 @@ public abstract class AbstractEntityPersister partsToSelect.add( getAttributeMapping( getSubclassPropertyIndex( lazyAttributeDescriptor.getName() ) ) ); } + return createLazyLoanPlan( partsToSelect ); + } + + private SingleIdArrayLoadPlan createLazyLoanPlan(List partsToSelect) { if ( partsToSelect.isEmpty() ) { // only one-to-one is lazily fetched return null; @@ -1542,75 +1552,117 @@ public abstract class AbstractEntityPersister final EntityEntry entry, final String fieldName, final SharedSessionContractImplementor session) { - - if ( !hasLazyProperties() ) { - throw new AssertionFailure( "no lazy properties" ); - } - - final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - assert interceptor != null : "Expecting bytecode interceptor to be non-null"; - - LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); - - final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupName( fieldName ); - final List fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupAttributeDescriptors( fetchGroup ); - - final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); - - final SingleIdArrayLoadPlan lazySelect = getSQLLazySelectLoadPlan( fetchGroup ); - - try { - Object result = null; - final Object[] values = lazySelect.load( id, session ); - int i = 0; - for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { - final boolean previousInitialized = initializedLazyAttributeNames.contains( fetchGroupAttributeDescriptor.getName() ); - - if ( previousInitialized ) { - // todo : one thing we should consider here is potentially un-marking an attribute as dirty based on the selected value - // we know the current value - getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); - // we know the selected value (see selectedValue below) - // we can use the attribute Type to tell us if they are the same - // - // assuming entity is a SelfDirtinessTracker we can also know if the attribute is - // currently considered dirty, and if really not dirty we would do the un-marking - // - // of course that would mean a new method on SelfDirtinessTracker to allow un-marking - - // its already been initialized (e.g. by a write) so we don't want to overwrite - i++; - continue; + if ( nonLazyPropertyNames.contains( fieldName ) ) { + // An eager property can be lazy because of an applied EntityGraph + final List partsToSelect = new ArrayList<>(1); + int propertyIndex = getPropertyIndex( fieldName ); + partsToSelect.add( getAttributeMapping( propertyIndex ) ); + SingleIdArrayLoadPlan lazyLoanPlan; + if ( nonLazyPropertyLoadPlansByName == null ) { + nonLazyPropertyLoadPlansByName = new HashMap<>(); + lazyLoanPlan = createLazyLoanPlan( partsToSelect ); + ; + nonLazyPropertyLoadPlansByName.put( fieldName, lazyLoanPlan ); + } + else { + lazyLoanPlan = nonLazyPropertyLoadPlansByName.get( fieldName ); + if ( lazyLoanPlan == null ) { + lazyLoanPlan = createLazyLoanPlan( partsToSelect ); + ; + nonLazyPropertyLoadPlansByName.put( fieldName, lazyLoanPlan ); } - - final Object selectedValue = values[i++]; - final boolean set = initializeLazyProperty( - fieldName, + } + try { + final Object[] values = lazyLoanPlan.load( id, session ); + final Object selectedValue = values[0]; + initializeLazyProperty( entity, entry, - fetchGroupAttributeDescriptor.getLazyIndex(), - selectedValue + selectedValue, + propertyIndex, + getPropertyTypes()[propertyIndex] ); - if ( set ) { - result = selectedValue; - interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); - } - + return selectedValue; + } + catch (JDBCException ex) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + ex.getSQLException(), + "could not initialize lazy properties: " + infoString( this, id, getFactory() ), + lazyLoanPlan.getJdbcSelect().getSqlString() + ); + } + } + else { + if ( !hasLazyProperties() ) { + throw new AssertionFailure( "no lazy properties" ); } - LOG.trace( "Done initializing lazy properties" ); + final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + assert interceptor != null : "Expecting bytecode interceptor to be non-null"; - return result; - } - catch ( JDBCException ex ) { - throw session.getJdbcServices().getSqlExceptionHelper().convert( - ex.getSQLException(), - "could not initialize lazy properties: " + infoString( this, id, getFactory() ), - lazySelect.getJdbcSelect().getSqlString() - ); + LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); + + final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getFetchGroupName( fieldName ); + final List fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getFetchGroupAttributeDescriptors( fetchGroup ); + + final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); + + final SingleIdArrayLoadPlan lazySelect = getSQLLazySelectLoadPlan( fetchGroup ); + + try { + Object result = null; + final Object[] values = lazySelect.load( id, session ); + int i = 0; + for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { + final boolean previousInitialized = initializedLazyAttributeNames.contains( + fetchGroupAttributeDescriptor.getName() ); + + if ( previousInitialized ) { + // todo : one thing we should consider here is potentially un-marking an attribute as dirty based on the selected value + // we know the current value - getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); + // we know the selected value (see selectedValue below) + // we can use the attribute Type to tell us if they are the same + // + // assuming entity is a SelfDirtinessTracker we can also know if the attribute is + // currently considered dirty, and if really not dirty we would do the un-marking + // + // of course that would mean a new method on SelfDirtinessTracker to allow un-marking + + // its already been initialized (e.g. by a write) so we don't want to overwrite + i++; + continue; + } + + final Object selectedValue = values[i++]; + final boolean set = initializeLazyProperty( + fieldName, + entity, + entry, + fetchGroupAttributeDescriptor, + selectedValue + ); + if ( set ) { + result = selectedValue; + interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + } + } + + LOG.trace( "Done initializing lazy properties" ); + + return result; + + } + catch (JDBCException ex) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + ex.getSQLException(), + "could not initialize lazy properties: " + infoString( this, id, getFactory() ), + lazySelect.getJdbcSelect().getSqlString() + ); + } } } @@ -1669,6 +1721,43 @@ public abstract class AbstractEntityPersister return fieldName.equals( lazyPropertyNames[index] ); } + + + protected boolean initializeLazyProperty( + final String fieldName, + final Object entity, + final EntityEntry entry, + LazyAttributeDescriptor fetchGroupAttributeDescriptor, + final Object propValue) { + final String name = fetchGroupAttributeDescriptor.getName(); + initializeLazyProperty( + entity, + entry, + propValue, + getPropertyIndex( name ), + fetchGroupAttributeDescriptor.getType() + ); + return fieldName.equals( name ); + } + + private void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { + setPropertyValue( entity, index, propValue ); + if ( entry.getLoadedState() != null ) { + // object have been loaded with setReadOnly(true); HHH-2236 + entry.getLoadedState()[index] = type.deepCopy( + propValue, + factory + ); + } + // If the entity has deleted state, then update that as well + if ( entry.getDeletedState() != null ) { + entry.getDeletedState()[index] = type.deepCopy( + propValue, + factory + ); + } + } + @Override public NavigableRole getNavigableRole() { return navigableRole; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java index bdcacb08ba..665b26c171 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java @@ -9,12 +9,16 @@ package org.hibernate.sql.results.graph.entity.internal; import java.util.function.BiConsumer; import org.hibernate.FetchNotFoundException; -import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.EntityUniqueKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.AppliedGraph; +import org.hibernate.graph.spi.AttributeNodeImplementor; import org.hibernate.internal.log.LoggingHelper; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -37,6 +41,7 @@ import org.hibernate.type.Type; import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.determineConcreteEntityDescriptor; /** @@ -193,7 +198,17 @@ public class EntityDelayedFetchInitializer // For unique-key mappings, we always use bytecode-laziness if possible, // because we can't generate a proxy based on the unique key yet if ( referencedModelPart.isLazy() ) { - instance = LazyPropertyInitializer.UNFETCHED_PROPERTY; + instance = UNFETCHED_PROPERTY; + } + else if ( getParent().isEntityInitializer() && isLazyByGraph( rowProcessingState ) ) { + // todo : manage the case when parent is an EmbeddableInitializer + final Object resolvedInstance = getParent().asEntityInitializer() + .getResolvedInstance( rowProcessingState ); + final LazyAttributeLoadingInterceptor persistentAttributeInterceptor = (LazyAttributeLoadingInterceptor) ManagedTypeHelper + .asPersistentAttributeInterceptable( resolvedInstance ).$$_hibernate_getInterceptor(); + + persistentAttributeInterceptor.addLazyFieldByGraph( navigablePath.getLocalName() ); + instance = UNFETCHED_PROPERTY; } else { instance = concreteDescriptor.loadByUniqueKey( @@ -224,7 +239,7 @@ public class EntityDelayedFetchInitializer // For primary key based mappings we only use bytecode-laziness if the attribute is optional, // because the non-optionality implies that it is safe to have a proxy else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) { - instance = LazyPropertyInitializer.UNFETCHED_PROPERTY; + instance = UNFETCHED_PROPERTY; } else { instance = session.internalLoad( @@ -244,6 +259,19 @@ public class EntityDelayedFetchInitializer } } + private boolean isLazyByGraph(RowProcessingState rowProcessingState) { + final AppliedGraph appliedGraph = rowProcessingState.getQueryOptions().getAppliedGraph(); + if ( appliedGraph != null && appliedGraph.getSemantic() == GraphSemantic.FETCH ) { + final AttributeNodeImplementor attributeNode = appliedGraph.getGraph() + .findAttributeNode( navigablePath.getLocalName() ); + if ( attributeNode != null && attributeNode.getAttributeDescriptor() == getInitializedPart().asAttributeMapping() ) { + return false; + } + return true; + } + return false; + } + @Override public void resolveInstance(Object instance, EntityDelayedFetchInitializerData data) { if ( instance == null ) {