diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractInterceptor.java index 0cc5df1e2e21..aa44d4ea92e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractInterceptor.java @@ -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; + } } /** diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractLazyLoadInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractLazyLoadInterceptor.java deleted file mode 100644 index d2f90411ce9f..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/AbstractLazyLoadInterceptor.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.bytecode.enhance.spi.interceptor; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; - -/** - * @author Steve Ebersole - */ -public abstract class AbstractLazyLoadInterceptor extends AbstractInterceptor implements BytecodeLazyAttributeInterceptor { - - @SuppressWarnings("unused") - public AbstractLazyLoadInterceptor(String entityName) { - super( entityName ); - } - - public AbstractLazyLoadInterceptor(String entityName, SharedSessionContractImplementor session) { - super( entityName ); - setSession( session ); - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java index d60364deb6c9..23b1030963d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java @@ -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 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 writtenFieldNames; - private Set collectionAttributeNames; - private Status status; - private final boolean initializeBeforeWrite; - public EnhancementAsProxyLazinessInterceptor( - String entityName, - Set 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() { @@ -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 ); } @@ -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 @@ -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() ); } } @@ -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(); } @@ -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 @@ -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 ); } @@ -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 ); @@ -265,7 +225,7 @@ protected Object handleWrite(Object target, String attributeName, Object oldValu setInitialized(); } - if ( inLineDirtyChecking ) { + if ( meta.inLineDirtyChecking ) { asSelfDirtinessTracker( target ).$$_hibernate_trackChange( attributeName ); } } @@ -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 @@ -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 identifierAttributeNames; + private final Set collectionAttributeNames; + private final boolean initializeBeforeWrite; + private final boolean inLineDirtyChecking; + private final EntityPersister persister; + + public EntityRelatedState(EntityPersister persister, + CompositeType nonAggregatedCidMapper, + Set 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 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(); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementHelper.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementHelper.java index d8f72e96d678..4cb4bede5908 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementHelper.java @@ -150,8 +150,8 @@ public static T performWork( String attributeName) { SharedSessionContractImplementor session = interceptor.getLinkedSession(); - boolean isTempSession = false; - boolean isJta = false; + final boolean isTempSession; + final boolean isJta; // first figure out which Session to use if ( session == null ) { @@ -160,7 +160,7 @@ public static T performWork( isTempSession = true; } else { - throwLazyInitializationException( Cause.NO_SESSION, entityName, attributeName ); + throw createLazyInitializationException( Cause.NO_SESSION, entityName, attributeName ); } } else if ( !session.isOpen() ) { @@ -169,7 +169,7 @@ else if ( !session.isOpen() ) { isTempSession = true; } else { - throwLazyInitializationException( Cause.CLOSED_SESSION, entityName, attributeName ); + throw createLazyInitializationException( Cause.CLOSED_SESSION, entityName, attributeName ); } } else if ( !session.isConnected() ) { @@ -178,9 +178,12 @@ else if ( !session.isConnected() ) { isTempSession = true; } else { - throwLazyInitializationException( Cause.DISCONNECTED_SESSION, entityName, attributeName); + throw createLazyInitializationException( Cause.DISCONNECTED_SESSION, entityName, attributeName); } } + else { + isTempSession = false; + } // If we are using a temporary Session, begin a transaction if necessary if ( isTempSession ) { @@ -198,6 +201,9 @@ else if ( !session.isConnected() ) { session.beginTransaction(); } } + else { + isJta = false; + } try { // do the actual work @@ -238,29 +244,13 @@ enum Cause { NO_SF_UUID } - private static void throwLazyInitializationException(Cause cause, String entityName, String attributeName) { - final String reason; - switch ( cause ) { - case NO_SESSION: { - reason = "no session and settings disallow loading outside the Session"; - break; - } - case CLOSED_SESSION: { - reason = "session is closed and settings disallow loading outside the Session"; - break; - } - case DISCONNECTED_SESSION: { - reason = "session is disconnected and settings disallow loading outside the Session"; - break; - } - case NO_SF_UUID: { - reason = "could not determine SessionFactory UUId to create temporary Session for loading"; - break; - } - default: { - reason = ""; - } - } + private static LazyInitializationException createLazyInitializationException(final Cause cause, final String entityName, final String attributeName) { + final String reason = switch ( cause ) { + case NO_SESSION -> "no session and settings disallow loading outside the Session"; + case CLOSED_SESSION -> "session is closed and settings disallow loading outside the Session"; + case DISCONNECTED_SESSION -> "session is disconnected and settings disallow loading outside the Session"; + case NO_SF_UUID -> "could not determine SessionFactory UUId to create temporary Session for loading"; + }; final String message = String.format( Locale.ROOT, @@ -270,7 +260,7 @@ private static void throwLazyInitializationException(Cause cause, String entityN reason ); - throw new LazyInitializationException( message ); + return new LazyInitializationException( message ); } private static SharedSessionContractImplementor openTemporarySessionForLoading( @@ -278,7 +268,7 @@ private static SharedSessionContractImplementor openTemporarySessionForLoading( String entityName, String attributeName) { if ( interceptor.getSessionFactoryUuid() == null ) { - throwLazyInitializationException( Cause.NO_SF_UUID, entityName, attributeName ); + throw createLazyInitializationException( Cause.NO_SF_UUID, entityName, attributeName ); } final SessionFactoryImplementor sf = SessionFactoryRegistry.INSTANCE.getSessionFactory( interceptor.getSessionFactoryUuid() ); 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 901e8ea42bff..50bb275549ce 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 @@ -27,22 +27,24 @@ * @author Luis Barreiro * @author Steve Ebersole */ -public class LazyAttributeLoadingInterceptor extends AbstractLazyLoadInterceptor { - private final Object identifier; +public class LazyAttributeLoadingInterceptor extends AbstractInterceptor implements BytecodeLazyAttributeInterceptor { - //N.B. this Set needs to be treated as immutable - private Set lazyFields; + private final Object identifier; + private EntityRelatedState entityMeta; private Set initializedLazyFields; - private Set mutableLazyFields; public LazyAttributeLoadingInterceptor( - String entityName, + EntityRelatedState entityMeta, Object identifier, - Set lazyFields, SharedSessionContractImplementor session) { - super( entityName, session ); this.identifier = identifier; - this.lazyFields = lazyFields; + this.entityMeta = entityMeta; + setSession( session ); + } + + @Override + public String getEntityName() { + return entityMeta.entityName; } @Override @@ -62,9 +64,7 @@ protected Object handleRead(Object target, String attributeName, Object value) { @Override protected Object handleWrite(Object target, String attributeName, Object oldValue, Object newValue) { - if ( !isAttributeLoaded( attributeName ) ) { - attributeInitialized( attributeName ); - } + attributeInitialized( attributeName ); return newValue; } @@ -125,7 +125,7 @@ public boolean isAttributeLoaded(String fieldName) { } private boolean isLazyAttribute(String fieldName) { - return lazyFields.contains( fieldName ); + return entityMeta.lazyFields.contains( fieldName ); } private boolean isInitializedLazyField(String fieldName) { @@ -133,7 +133,7 @@ private boolean isInitializedLazyField(String fieldName) { } public boolean hasAnyUninitializedAttributes() { - if ( lazyFields.isEmpty() ) { + if ( entityMeta.lazyFields.isEmpty() ) { return false; } @@ -141,7 +141,7 @@ public boolean hasAnyUninitializedAttributes() { return true; } - for ( String fieldName : lazyFields ) { + for ( String fieldName : entityMeta.lazyFields ) { if ( !initializedLazyFields.contains( fieldName ) ) { return true; } @@ -152,7 +152,7 @@ public boolean hasAnyUninitializedAttributes() { @Override public String toString() { - return getClass().getSimpleName() + "(entityName=" + getEntityName() + " ,lazyFields=" + lazyFields + ')'; + return getClass().getSimpleName() + "(entityName=" + getEntityName() + " ,lazyFields=" + entityMeta.lazyFields + ')'; } private void takeCollectionSizeSnapshot(Object target, String fieldName, Object value) { @@ -192,14 +192,43 @@ public Set getInitializedLazyAttributeNames() { } public void addLazyFieldByGraph(String fieldName) { - if ( mutableLazyFields == null ) { - mutableLazyFields = new HashSet<>( lazyFields ); - lazyFields = mutableLazyFields; + if ( entityMeta.shared ) { + //We need to make a defensive copy first to not affect other entities of the same type; + //as we create a copy we lose some of the efficacy of using a separate class to track this state, + //but this is a corner case so we prefer to optimise for the common case. + entityMeta = entityMeta.toNonSharedMutableState(); } - mutableLazyFields.add( fieldName ); + entityMeta.lazyFields.add( fieldName ); } public void clearInitializedLazyFields() { initializedLazyFields = null; } + + /** + * 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 Set lazyFields; + private final boolean shared; + + public EntityRelatedState(String entityName, Set lazyFields) { + this.entityName = entityName; + this.lazyFields = lazyFields; //N.B. this is an immutable, compact set + this.shared = true; + } + private EntityRelatedState(String entityName, Set lazyFields, boolean shared) { + this.entityName = entityName; + this.lazyFields = lazyFields; + this.shared = shared; + } + private EntityRelatedState toNonSharedMutableState() { + return new EntityRelatedState( entityName, new HashSet<>( lazyFields ), false ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/SessionAssociationMarkers.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/SessionAssociationMarkers.java new file mode 100644 index 000000000000..39e2428e4d51 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/SessionAssociationMarkers.java @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.bytecode.enhance.spi.interceptor; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +/** + * Contains the state necessary to properly implement SessionAssociableInterceptor; + * this allows reuse of this particular object across multiple interceptor instances + * and keeps them from getting too allocation-intensive. + * It also allows for a very elegant, simple way for the Session to clear references + * to itself on close. + */ +public final class SessionAssociationMarkers { + + private static final SessionAssociationMarkers NON_ASSOCIATED = new SessionAssociationMarkers(); + + final boolean allowLoadOutsideTransaction; + final String sessionFactoryUuid; + transient SharedSessionContractImplementor session; //TODO by resetting this one field on Session#close we might be able to avoid iterating on all managed instances to de-associate them? Only if we guarantee all managed types use this. + + public SessionAssociationMarkers(final SharedSessionContractImplementor session) { + this.allowLoadOutsideTransaction = session.getFactory().getSessionFactoryOptions() + .isInitializeLazyStateOutsideTransactionsEnabled(); + if ( this.allowLoadOutsideTransaction ) { + this.sessionFactoryUuid = session.getFactory().getUuid(); + } + else { + this.sessionFactoryUuid = null; + } + this.session = session; + } + + /** + * Constructor for the singleton representing non-associated + * state. + */ + private SessionAssociationMarkers() { + this.allowLoadOutsideTransaction = false; + this.sessionFactoryUuid = null; + this.session = null; + } + + /** + * Copying constructor for when we're allowed to load outside of transactions + * and need to transparently reassociated to the SessionFactory having the + * specified UUID. + * @param sessionFactoryUuid + */ + private SessionAssociationMarkers(String sessionFactoryUuid) { + this.allowLoadOutsideTransaction = true; + this.sessionFactoryUuid = sessionFactoryUuid; + this.session = null; + } + + public SessionAssociationMarkers deAssociatedCopy() { + if ( allowLoadOutsideTransaction ) { + return new SessionAssociationMarkers( sessionFactoryUuid ); + } + else { + return NON_ASSOCIATED; + } + } + + /** + * Careful as this mutates the state of this instance, which is possibly + * used by multiple managed entities. + * Removes the reference to the session; useful on Session close. + */ + public void sessionClosed() { + this.session = null; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java index ccd73db655af..c4926385fba7 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeEnhancementMetadataPojoImpl.java @@ -72,6 +72,8 @@ public static BytecodeEnhancementMetadata from( private final CompositeType nonAggregatedCidMapper; private final boolean enhancedForLazyLoading; private final LazyAttributesMetadata lazyAttributesMetadata; + private final LazyAttributeLoadingInterceptor.EntityRelatedState lazyAttributeLoadingInterceptorState; + private volatile transient EnhancementAsProxyLazinessInterceptor.EntityRelatedState enhancementAsProxyInterceptorState; BytecodeEnhancementMetadataPojoImpl( String entityName, @@ -89,6 +91,8 @@ public static BytecodeEnhancementMetadata from( this.identifierAttributeNames = identifierAttributeNames; this.enhancedForLazyLoading = enhancedForLazyLoading; this.lazyAttributesMetadata = lazyAttributesMetadata; + this.lazyAttributeLoadingInterceptorState = new LazyAttributeLoadingInterceptor.EntityRelatedState( + getEntityName(), lazyAttributesMetadata.getLazyAttributeNames() ); } @Override @@ -217,9 +221,8 @@ public LazyAttributeLoadingInterceptor injectInterceptor( ); } final LazyAttributeLoadingInterceptor interceptor = new LazyAttributeLoadingInterceptor( - getEntityName(), + this.lazyAttributeLoadingInterceptorState, identifier, - lazyAttributesMetadata.getLazyAttributeNames(), session ); @@ -233,12 +236,11 @@ public void injectEnhancedEntityAsProxyInterceptor( Object entity, EntityKey entityKey, SharedSessionContractImplementor session) { + EnhancementAsProxyLazinessInterceptor.EntityRelatedState meta = getEnhancementAsProxyLazinessInterceptorMetastate( session ); injectInterceptor( entity, new EnhancementAsProxyLazinessInterceptor( - entityName, - identifierAttributeNames, - nonAggregatedCidMapper, + meta, entityKey, session ), @@ -246,6 +248,23 @@ public void injectEnhancedEntityAsProxyInterceptor( ); } + //This state object needs to be lazily initialized as it needs access to the Persister, but once + //initialized it can be reused across multiple sessions. + private EnhancementAsProxyLazinessInterceptor.EntityRelatedState getEnhancementAsProxyLazinessInterceptorMetastate(SharedSessionContractImplementor session) { + EnhancementAsProxyLazinessInterceptor.EntityRelatedState state = this.enhancementAsProxyInterceptorState; + if ( state == null ) { + final EntityPersister entityPersister = session.getFactory().getMappingMetamodel() + .getEntityDescriptor( entityName ); + state = new EnhancementAsProxyLazinessInterceptor.EntityRelatedState( + entityPersister, + nonAggregatedCidMapper, + identifierAttributeNames + ); + this.enhancementAsProxyInterceptorState = state; + } + return state; + } + @Override public void injectInterceptor( Object entity, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index 5b73af08a162..63ad547da9eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -40,6 +40,7 @@ import org.hibernate.Transaction; import org.hibernate.UnknownProfileException; import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.jdbc.LobCreator; @@ -1237,6 +1238,11 @@ public Object loadFromSecondLevelCache(EntityPersister persister, EntityKey enti return delegate.loadFromSecondLevelCache( persister, entityKey, instanceToLoad, lockMode ); } + @Override + public SessionAssociationMarkers getSessionAssociationMarkers() { + return delegate.getSessionAssociationMarkers(); + } + @Override public boolean isIdentifierRollbackEnabled() { return delegate.isIdentifierRollbackEnabled(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java index a39963527114..c304cb9f899c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java @@ -17,6 +17,7 @@ import org.hibernate.LockOptions; import org.hibernate.StatelessSession; import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.dialect.Dialect; import org.hibernate.event.spi.EventSource; import org.hibernate.query.Query; @@ -572,4 +573,15 @@ default boolean isStatelessSession() { */ @Incubating Object loadFromSecondLevelCache(EntityPersister persister, EntityKey entityKey, Object instanceToLoad, LockMode lockMode); + + /** + * Wrap all state that lazy loading interceptors might need to + * manage association with this session, or to handle lazy loading + * after detachment via the UUID of the SessionFactory. + * N.B. this captures the current Session, however it can get + * updated to a null session (for detached entities) or updated to + * a different Session. + */ + @Incubating + SessionAssociationMarkers getSessionAssociationMarkers(); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 20c61c050270..351216a702eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -20,6 +20,7 @@ import org.hibernate.SharedSessionContract; import org.hibernate.Transaction; import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.jdbc.LobCreator; @@ -693,6 +694,11 @@ public Object loadFromSecondLevelCache(EntityPersister persister, EntityKey enti return delegate.loadFromSecondLevelCache( persister, entityKey, instanceToLoad, lockMode ); } + @Override + public SessionAssociationMarkers getSessionAssociationMarkers() { + return delegate.getSessionAssociationMarkers(); + } + @Override public boolean isIdentifierRollbackEnabled() { return delegate.isIdentifierRollbackEnabled(); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index b058c6a77738..1ba70aa4843d 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -26,6 +26,7 @@ import org.hibernate.UnknownEntityTypeException; import org.hibernate.binder.internal.TenantIdBinder; import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.dialect.Dialect; @@ -179,6 +180,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont //Lazily initialized private transient ExceptionConverter exceptionConverter; + private transient SessionAssociationMarkers sessionAssociationMarkers; public AbstractSharedSessionContract(SessionFactoryImpl factory, SessionCreationOptions options) { this.factory = factory; @@ -476,6 +478,11 @@ public void close() { removeSharedSessionTransactionObserver( transactionCoordinator ); } + if ( sessionAssociationMarkers != null ) { + sessionAssociationMarkers.sessionClosed(); + sessionAssociationMarkers = null; + } + try { if ( shouldCloseJdbcCoordinatorOnClose( isTransactionCoordinatorShared ) ) { jdbcCoordinator.close(); @@ -1586,6 +1593,14 @@ public FormatMapper getJsonFormatMapper() { return factoryOptions.getJsonFormatMapper(); } + @Override + public SessionAssociationMarkers getSessionAssociationMarkers() { + if ( this.sessionAssociationMarkers == null ) { + this.sessionAssociationMarkers = new SessionAssociationMarkers( this ); + } + return sessionAssociationMarkers; + } + @Serial private void writeObject(ObjectOutputStream oos) throws IOException { if ( log.isTraceEnabled() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java index 3f5de91f651a..ed33ffb83280 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractEntityInstantiatorPojo.java @@ -23,6 +23,7 @@ public abstract class AbstractEntityInstantiatorPojo extends AbstractPojoInstant private final EntityMetamodel entityMetamodel; private final Class proxyInterface; private final boolean applyBytecodeInterception; + private final LazyAttributeLoadingInterceptor.EntityRelatedState loadingInterceptorState; public AbstractEntityInstantiatorPojo( EntityMetamodel entityMetamodel, @@ -34,17 +35,25 @@ public AbstractEntityInstantiatorPojo( //TODO this PojoEntityInstantiator appears to not be reused ?! this.applyBytecodeInterception = isPersistentAttributeInterceptableType( persistentClass.getMappedClass() ); + if ( applyBytecodeInterception ) { + this.loadingInterceptorState = new LazyAttributeLoadingInterceptor.EntityRelatedState( + entityMetamodel.getName(), + entityMetamodel.getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getLazyAttributeNames() + ); + } + else { + this.loadingInterceptorState = null; + } } protected Object applyInterception(Object entity) { if ( applyBytecodeInterception ) { asPersistentAttributeInterceptable( entity ) .$$_hibernate_setInterceptor( new LazyAttributeLoadingInterceptor( - entityMetamodel.getName(), + loadingInterceptorState, null, - entityMetamodel.getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getLazyAttributeNames(), null ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityInstantiatorPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityInstantiatorPojoStandard.java index aad8deb5811a..2aacce91bd95 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityInstantiatorPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityInstantiatorPojoStandard.java @@ -25,10 +25,9 @@ public class EntityInstantiatorPojoStandard extends AbstractEntityInstantiatorPojo { private static final CoreMessageLogger LOG = CoreLogging.messageLogger( EntityInstantiatorPojoStandard.class ); - private final EntityMetamodel entityMetamodel; private final Class proxyInterface; private final boolean applyBytecodeInterception; - + private final LazyAttributeLoadingInterceptor.EntityRelatedState loadingInterceptorState; private final Constructor constructor; public EntityInstantiatorPojoStandard( @@ -36,10 +35,20 @@ public EntityInstantiatorPojoStandard( PersistentClass persistentClass, JavaType javaType) { super( entityMetamodel, persistentClass, javaType ); - this.entityMetamodel = entityMetamodel; proxyInterface = persistentClass.getProxyInterface(); constructor = isAbstract() ? null : resolveConstructor( getMappedPojoClass() ); applyBytecodeInterception = isPersistentAttributeInterceptableType( persistentClass.getMappedClass() ); + if ( applyBytecodeInterception ) { + this.loadingInterceptorState = new LazyAttributeLoadingInterceptor.EntityRelatedState( + entityMetamodel.getName(), + entityMetamodel.getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getLazyAttributeNames() + ); + } + else { + this.loadingInterceptorState = null; + } } protected static Constructor resolveConstructor(Class mappedPojoClass) { @@ -62,11 +71,8 @@ protected Object applyInterception(Object entity) { if ( applyBytecodeInterception ) { asPersistentAttributeInterceptable( entity ) .$$_hibernate_setInterceptor( new LazyAttributeLoadingInterceptor( - entityMetamodel.getName(), + loadingInterceptorState, null, - entityMetamodel.getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getLazyAttributeNames(), null ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index 9cb4b41d9712..f822c2136a3c 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -59,6 +59,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import static java.util.Collections.singleton; +import static java.util.Collections.unmodifiableSet; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.util.ReflectHelper.isAbstractClass; import static org.hibernate.internal.util.ReflectHelper.isFinalClass; @@ -193,10 +194,11 @@ public EntityMetamodel( final Set idAttributeNames; if ( identifierMapperComponent != null ) { nonAggregatedCidMapper = identifierMapperComponent.getType(); - idAttributeNames = new HashSet<>( ); + HashSet tmpSet = new HashSet<>(); for ( Property property : identifierMapperComponent.getProperties() ) { - idAttributeNames.add( property.getName() ); + tmpSet.add( property.getName() ); } + idAttributeNames = toSmallSet( unmodifiableSet( tmpSet ) ); } else { nonAggregatedCidMapper = null;