HHH-18489 Lazy, unowned one-to-one associations get loaded eagerly in queries - even with bytecode enhancement

This commit is contained in:
Andrea Boriero 2024-08-22 14:04:17 +02:00 committed by Steve Ebersole
parent b407aa7679
commit 2f2dbbe2e6
3 changed files with 191 additions and 66 deletions

View File

@ -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<String> lazyFields;
private Set<String> lazyFields;
private Set<String> initializedLazyFields;
private Set<String> 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 );
}
}

View File

@ -378,6 +378,7 @@ public abstract class AbstractEntityPersister
private final String[] lazyPropertyNames;
private final int[] lazyPropertyNumbers;
private final Type[] lazyPropertyTypes;
private final Set<String> 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<UniqueKeyEntry> uniqueKeyEntries = null; //lazily initialized
private HashMap<String,SingleIdArrayLoadPlan> 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<Property> thisClassProperties = new HashSet<>();
final ArrayList<String> 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<ModelPart> 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<LazyAttributeDescriptor> fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata()
.getLazyAttributesMetadata()
.getFetchGroupAttributeDescriptors( fetchGroup );
final Set<String> 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<ModelPart> 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<LazyAttributeDescriptor> fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata()
.getLazyAttributesMetadata()
.getFetchGroupAttributeDescriptors( fetchGroup );
final Set<String> 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;

View File

@ -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<Object> 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 ) {