HHH-18489 Lazy, unowned one-to-one associations get loaded eagerly in queries - even with bytecode enhancement
This commit is contained in:
parent
b407aa7679
commit
2f2dbbe2e6
|
@ -34,8 +34,9 @@ public class LazyAttributeLoadingInterceptor extends AbstractLazyLoadInterceptor
|
||||||
private final Object identifier;
|
private final Object identifier;
|
||||||
|
|
||||||
//N.B. this Set needs to be treated as immutable
|
//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> initializedLazyFields;
|
||||||
|
private Set<String> mutableLazyFields;
|
||||||
|
|
||||||
public LazyAttributeLoadingInterceptor(
|
public LazyAttributeLoadingInterceptor(
|
||||||
String entityName,
|
String entityName,
|
||||||
|
@ -193,4 +194,11 @@ public class LazyAttributeLoadingInterceptor extends AbstractLazyLoadInterceptor
|
||||||
return initializedLazyFields == null ? Collections.emptySet() : initializedLazyFields;
|
return initializedLazyFields == null ? Collections.emptySet() : initializedLazyFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addLazyFieldByGraph(String fieldName) {
|
||||||
|
if ( mutableLazyFields == null ) {
|
||||||
|
mutableLazyFields = new HashSet<>( lazyFields );
|
||||||
|
lazyFields = mutableLazyFields;
|
||||||
|
}
|
||||||
|
mutableLazyFields.add( fieldName );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -378,6 +378,7 @@ public abstract class AbstractEntityPersister
|
||||||
private final String[] lazyPropertyNames;
|
private final String[] lazyPropertyNames;
|
||||||
private final int[] lazyPropertyNumbers;
|
private final int[] lazyPropertyNumbers;
|
||||||
private final Type[] lazyPropertyTypes;
|
private final Type[] lazyPropertyTypes;
|
||||||
|
private final Set<String> nonLazyPropertyNames;
|
||||||
|
|
||||||
//information about all properties in class hierarchy
|
//information about all properties in class hierarchy
|
||||||
private final String[] subclassPropertyNameClosure;
|
private final String[] subclassPropertyNameClosure;
|
||||||
|
@ -469,6 +470,7 @@ public abstract class AbstractEntityPersister
|
||||||
private final boolean implementsLifecycle;
|
private final boolean implementsLifecycle;
|
||||||
|
|
||||||
private List<UniqueKeyEntry> uniqueKeyEntries = null; //lazily initialized
|
private List<UniqueKeyEntry> uniqueKeyEntries = null; //lazily initialized
|
||||||
|
private HashMap<String,SingleIdArrayLoadPlan> nonLazyPropertyLoadPlansByName;
|
||||||
|
|
||||||
public AbstractEntityPersister(
|
public AbstractEntityPersister(
|
||||||
final PersistentClass persistentClass,
|
final PersistentClass persistentClass,
|
||||||
|
@ -606,6 +608,7 @@ public abstract class AbstractEntityPersister
|
||||||
propertyColumnUpdateable = new boolean[hydrateSpan][];
|
propertyColumnUpdateable = new boolean[hydrateSpan][];
|
||||||
propertyColumnInsertable = new boolean[hydrateSpan][];
|
propertyColumnInsertable = new boolean[hydrateSpan][];
|
||||||
sharedColumnNames = new HashSet<>();
|
sharedColumnNames = new HashSet<>();
|
||||||
|
nonLazyPropertyNames = new HashSet<>();
|
||||||
|
|
||||||
final HashSet<Property> thisClassProperties = new HashSet<>();
|
final HashSet<Property> thisClassProperties = new HashSet<>();
|
||||||
final ArrayList<String> lazyNames = new ArrayList<>();
|
final ArrayList<String> lazyNames = new ArrayList<>();
|
||||||
|
@ -663,6 +666,9 @@ public abstract class AbstractEntityPersister
|
||||||
lazyNumbers.add( i );
|
lazyNumbers.add( i );
|
||||||
lazyTypes.add( prop.getValue().getType() );
|
lazyTypes.add( prop.getValue().getType() );
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
nonLazyPropertyNames.add( prop.getName() );
|
||||||
|
}
|
||||||
|
|
||||||
propertyColumnUpdateable[i] = prop.getValue().getColumnUpdateability();
|
propertyColumnUpdateable[i] = prop.getValue().getColumnUpdateability();
|
||||||
propertyColumnInsertable[i] = prop.getValue().getColumnInsertability();
|
propertyColumnInsertable[i] = prop.getValue().getColumnInsertability();
|
||||||
|
@ -1222,6 +1228,10 @@ public abstract class AbstractEntityPersister
|
||||||
partsToSelect.add( getAttributeMapping( getSubclassPropertyIndex( lazyAttributeDescriptor.getName() ) ) );
|
partsToSelect.add( getAttributeMapping( getSubclassPropertyIndex( lazyAttributeDescriptor.getName() ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return createLazyLoanPlan( partsToSelect );
|
||||||
|
}
|
||||||
|
|
||||||
|
private SingleIdArrayLoadPlan createLazyLoanPlan(List<ModelPart> partsToSelect) {
|
||||||
if ( partsToSelect.isEmpty() ) {
|
if ( partsToSelect.isEmpty() ) {
|
||||||
// only one-to-one is lazily fetched
|
// only one-to-one is lazily fetched
|
||||||
return null;
|
return null;
|
||||||
|
@ -1542,75 +1552,117 @@ public abstract class AbstractEntityPersister
|
||||||
final EntityEntry entry,
|
final EntityEntry entry,
|
||||||
final String fieldName,
|
final String fieldName,
|
||||||
final SharedSessionContractImplementor session) {
|
final SharedSessionContractImplementor session) {
|
||||||
|
if ( nonLazyPropertyNames.contains( fieldName ) ) {
|
||||||
if ( !hasLazyProperties() ) {
|
// An eager property can be lazy because of an applied EntityGraph
|
||||||
throw new AssertionFailure( "no lazy properties" );
|
final List<ModelPart> partsToSelect = new ArrayList<>(1);
|
||||||
}
|
int propertyIndex = getPropertyIndex( fieldName );
|
||||||
|
partsToSelect.add( getAttributeMapping( propertyIndex ) );
|
||||||
final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor();
|
SingleIdArrayLoadPlan lazyLoanPlan;
|
||||||
assert interceptor != null : "Expecting bytecode interceptor to be non-null";
|
if ( nonLazyPropertyLoadPlansByName == null ) {
|
||||||
|
nonLazyPropertyLoadPlansByName = new HashMap<>();
|
||||||
LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName );
|
lazyLoanPlan = createLazyLoanPlan( partsToSelect );
|
||||||
|
;
|
||||||
final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata()
|
nonLazyPropertyLoadPlansByName.put( fieldName, lazyLoanPlan );
|
||||||
.getLazyAttributesMetadata()
|
}
|
||||||
.getFetchGroupName( fieldName );
|
else {
|
||||||
final List<LazyAttributeDescriptor> fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata()
|
lazyLoanPlan = nonLazyPropertyLoadPlansByName.get( fieldName );
|
||||||
.getLazyAttributesMetadata()
|
if ( lazyLoanPlan == null ) {
|
||||||
.getFetchGroupAttributeDescriptors( fetchGroup );
|
lazyLoanPlan = createLazyLoanPlan( partsToSelect );
|
||||||
|
;
|
||||||
final Set<String> initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames();
|
nonLazyPropertyLoadPlansByName.put( fieldName, lazyLoanPlan );
|
||||||
|
|
||||||
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++];
|
try {
|
||||||
final boolean set = initializeLazyProperty(
|
final Object[] values = lazyLoanPlan.load( id, session );
|
||||||
fieldName,
|
final Object selectedValue = values[0];
|
||||||
|
initializeLazyProperty(
|
||||||
entity,
|
entity,
|
||||||
entry,
|
entry,
|
||||||
fetchGroupAttributeDescriptor.getLazyIndex(),
|
selectedValue,
|
||||||
selectedValue
|
propertyIndex,
|
||||||
|
getPropertyTypes()[propertyIndex]
|
||||||
);
|
);
|
||||||
if ( set ) {
|
return selectedValue;
|
||||||
result = selectedValue;
|
}
|
||||||
interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() );
|
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;
|
LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName );
|
||||||
}
|
|
||||||
catch ( JDBCException ex ) {
|
final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata()
|
||||||
throw session.getJdbcServices().getSqlExceptionHelper().convert(
|
.getLazyAttributesMetadata()
|
||||||
ex.getSQLException(),
|
.getFetchGroupName( fieldName );
|
||||||
"could not initialize lazy properties: " + infoString( this, id, getFactory() ),
|
final List<LazyAttributeDescriptor> fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata()
|
||||||
lazySelect.getJdbcSelect().getSqlString()
|
.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] );
|
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
|
@Override
|
||||||
public NavigableRole getNavigableRole() {
|
public NavigableRole getNavigableRole() {
|
||||||
return navigableRole;
|
return navigableRole;
|
||||||
|
|
|
@ -9,12 +9,16 @@ package org.hibernate.sql.results.graph.entity.internal;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.hibernate.FetchNotFoundException;
|
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.EntityHolder;
|
||||||
import org.hibernate.engine.spi.EntityKey;
|
import org.hibernate.engine.spi.EntityKey;
|
||||||
import org.hibernate.engine.spi.EntityUniqueKey;
|
import org.hibernate.engine.spi.EntityUniqueKey;
|
||||||
import org.hibernate.engine.spi.PersistenceContext;
|
import org.hibernate.engine.spi.PersistenceContext;
|
||||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
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.internal.log.LoggingHelper;
|
||||||
import org.hibernate.metamodel.mapping.ModelPart;
|
import org.hibernate.metamodel.mapping.ModelPart;
|
||||||
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
|
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
|
||||||
|
@ -37,6 +41,7 @@ import org.hibernate.type.Type;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
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;
|
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,
|
// For unique-key mappings, we always use bytecode-laziness if possible,
|
||||||
// because we can't generate a proxy based on the unique key yet
|
// because we can't generate a proxy based on the unique key yet
|
||||||
if ( referencedModelPart.isLazy() ) {
|
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 {
|
else {
|
||||||
instance = concreteDescriptor.loadByUniqueKey(
|
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,
|
// 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
|
// because the non-optionality implies that it is safe to have a proxy
|
||||||
else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) {
|
else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) {
|
||||||
instance = LazyPropertyInitializer.UNFETCHED_PROPERTY;
|
instance = UNFETCHED_PROPERTY;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
instance = session.internalLoad(
|
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
|
@Override
|
||||||
public void resolveInstance(Object instance, EntityDelayedFetchInitializerData data) {
|
public void resolveInstance(Object instance, EntityDelayedFetchInitializerData data) {
|
||||||
if ( instance == null ) {
|
if ( instance == null ) {
|
||||||
|
|
Loading…
Reference in New Issue