Skip to content

Lighter interceptors: more efficient initialization and lower memory usage #10387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 27, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,54 @@
* @author Steve Ebersole
*/
public abstract class AbstractInterceptor implements SessionAssociableInterceptor {
private final String entityName;

private transient SharedSessionContractImplementor session;
private boolean allowLoadOutsideTransaction;
private String sessionFactoryUuid;
private SessionAssociationMarkers sessionAssociation;

public AbstractInterceptor(String entityName) {
this.entityName = entityName;
}

public String getEntityName() {
return entityName;
protected AbstractInterceptor() {
}

@Override
public SharedSessionContractImplementor getLinkedSession() {
return session;
return this.sessionAssociation != null ? this.sessionAssociation.session : null;
}

@Override
public void setSession(SharedSessionContractImplementor session) {
this.session = session;
if ( session != null && !allowLoadOutsideTransaction ) {
this.allowLoadOutsideTransaction = session.getFactory().getSessionFactoryOptions().isInitializeLazyStateOutsideTransactionsEnabled();
if ( this.allowLoadOutsideTransaction ) {
this.sessionFactoryUuid = session.getFactory().getUuid();
}
if ( session != null ) {
this.sessionAssociation = session.getSessionAssociationMarkers();
}
else {
unsetSession();
}
}

@Override
public void unsetSession() {
this.session = null;
if ( this.sessionAssociation != null ) {
//We shouldn't mutate the original instance as it's shared across multiple entities,
//but we can get a version of it which represents the same state except it doesn't have the session set:
this.sessionAssociation = this.sessionAssociation.deAssociatedCopy();
}
}

@Override
public boolean allowLoadOutsideTransaction() {
return allowLoadOutsideTransaction;
if ( this.sessionAssociation != null ) {
return this.sessionAssociation.allowLoadOutsideTransaction;
}
else {
return false;
}
}

@Override
public String getSessionFactoryUuid() {
return sessionFactoryUuid;
if ( this.sessionAssociation != null ) {
return this.sessionAssociation.sessionFactoryUuid;
}
else {
return null;
}
}

/**
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,68 +18,35 @@
import org.hibernate.type.CompositeType;
import org.hibernate.type.Type;

import static java.util.Collections.unmodifiableSet;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker;
import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTrackerType;
import static org.hibernate.internal.util.collections.CollectionHelper.toSmallSet;

/**
* @author Steve Ebersole
*/
public class EnhancementAsProxyLazinessInterceptor extends AbstractLazyLoadInterceptor {
private final Set<String> identifierAttributeNames;
private final CompositeType nonAggregatedCidMapper;
public class EnhancementAsProxyLazinessInterceptor extends AbstractInterceptor implements BytecodeLazyAttributeInterceptor {

private final EntityKey entityKey;

private final boolean inLineDirtyChecking;
private final EntityRelatedState meta;
private Set<String> writtenFieldNames;
private Set<String> collectionAttributeNames;

private Status status;

private final boolean initializeBeforeWrite;

public EnhancementAsProxyLazinessInterceptor(
String entityName,
Set<String> identifierAttributeNames,
CompositeType nonAggregatedCidMapper,
EntityRelatedState meta,
EntityKey entityKey,
SharedSessionContractImplementor session) {
super( entityName, session );

this.identifierAttributeNames = identifierAttributeNames;
assert identifierAttributeNames != null;

this.nonAggregatedCidMapper = nonAggregatedCidMapper;
assert nonAggregatedCidMapper != null || identifierAttributeNames.size() == 1;

this.entityKey = entityKey;

final EntityPersister entityPersister =
session.getFactory().getMappingMetamodel()
.getEntityDescriptor( entityName );
if ( entityPersister.hasCollections() ) {
final Type[] propertyTypes = entityPersister.getPropertyTypes();
final String[] propertyNames = entityPersister.getPropertyNames();
collectionAttributeNames = new HashSet<>();
for ( int i = 0; i < propertyTypes.length; i++ ) {
Type propertyType = propertyTypes[i];
if ( propertyType instanceof CollectionType ) {
collectionAttributeNames.add( propertyNames[i] );
}
}
}

this.inLineDirtyChecking = isSelfDirtinessTrackerType( entityPersister.getMappedClass() );
// if self-dirty tracking is enabled but DynamicUpdate is not enabled then we need to
// initialize the entity because the precomputed update statement contains even not
// dirty properties. And so we need all the values we have to initialize. Or, if it's
// versioned, we need to fetch the current version.
initializeBeforeWrite =
!inLineDirtyChecking
|| !entityPersister.getEntityMetamodel().isDynamicUpdate()
|| entityPersister.isVersioned();
this.meta = meta;
status = Status.UNINITIALIZED;
setSession( session );
}

@Override
public String getEntityName() {
return meta.entityName;
}

public EntityKey getEntityKey() {
Expand All @@ -95,7 +62,7 @@ protected Object handleRead(Object target, String attributeName, Object value) {

// the attribute being read is an entity-id attribute
// - we already know the id, return that
if ( identifierAttributeNames.contains( attributeName ) ) {
if ( meta.identifierAttributeNames.contains( attributeName ) ) {
return extractIdValue( target, attributeName );
}

Expand All @@ -113,14 +80,10 @@ private Object read(
final Object[] writtenAttributeValues;
final AttributeMapping[] writtenAttributeMappings;

final EntityPersister entityPersister =
session.getFactory().getMappingMetamodel()
.getEntityDescriptor( getEntityName() );

if ( writtenFieldNames != null && !writtenFieldNames.isEmpty() ) {

// enhancement has dirty-tracking available and at least one attribute was explicitly set

final EntityPersister entityPersister = meta.persister;
if ( writtenFieldNames.contains( attributeName ) ) {
// the requested attribute was one of the attributes explicitly set,
// we can just return the explicitly-set value
Expand Down Expand Up @@ -155,7 +118,7 @@ private Object read(
for ( int i = 0; i < writtenAttributeMappings.length; i++ ) {
final AttributeMapping attribute = writtenAttributeMappings[i];
attribute.setValue(target, writtenAttributeValues[i] );
if ( inLineDirtyChecking ) {
if ( meta.inLineDirtyChecking ) {
asSelfDirtinessTracker(target).$$_hibernate_trackChange( attribute.getAttributeName() );
}
}
Expand All @@ -167,6 +130,7 @@ private Object read(

private Object extractIdValue(Object target, String attributeName) {
// access to the id or part of it for non-aggregated cid
final CompositeType nonAggregatedCidMapper = meta.nonAggregatedCidMapper;
if ( nonAggregatedCidMapper == null ) {
return getIdentifier();
}
Expand Down Expand Up @@ -211,17 +175,13 @@ public Object forceInitialize(
);
}

final EntityPersister persister =
session.getFactory().getMappingMetamodel()
.getEntityDescriptor( getEntityName() );

if ( isTemporarySession ) {
// Add an entry for this entity in the PC of the temp Session
session.getPersistenceContext()
.addEnhancedProxy( entityKey, asPersistentAttributeInterceptable( target ) );
}

return persister.initializeEnhancedEntityUsedAsProxy( target, attributeName, session );
return meta.persister.initializeEnhancedEntityUsedAsProxy( target, attributeName, session );
}

@Override
Expand All @@ -230,19 +190,19 @@ protected Object handleWrite(Object target, String attributeName, Object oldValu
throw new IllegalStateException( "EnhancementAsProxyLazinessInterceptor interception on an initialized instance" );
}

if ( identifierAttributeNames.contains( attributeName ) ) {
if ( meta.identifierAttributeNames.contains( attributeName ) ) {
// it is illegal for the identifier value to be changed. Normally Hibernate
// validates this during flush. However, here it's dangerous to just allow the
// new value to be set and continue on waiting for the flush for validation
// because this interceptor manages the entity's entry in the PC itself. So
// just do the check here up-front
final boolean changed;
if ( nonAggregatedCidMapper == null ) {
if ( meta.nonAggregatedCidMapper == null ) {
changed = ! entityKey.getPersister().getIdentifierType().isEqual( oldValue, newValue );
}
else {
final int subAttrIndex = nonAggregatedCidMapper.getPropertyIndex( attributeName );
final Type subAttrType = nonAggregatedCidMapper.getSubtypes()[subAttrIndex];
final int subAttrIndex = meta.nonAggregatedCidMapper.getPropertyIndex( attributeName );
final Type subAttrType = meta.nonAggregatedCidMapper.getSubtypes()[subAttrIndex];
changed = ! subAttrType.isEqual( oldValue, newValue );
}

Expand All @@ -255,8 +215,8 @@ protected Object handleWrite(Object target, String attributeName, Object oldValu
return newValue;
}

if ( initializeBeforeWrite
|| collectionAttributeNames != null && collectionAttributeNames.contains( attributeName ) ) {
if ( meta.initializeBeforeWrite
|| meta.collectionAttributeNames != null && meta.collectionAttributeNames.contains( attributeName ) ) {
// we need to force-initialize the proxy - the fetch group to which the `attributeName` belongs
try {
forceInitialize( target, attributeName );
Expand All @@ -265,7 +225,7 @@ protected Object handleWrite(Object target, String attributeName, Object oldValu
setInitialized();
}

if ( inLineDirtyChecking ) {
if ( meta.inLineDirtyChecking ) {
asSelfDirtinessTracker( target ).$$_hibernate_trackChange( attributeName );
}
}
Expand Down Expand Up @@ -303,7 +263,7 @@ public boolean isAttributeLoaded(String fieldName) {
throw new UnsupportedOperationException( "Call to EnhancementAsProxyLazinessInterceptor#isAttributeLoaded on an interceptor which is marked as initialized" );
}
// Only fields from the identifier are loaded (until it's initialized)
return identifierAttributeNames.contains( fieldName );
return meta.identifierAttributeNames.contains( fieldName );
}

@Override
Expand Down Expand Up @@ -345,4 +305,58 @@ private enum Status {
INITIALIZING,
INITIALIZED
}

/**
* This is an helper object to group all state which relates to a particular entity type,
* and which is needed for this interceptor.
* Grouping such state allows for upfront construction as a per-entity singleton:
* this reduces processing work on creation of an interceptor instance and is more
* efficient from a point of view of memory usage and memory layout.
*/
public static class EntityRelatedState {

private final String entityName;
private final CompositeType nonAggregatedCidMapper;
private final Set<String> identifierAttributeNames;
private final Set<String> collectionAttributeNames;
private final boolean initializeBeforeWrite;
private final boolean inLineDirtyChecking;
private final EntityPersister persister;

public EntityRelatedState(EntityPersister persister,
CompositeType nonAggregatedCidMapper,
Set<String> identifierAttributeNames) {
this.identifierAttributeNames = identifierAttributeNames;
this.entityName = persister.getEntityName();
this.nonAggregatedCidMapper = nonAggregatedCidMapper;
this.persister = persister;
assert nonAggregatedCidMapper != null || identifierAttributeNames.size() == 1;

if ( persister.hasCollections() ) {
final Set<String> tmpCollectionAttributeNames = new HashSet<>();
final Type[] propertyTypes = persister.getPropertyTypes();
final String[] propertyNames = persister.getPropertyNames();
for ( int i = 0; i < propertyTypes.length; i++ ) {
Type propertyType = propertyTypes[i];
if ( propertyType instanceof CollectionType ) {
tmpCollectionAttributeNames.add( propertyNames[i] );
}
}
this.collectionAttributeNames = toSmallSet( unmodifiableSet( tmpCollectionAttributeNames ) );
}
else {
this.collectionAttributeNames = Collections.emptySet();
}

this.inLineDirtyChecking = isSelfDirtinessTrackerType( persister.getMappedClass() );
// if self-dirty tracking is enabled but DynamicUpdate is not enabled then we need to
// initialize the entity because the precomputed update statement contains even not
// dirty properties. And so we need all the values we have to initialize. Or, if it's
// versioned, we need to fetch the current version.
this.initializeBeforeWrite =
!inLineDirtyChecking
|| !persister.getEntityMetamodel().isDynamicUpdate()
|| persister.isVersioned();
}
}
}
Loading