diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java index 70c57c653ca6..01f3eed26e2a 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java @@ -297,6 +297,10 @@ public void resolveInstance(@Nullable Object instance, NonAggregatedIdentifierMa data.setState( State.MISSING ); data.setInstance( null ); } + else if ( hasIdClass ) { + resolveKey( data ); + resolveInstance( data ); + } else { data.setState( State.INITIALIZED ); data.setInstance( instance ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java index 4f3db569faf4..d3984f77afa4 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java @@ -138,10 +138,11 @@ public void resolveInstance(Object instance, Data data) { data.setInstance( null ); return; } - final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); // Only need to extract the identifier if the identifier has a many to one final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); - data.entityKey = null; data.entityIdentifier = null; if ( lazyInitializer == null ) { // Entity is most probably initialized @@ -162,10 +163,10 @@ && getAttributeInterceptor( instance ) // If the entity initializer is null, we know the entity is fully initialized, // otherwise it will be initialized by some other initializer data.setState( State.RESOLVED ); - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); } - if ( keyIsEager && data.entityIdentifier == null ) { - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); + if ( data.entityIdentifier == null ) { + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); } } else if ( lazyInitializer.isUninitialized() ) { @@ -175,15 +176,47 @@ else if ( lazyInitializer.isUninitialized() ) { else { // Entity is initialized data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); - } + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); data.setInstance( lazyInitializer.getImplementation() ); } + data.entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); + final var entityHolder = persistenceContext.getEntityHolder( + data.entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityKey = entityHolder.getEntityKey(); + data.entityIdentifier = data.entityKey.getIdentifier(); + if ( entityHolder.isInitialized() ) { + data.setState( State.INITIALIZED ); + } + else { + data.setState( State.RESOLVED ); + } + } + else { + data.setState( State.RESOLVED ); + } + } + if ( data.getState() == State.RESOLVED ) { - resolveInstanceFromIdentifier( data ); + // similar to resolveInstanceFromIdentifier, but we already have the holder here + if ( data.batchDisabled ) { + initialize( data, entityHolder, session, persistenceContext ); + } + else if ( entityHolder == null || !entityHolder.isEventuallyInitialized() ) { + // need to add the key to the batch queue only when the entity has not been already loaded or + // there isn't another initializer that is loading it + registerResolutionListener( data ); + registerToBatchFetchQueue( data ); + } } + if ( keyIsEager ) { final Initializer initializer = keyAssembler.getInitializer(); assert initializer != null; @@ -233,6 +266,7 @@ public void initializeInstanceFromParent(Object parentInstance, Data data) { : parentInstance; // No need to initialize these fields data.entityKey = null; + data.entityIdentifier = null; data.setInstance( null ); if ( instance == null ) { data.setState( State.MISSING ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java index ba21c6d7b458..6214c59791e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java @@ -207,33 +207,50 @@ public void resolveInstance(Object instance, DiscriminatedEntityInitializerData data.setInstance( null ); } else { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); if ( lazyInitializer == null ) { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - final SharedSessionContractImplementor session = rowProcessingState.getSession(); - data.concreteDescriptor = session.getEntityPersister( null, instance ); - data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); - } + data.concreteDescriptor = session.getEntityPersister( null, instance ); + data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( eager ? State.RESOLVED : State.INITIALIZED ); - if ( keyIsEager ) { - // Read the discriminator from the result set if necessary - final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); - data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); - } + // Read the discriminator from the result set if necessary + final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); + data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); } else { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.concreteDescriptor = rowProcessingState.getSession().getEntityPersister( null, lazyInitializer.getImplementation() ); - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + data.concreteDescriptor = session.getEntityPersister( null, lazyInitializer.getImplementation() ); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + } + + + final var entityKey = new EntityKey( data.entityIdentifier, data.concreteDescriptor ); + final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( + entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setState( !eager || entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else { + data.setState( State.RESOLVED ); + initializeInstance( data ); } } - data.setInstance( instance ); + else { + data.setInstance( instance ); + } + if ( keyIsEager ) { final Initializer initializer = keyValueAssembler.getInitializer(); assert initializer != null; 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 92441140cc40..bb0912b29af6 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 @@ -174,94 +174,102 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { concreteDescriptor = entityPersister; } - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - if ( selectByUniqueKey ) { - final String uniqueKeyPropertyName = referencedModelPart.getReferencedPropertyName(); - final Type uniqueKeyPropertyType = ( referencedModelPart.getReferencedPropertyName() == null ) ? - concreteDescriptor.getIdentifierType() : - session.getFactory().getRuntimeMetamodels() - .getReferencedPropertyType( - concreteDescriptor.getEntityName(), - uniqueKeyPropertyName - ); - - final EntityUniqueKey euk = new EntityUniqueKey( - concreteDescriptor.getEntityName(), - uniqueKeyPropertyName, - data.entityIdentifier, - uniqueKeyPropertyType, - session.getFactory() - ); - Object instance = persistenceContext.getEntity( euk ); - if ( instance == null ) { - // 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 = UNFETCHED_PROPERTY; - } - else { - // Try to load a PersistentAttributeInterceptable. If we get one, we can add the lazy - // field to the interceptor. If we don't get one, we load the entity by unique key. - PersistentAttributeInterceptable persistentAttributeInterceptable = null; - if ( getParent().isEntityInitializer() && isLazyByGraph( rowProcessingState ) ) { - final Object resolvedInstance = - getParent().asEntityInitializer().getResolvedInstance( rowProcessingState ); - persistentAttributeInterceptable = - ManagedTypeHelper.asPersistentAttributeInterceptableOrNull( resolvedInstance ); - } + initialize( data, null, concreteDescriptor ); + } + } - if ( persistentAttributeInterceptable != null ) { - final LazyAttributeLoadingInterceptor persistentAttributeInterceptor = (LazyAttributeLoadingInterceptor) persistentAttributeInterceptable.$$_hibernate_getInterceptor(); - persistentAttributeInterceptor.addLazyFieldByGraph( navigablePath.getLocalName() ); - instance = UNFETCHED_PROPERTY; - } - else { - instance = concreteDescriptor.loadByUniqueKey( - uniqueKeyPropertyName, - data.entityIdentifier, - session + protected void initialize(EntityDelayedFetchInitializerData data, @Nullable EntityKey entityKey, EntityPersister concreteDescriptor) { + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final SharedSessionContractImplementor session = rowProcessingState.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + if ( selectByUniqueKey ) { + final String uniqueKeyPropertyName = referencedModelPart.getReferencedPropertyName(); + final Type uniqueKeyPropertyType = ( referencedModelPart.getReferencedPropertyName() == null ) ? + concreteDescriptor.getIdentifierType() : + session.getFactory().getRuntimeMetamodels() + .getReferencedPropertyType( + concreteDescriptor.getEntityName(), + uniqueKeyPropertyName ); - // If the entity was not in the Persistence Context, but was found now, - // add it to the Persistence Context - if ( instance != null ) { - persistenceContext.addEntity( euk, instance ); - } + final EntityUniqueKey euk = new EntityUniqueKey( + concreteDescriptor.getEntityName(), + uniqueKeyPropertyName, + data.entityIdentifier, + uniqueKeyPropertyType, + session.getFactory() + ); + Object instance = persistenceContext.getEntity( euk ); + if ( instance == null ) { + // 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 = UNFETCHED_PROPERTY; + } + else { + // Try to load a PersistentAttributeInterceptable. If we get one, we can add the lazy + // field to the interceptor. If we don't get one, we load the entity by unique key. + PersistentAttributeInterceptable persistentAttributeInterceptable = null; + if ( getParent().isEntityInitializer() && isLazyByGraph( rowProcessingState ) ) { + final Object resolvedInstance = + getParent().asEntityInitializer().getResolvedInstance( rowProcessingState ); + persistentAttributeInterceptable = + ManagedTypeHelper.asPersistentAttributeInterceptableOrNull( resolvedInstance ); + } + + if ( persistentAttributeInterceptable != null ) { + final LazyAttributeLoadingInterceptor persistentAttributeInterceptor = (LazyAttributeLoadingInterceptor) persistentAttributeInterceptable.$$_hibernate_getInterceptor(); + persistentAttributeInterceptor.addLazyFieldByGraph( navigablePath.getLocalName() ); + instance = UNFETCHED_PROPERTY; + } + else { + instance = concreteDescriptor.loadByUniqueKey( + uniqueKeyPropertyName, + data.entityIdentifier, + session + ); + + // If the entity was not in the Persistence Context, but was found now, + // add it to the Persistence Context + if ( instance != null ) { + persistenceContext.addEntity( euk, instance ); } } } - if ( instance != null ) { - instance = persistenceContext.proxyFor( instance ); - } - data.setInstance( instance ); + } + if ( instance != null ) { + instance = persistenceContext.proxyFor( instance ); + } + data.setInstance( instance ); + } + else { + final EntityKey ek = entityKey == null ? + new EntityKey( data.entityIdentifier, concreteDescriptor ) : + entityKey; + final EntityHolder holder = persistenceContext.getEntityHolder( ek ); + final Object instance; + if ( holder != null && holder.getEntity() != null ) { + instance = persistenceContext.proxyFor( holder, concreteDescriptor ); + } + // 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 = UNFETCHED_PROPERTY; } else { - final EntityKey entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); - final EntityHolder holder = persistenceContext.getEntityHolder( entityKey ); - final Object instance; - if ( holder != null && holder.getEntity() != null ) { - instance = persistenceContext.proxyFor( holder, concreteDescriptor ); - } - // 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 = UNFETCHED_PROPERTY; - } - else { - instance = session.internalLoad( - concreteDescriptor.getEntityName(), - data.entityIdentifier, - false, - false - ); - - final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( instance ); - if ( lazyInitializer != null ) { - lazyInitializer.setUnwrap( referencedModelPart.isUnwrapProxy() && concreteDescriptor.isInstrumented() ); - } + instance = session.internalLoad( + concreteDescriptor.getEntityName(), + data.entityIdentifier, + false, + false + ); + + final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( instance ); + if ( lazyInitializer != null ) { + lazyInitializer.setUnwrap( referencedModelPart.isUnwrapProxy() && concreteDescriptor.isInstrumented() ); } - data.setInstance( instance ); } + data.setInstance( instance ); } } @@ -287,9 +295,28 @@ public void resolveInstance(Object instance, EntityDelayedFetchInitializerData d // This initializer is done initializing, since this is only invoked for delayed or select initializers data.setState( State.INITIALIZED ); data.setInstance( instance ); - final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var entityDescriptor = getEntityDescriptor(); + data.entityIdentifier = entityDescriptor.getIdentifier( instance, session ); + + final var entityKey = new EntityKey( data.entityIdentifier, entityDescriptor ); + final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( + entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setInstance( managed ); + } + else { + initialize( data, entityKey, entityDescriptor ); + } + } if ( keyIsEager ) { - data.entityIdentifier = getEntityDescriptor().getIdentifier( instance, rowProcessingState.getSession() ); final Initializer initializer = identifierAssembler.getInitializer(); assert initializer != null; initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index 35f17c1a6b89..4f3c57f5d964 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java @@ -923,28 +923,57 @@ public void resolveInstance(Object instance, EntityInitializerData data) { setMissing( data ); return; } - data.setInstance( instance ); final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); final RowProcessingState rowProcessingState = data.getRowProcessingState(); final SharedSessionContractImplementor session = rowProcessingState.getSession(); final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); if ( lazyInitializer == null ) { // Entity is most probably initialized - data.entityInstanceForNotify = instance; data.concreteDescriptor = session.getEntityPersister( null, instance ); resolveEntityKey( data, data.concreteDescriptor.getIdentifier( instance, session ) ); - data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); - if ( data.entityHolder == null ) { - // Entity was most probably removed in the same session without setting this association to null. - // Since this load request can happen through `find()` which doesn't auto-flush on association joins, - // the entity must be fully initialized, even if it is removed already - data.entityHolder = persistenceContext.claimEntityHolderIfPossible( - data.entityKey, - data.entityInstanceForNotify, - rowProcessingState.getJdbcValuesSourceProcessingState(), - this - ); + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + null, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); + if ( data.entityHolder.getManagedObject() == null ) { + final EntityEntry entry = persistenceContext.getEntry( instance ); // make sure an EntityEntry exists + if ( entry == null ) { + // We cannot reuse an entity instance that has no entry in the PC, + // this can happen if the parent entity contained a detached instance. + // We need to create a new instance in this case (see resolveEntityInstance1) + instance = resolveEntityInstance( data ); + } + else { + // Entity was most probably removed in the same session without setting this association to null. + // Since this load request can happen through `find()` which doesn't auto-flush on association joins, + // the entity must be fully initialized, even if it is removed already + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + instance, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); + } } + else if ( data.entityHolder.getEntity() == null ) { + assert data.entityHolder.getProxy() != instance; + instance = resolveEntityInstance( data ); + data.entityKey = data.entityHolder.getEntityKey(); + if ( data.entityHolder.getProxy() != null ) { + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( instance ); + } + } + else if ( data.entityHolder.getEntity() != instance ) { + // The instance contained in the parent entity is different from the managed persistent instance + // currently in the persistence context. We should always initialize the managed one in this case. + instance = data.entityHolder.getEntity(); + data.entityKey = data.entityHolder.getEntityKey(); + } + + data.entityInstanceForNotify = instance; + if ( data.concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() && isPersistentAttributeInterceptable( data.entityInstanceForNotify ) && getAttributeInterceptor( data.entityInstanceForNotify ) @@ -981,20 +1010,51 @@ else if ( lazyInitializer.isUninitialized() ) { rowProcessingState.getJdbcValuesSourceProcessingState(), this ); + // Resolve and potentially create the entity instance - data.entityInstanceForNotify = resolveEntityInstance( data ); - lazyInitializer.setImplementation( data.entityInstanceForNotify ); - registerLoadingEntity( data, data.entityInstanceForNotify ); + if ( data.entityHolder.getProxy() == instance ) { + data.entityInstanceForNotify = resolveEntityInstance( data ); + lazyInitializer.setImplementation( data.entityInstanceForNotify ); + } + else if ( data.entityHolder.getEntity() == null ) { + data.entityInstanceForNotify = resolveEntityInstance( data ); + if ( data.entityHolder.getProxy() != null ) { + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( data.entityInstanceForNotify ); + } + } + else { + data.entityInstanceForNotify = data.entityHolder.getEntity(); + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } } else { - data.entityInstanceForNotify = lazyInitializer.getImplementation(); - data.concreteDescriptor = session.getEntityPersister( null, data.entityInstanceForNotify ); + final var implementation = lazyInitializer.getImplementation(); + data.concreteDescriptor = session.getEntityPersister( null, implementation ); resolveEntityKey( data, lazyInitializer.getInternalIdentifier() ); data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); - // Even though the lazyInitializer reports it is initialized, check if the entity holder reports initialized, - // because in a nested initialization scenario, this nested initializer must initialize the entity - data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + if ( data.entityHolder.getProxy() == instance ) { + data.entityInstanceForNotify = implementation; + // Even though the lazyInitializer reports it is initialized, check if the entity holder reports initialized, + // because in a nested initialization scenario, this nested initializer must initialize the entity + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else if ( data.entityHolder.getEntity() == null ) { + data.entityInstanceForNotify = resolveEntityInstance( data ); + data.entityKey = data.entityHolder.getEntityKey(); + if ( data.entityHolder.getProxy() != null ) { + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( data.entityInstanceForNotify ); + } + data.setState( State.RESOLVED ); + } + else { + data.entityInstanceForNotify = data.entityHolder.getEntity(); + data.entityKey = data.entityHolder.getEntityKey(); + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } } + + data.setInstance( data.entityHolder.getManagedObject() ); + if ( identifierAssembler != null ) { final Initializer initializer = identifierAssembler.getInitializer(); if ( initializer != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java index b7fc9523439a..c0d6719e5e7e 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java @@ -159,27 +159,44 @@ public void resolveInstance(Object instance, Data data) { data.setInstance( null ); } else { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final LazyInitializer lazyInitializer = extractLazyInitializer( data.getInstance() ); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); + final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); if ( lazyInitializer == null ) { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); - } + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); - } + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); } else { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + } + + final var entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); + final var entityHolder = persistenceContext.getEntityHolder( + entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setState( entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else { + initialize( data, null, session, persistenceContext ); } } - data.setInstance( instance ); + else { + data.setInstance( instance ); + } + if ( keyIsEager ) { final Initializer initializer = keyAssembler.getInitializer(); assert initializer != null; @@ -204,10 +221,16 @@ public void initializeInstance(Data data) { protected void initialize(EntitySelectFetchInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final SharedSessionContractImplementor session = rowProcessingState.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); final EntityKey entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); + initialize( data, persistenceContext.getEntityHolder( entityKey ), session, persistenceContext ); + } - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - final EntityHolder holder = persistenceContext.getEntityHolder( entityKey ); + protected void initialize( + EntitySelectFetchInitializerData data, + @Nullable EntityHolder holder, + SharedSessionContractImplementor session, + PersistenceContext persistenceContext) { if ( holder != null ) { data.setInstance( persistenceContext.proxyFor( holder, concreteDescriptor ) ); if ( holder.getEntityInitializer() == null ) { @@ -254,8 +277,8 @@ else if ( data.getInstance() == null ) { } persistenceContext.claimEntityHolderIfPossible( new EntityKey( data.entityIdentifier, concreteDescriptor ), - instance, - rowProcessingState.getJdbcValuesSourceProcessingState(), + null, + data.getRowProcessingState().getJdbcValuesSourceProcessingState(), this ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedCollectionInitializationJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedCollectionInitializationJoinFetchTest.java new file mode 100644 index 000000000000..5fd059dbeeaa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedCollectionInitializationJoinFetchTest.java @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.collection; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OrderColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedCollectionInitializationJoinFetchTest.EntityA.class, + DetachedCollectionInitializationJoinFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedCollectionInitializationJoinFetchTest { + @Test + public void testTransientInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( new ArrayList<>( entityA.getB() ), session ); + } ); + } + + @Test + public void testUninitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + session.clear(); + + fetchQuery( entityA.b, session ); + } ); + } + + @Test + public void testInitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( entityA.getB(), session ); + } ); + } + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB1 = new EntityB(); + entityB1.id = 1L; + entityB1.name = "b_1"; + session.persist( entityB1 ); + final var entityB2 = new EntityB(); + entityB2.id = 2L; + entityB2.name = "b_2"; + session.persist( entityB2 ); + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b.add( entityB1 ); + entityA.b.add( entityB2 ); + session.persist( entityA ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + private void fetchQuery(List b, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 2L; + entityA.setB( b ); + session.persist( entityA ); + + // If persist triggers lazy initialization, the EntityB instances will be persistent + final boolean wasB1Managed = session.contains( b.get( 0 ) ); + final boolean wasB2Managed = session.contains( b.get( 1 ) ); + + final var result = session.createQuery( + "from EntityA a left join fetch a.b where a.id = 2", + EntityA.class + ).getSingleResult(); + + // We always need to initialize the collection on flush + assertThat( Hibernate.isInitialized( b ) ).isTrue(); + + final var descriptor = session.getSessionFactory() + .getMappingMetamodel() + .getCollectionDescriptor( EntityA.class.getName() + ".b" ); + final PersistentCollection collection = session.getPersistenceContextInternal() + .getCollection( new CollectionKey( descriptor, entityA.id ) ); + assertThat( Hibernate.isInitialized( collection ) ).isTrue(); + // Currently, the collection instance is re-used if we find a detached PersistentCollection. + // Not making any assertion here, as also always wrapping the value in a new instance would be acceptable + // assertThat( collection ).isNotSameAs( b ); + + // The detached instances in the collection should not be the same as the + // managed instances initialized in the persistence context. + assertThat( result.getB().get( 0 ) == session.getReference( EntityB.class, 1L ) ) + .isEqualTo( wasB1Managed ); + assertThat( result.getB().get( 1 ) == session.getReference( EntityB.class, 2L ) ) + .isEqualTo( wasB2Managed ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + private String name; + + @ManyToMany(fetch = FetchType.LAZY) + @OrderColumn + private List b = new ArrayList<>(); + + public List getB() { + return b; + } + + public void setB(List b) { + this.b = b; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedNonJoinedCollectionInitializationJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedNonJoinedCollectionInitializationJoinFetchTest.java new file mode 100644 index 000000000000..836cbf45e6ef --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedNonJoinedCollectionInitializationJoinFetchTest.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.collection; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OrderColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedNonJoinedCollectionInitializationJoinFetchTest.EntityA.class, + DetachedNonJoinedCollectionInitializationJoinFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedNonJoinedCollectionInitializationJoinFetchTest { + @Test + public void testTransientInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( new ArrayList<>( entityA.getB() ), session ); + } ); + } + + @Test + public void testUninitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + session.clear(); + + fetchQuery( entityA.b, session ); + } ); + } + + @Test + public void testInitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( entityA.getB(), session ); + } ); + } + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB1 = new EntityB(); + entityB1.id = 1L; + entityB1.name = "b_1"; + session.persist( entityB1 ); + final var entityB2 = new EntityB(); + entityB2.id = 2L; + entityB2.name = "b_2"; + session.persist( entityB2 ); + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b.add( entityB1 ); + entityA.b.add( entityB2 ); + session.persist( entityA ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + private void fetchQuery(List b, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 2L; + entityA.setB( b ); + session.persist( entityA ); + + // If persist triggers lazy initialization, the EntityB instances will be persistent + final boolean wasB1Managed = session.contains( b.get( 0 ) ); + final boolean wasB2Managed = session.contains( b.get( 1 ) ); + + final var result = session.createQuery( + "from EntityA a where a.id = 2", + EntityA.class + ).getSingleResult(); + + // We always need to initialize the collection on flush + assertThat( Hibernate.isInitialized( b ) ).isTrue(); + + final var descriptor = session.getSessionFactory() + .getMappingMetamodel() + .getCollectionDescriptor( EntityA.class.getName() + ".b" ); + final PersistentCollection collection = session.getPersistenceContextInternal() + .getCollection( new CollectionKey( descriptor, entityA.id ) ); + assertThat( Hibernate.isInitialized( collection ) ).isTrue(); + // Currently, the collection instance is re-used if we find a detached PersistentCollection. + // Not making any assertion here, as also always wrapping the value in a new instance would be acceptable + // assertThat( collection ).isNotSameAs( b ); + + // The detached instances in the collection should not be the same as the + // managed instances initialized in the persistence context. + assertThat( result.getB().get( 0 ) == session.getReference( EntityB.class, 1L ) ) + .isEqualTo( wasB1Managed ); + assertThat( result.getB().get( 1 ) == session.getReference( EntityB.class, 2L ) ) + .isEqualTo( wasB2Managed ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + private String name; + + @ManyToMany(fetch = FetchType.EAGER) + @OrderColumn + @Fetch( FetchMode.SELECT ) + private List b = new ArrayList<>(); + + public List getB() { + return b; + } + + public void setB(List b) { + this.b = b; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java new file mode 100644 index 000000000000..e593c994b239 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java @@ -0,0 +1,224 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Any; +import org.hibernate.annotations.AnyDiscriminatorValue; +import org.hibernate.annotations.AnyKeyJavaClass; +import org.hibernate.engine.spi.PrimeAmongSecondarySupertypes; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationAnyFetchTest.EntityA.class, + DetachedReferenceInitializationAnyFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationAnyFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + final var wasManagedInitialized = Hibernate.isInitialized( reference ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized || !( reference instanceof PrimeAmongSecondarySupertypes ) ); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @Any(fetch = FetchType.LAZY) + @AnyKeyJavaClass(Long.class) + @JoinColumn(name = "b_id") //the foreign key column + @Column(name = "b_type") //the discriminator column + @AnyDiscriminatorValue(discriminator = "B", entity = EntityB.class) + private Object b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationBatchFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationBatchFetchTest.java new file mode 100644 index 000000000000..79670834ca68 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationBatchFetchTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationBatchFetchTest.EntityA.class, + DetachedReferenceInitializationBatchFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationBatchFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + final var entityA2 = new EntityA(); + entityA2.id = 2L; + session.persist( entityA2 ); + + final var wasInitialized = Hibernate.isInitialized( entityB ); + + final var result = session.createQuery( + "from EntityA a order by a.id", + EntityA.class + ).getResultList().get( 0 ); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne + private EntityB b; + } + + @BatchSize( size = 10 ) + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationDelayedFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationDelayedFetchTest.java new file mode 100644 index 000000000000..e6e9fbb3ce4e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationDelayedFetchTest.java @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationDelayedFetchTest.EntityA.class, + DetachedReferenceInitializationDelayedFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationDelayedFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + final var wasManagedInitialized = Hibernate.isInitialized( reference ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + + assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized ); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java new file mode 100644 index 000000000000..26571d8cac87 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java @@ -0,0 +1,212 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationEagerFetchTest.EntityA.class, + DetachedReferenceInitializationEagerFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationEagerFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasInitialized = Hibernate.isInitialized( entityB ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerUniqueFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerUniqueFetchTest.java new file mode 100644 index 000000000000..6a4b7e7b7b88 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerUniqueFetchTest.java @@ -0,0 +1,182 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.PrimeAmongSecondarySupertypes; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationEagerUniqueFetchTest.EntityA.class, + DetachedReferenceInitializationEagerUniqueFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationEagerUniqueFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + final var wasManagedInitialized = Hibernate.isInitialized( reference ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized || !( reference instanceof PrimeAmongSecondarySupertypes ) ); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "entityB_id", referencedColumnName = "uniqueValue") + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + @Column(unique = true) + private Long uniqueValue; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationJoinFetchTest.java new file mode 100644 index 000000000000..b7f54991f575 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationJoinFetchTest.java @@ -0,0 +1,213 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationJoinFetchTest.EntityA.class, + DetachedReferenceInitializationJoinFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationJoinFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasInitialized = Hibernate.isInitialized( entityB ); + + final var result = session.createQuery( + "from EntityA a left join fetch a.b", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +}