From b89df051883312890a744100951578cd4d76fdb8 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 20 Nov 2025 22:35:43 +0100 Subject: [PATCH] HHH-19273 Don't trust state of read-only associations for initializtion --- ...ractBatchEntitySelectFetchInitializer.java | 128 +++-- .../DiscriminatedEntityInitializer.java | 63 ++- .../EntityDelayedFetchInitializer.java | 125 +++-- .../internal/EntityInitializerImpl.java | 91 +++- ...titySelectFetchByUniqueKeyInitializer.java | 88 ++++ .../EntitySelectFetchInitializer.java | 46 +- ...edReferenceInitializationAnyFetchTest.java | 293 ++++++++++- ...ReferenceInitializationBatchFetchTest.java | 298 ++++++++++- ...ferenceInitializationDelayedFetchTest.java | 296 ++++++++++- ...erenceInitializationEagerAnyFetchTest.java | 472 ++++++++++++++++++ ...ReferenceInitializationEagerFetchTest.java | 290 ++++++++++- ...nceInitializationEagerUniqueFetchTest.java | 203 +++++++- ...dReferenceInitializationJoinFetchTest.java | 298 ++++++++++- ...eloadInconsistentReadOnlyAnyFetchTest.java | 118 +++++ ...oadInconsistentReadOnlyBatchFetchTest.java | 124 +++++ ...dInconsistentReadOnlyDelayedFetchTest.java | 116 +++++ ...InconsistentReadOnlyEagerAnyFetchTest.java | 120 +++++ ...oadInconsistentReadOnlyEagerToOneTest.java | 169 +++++++ ...onsistentReadOnlyEagerUniqueFetchTest.java | 119 +++++ 19 files changed, 3189 insertions(+), 268 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerAnyFetchTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyAnyFetchTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyBatchFetchTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyDelayedFetchTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerAnyFetchTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerToOneTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerUniqueFetchTest.java 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 5e4e47cdd9f6..0acc15d76471 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 @@ -131,100 +131,88 @@ protected void resolveInstanceFromIdentifier(Data data) { @Override public void resolveInstance(Object instance, Data data) { - if ( instance == null ) { + final boolean identifierResolved = resolveIdentifier( instance, data ); + if ( data.entityIdentifier == null ) { data.setState( State.MISSING ); data.entityKey = null; data.setInstance( null ); - return; } - 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.entityIdentifier = null; - if ( lazyInitializer == null ) { - // Entity is most probably initialized - data.setInstance( instance ); - final PersistentAttributeInterceptor interceptor; - if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() - && isPersistentAttributeInterceptable( instance ) - && ( interceptor = getAttributeInterceptor( instance ) ) instanceof EnhancementAsProxyLazinessInterceptor ) { - final EnhancementAsProxyLazinessInterceptor enhancementInterceptor = (EnhancementAsProxyLazinessInterceptor) interceptor; - if ( enhancementInterceptor.isInitialized() ) { - data.setState( State.INITIALIZED ); + else { + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); + final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); + if ( lazyInitializer == null ) { + // Entity is most probably initialized + final PersistentAttributeInterceptor interceptor; + if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && isPersistentAttributeInterceptable( instance ) + && ( interceptor = getAttributeInterceptor( instance ) ) instanceof EnhancementAsProxyLazinessInterceptor ) { + final EnhancementAsProxyLazinessInterceptor enhancementInterceptor = (EnhancementAsProxyLazinessInterceptor) interceptor; + if ( enhancementInterceptor.isInitialized() ) { + data.setState( State.INITIALIZED ); + } + else { + data.setState( State.RESOLVED ); + } } else { + // 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 = enhancementInterceptor.getIdentifier(); } } - else { - // If the entity initializer is null, we know the entity is fully initialized, - // otherwise it will be initialized by some other initializer + else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); } - if ( data.entityIdentifier == null ) { - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); + else { + // Entity is initialized + data.setState( State.INITIALIZED ); } - } - else if ( lazyInitializer.isUninitialized() ) { - data.setState( State.RESOLVED ); - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); - } - else { - // Entity is initialized - data.setState( State.INITIALIZED ); - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); - data.setInstance( lazyInitializer.getImplementation() ); - } - data.entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); - final var entityHolder = persistenceContext.getEntityHolder( - data.entityKey - ); + 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 ); + if ( entityHolder == null || instance == 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(); + data.setState( entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); } else { data.setState( State.RESOLVED ); } } else { - data.setState( State.RESOLVED ); + data.setInstance( instance ); } - } - if ( data.getState() == State.RESOLVED ) { - // 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 ( data.getState() == State.RESOLVED ) { + // 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; - initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); - } - else if ( rowProcessingState.needsResolveState() ) { - // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit - keyAssembler.resolveState( rowProcessingState ); + if ( keyIsEager && !identifierResolved ) { + final Initializer initializer = keyAssembler.getInitializer(); + assert initializer != null; + initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); + } + else if ( rowProcessingState.needsResolveState() && !identifierResolved ) { + // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit + keyAssembler.resolveState( rowProcessingState ); + } } } 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 2baef932bd04..0f4c9f54b8a8 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 @@ -15,7 +15,9 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.log.LoggingHelper; import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.DiscriminatedAssociationModelPart; +import org.hibernate.metamodel.mapping.DiscriminatorMapping; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; @@ -53,6 +55,7 @@ public class DiscriminatedEntityInitializer private final boolean resultInitializer; private final boolean keyIsEager; private final boolean hasLazySubInitializer; + protected final boolean isReadOnly; public static class DiscriminatedEntityInitializerData extends InitializerData { protected EntityPersister concreteDescriptor; @@ -90,6 +93,16 @@ public DiscriminatedEntityInitializer( this.keyIsEager = initializer.isEager(); this.hasLazySubInitializer = !initializer.isEager() || initializer.hasLazySubInitializers(); } + this.isReadOnly = isReadOnly( fetchedPart ); + } + + private static boolean isReadOnly(DiscriminatedAssociationModelPart fetchedPart) { + final BasicValuedModelPart keyPart = fetchedPart.getKeyPart(); + final DiscriminatorMapping discriminatorMapping = fetchedPart.getDiscriminatorMapping(); + return !keyPart.isInsertable() + && !keyPart.isUpdateable() + && !discriminatorMapping.isInsertable() + && !discriminatorMapping.isUpdateable(); } @Override @@ -201,13 +214,16 @@ else if ( instance == null ) { ) ); } - @Override - public void resolveInstance(Object instance, DiscriminatedEntityInitializerData data) { - if ( instance == null ) { - data.setState( State.MISSING ); + protected boolean resolveIdentifier(Object instance, DiscriminatedEntityInitializerData data) { + final boolean identifierResolved; + if ( instance == null && !isReadOnly ) { data.entityIdentifier = null; - data.concreteDescriptor = null; - data.setInstance( null ); + identifierResolved = true; + } + else if ( isReadOnly ) { + // When the mapping is read-only, we can't trust the state of the persistence context + resolveKey( data ); + identifierResolved = true; } else { final var rowProcessingState = data.getRowProcessingState(); @@ -217,6 +233,7 @@ public void resolveInstance(Object instance, DiscriminatedEntityInitializerData data.setState( State.INITIALIZED ); data.concreteDescriptor = session.getEntityPersister( null, instance ); data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); + identifierResolved = false; } else if ( lazyInitializer.isUninitialized() ) { data.setState( eager ? State.RESOLVED : State.INITIALIZED ); @@ -224,20 +241,35 @@ else if ( lazyInitializer.isUninitialized() ) { final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + identifierResolved = true; } else { data.setState( State.INITIALIZED ); data.concreteDescriptor = session.getEntityPersister( null, lazyInitializer.getImplementation() ); data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + identifierResolved = false; } + assert data.entityIdentifier != null; + } + return identifierResolved; + } - + @Override + public void resolveInstance(Object instance, DiscriminatedEntityInitializerData data) { + final boolean identifierResolved = resolveIdentifier( instance, data ); + if ( data.entityIdentifier == null ) { + data.setState( State.MISSING ); + data.concreteDescriptor = null; + data.setInstance( null ); + } + else { + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); final var entityKey = new EntityKey( data.entityIdentifier, data.concreteDescriptor ); - final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( - entityKey - ); + final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( entityKey ); - if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + if ( entityHolder == null || instance == null + || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { // the existing entity instance is detached or transient if ( entityHolder != null ) { final var managed = entityHolder.getManagedObject(); @@ -254,15 +286,18 @@ else if ( lazyInitializer.isUninitialized() ) { data.setInstance( instance ); } - if ( keyIsEager ) { + if ( keyIsEager && !identifierResolved ) { final Initializer initializer = keyValueAssembler.getInitializer(); assert initializer != null; initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); + if ( rowProcessingState.needsResolveState() ) { + discriminatorValueAssembler.resolveState( rowProcessingState ); + } } - else if ( rowProcessingState.needsResolveState() ) { + else if ( rowProcessingState.needsResolveState() && !identifierResolved ) { // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit - discriminatorValueAssembler.resolveState( rowProcessingState ); keyValueAssembler.resolveState( rowProcessingState ); + discriminatorValueAssembler.resolveState( rowProcessingState ); } } } 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 e5c1dbd28184..6d7e46c3c6fe 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 @@ -43,6 +43,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.determineConcreteEntityDescriptor; /** @@ -62,6 +63,7 @@ public class EntityDelayedFetchInitializer private final @Nullable BasicResultAssembler discriminatorAssembler; private final boolean keyIsEager; private final boolean hasLazySubInitializer; + private final boolean isReadOnly; public static class EntityDelayedFetchInitializerData extends InitializerData { // per-row state @@ -102,6 +104,7 @@ public EntityDelayedFetchInitializer( this.keyIsEager = initializer.isEager(); this.hasLazySubInitializer = !initializer.isEager() || initializer.hasLazySubInitializers(); } + this.isReadOnly = EntityInitializerImpl.isReadOnly( referencedModelPart ); } @Override @@ -137,49 +140,59 @@ public void resolveFromPreviousRow(EntityDelayedFetchInitializerData data) { @Override public void resolveInstance(EntityDelayedFetchInitializerData data) { - if ( data.getState() != State.KEY_RESOLVED ) { + if ( data.getState() != State.KEY_RESOLVED && data.getState() != State.RESOLVED ) { return; } + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + if ( data.getState() == State.KEY_RESOLVED ) { + data.entityIdentifier = identifierAssembler.assemble(rowProcessingState); + } + // This initializer is done initializing, since this is only invoked for delayed or select initializers data.setState( State.INITIALIZED ); - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - data.entityIdentifier = identifierAssembler.assemble( rowProcessingState ); - if ( data.entityIdentifier == null ) { data.setInstance( null ); data.setState( State.MISSING ); } else { - final SharedSessionContractImplementor session = rowProcessingState.getSession(); - - final EntityPersister entityPersister = getEntityDescriptor(); - final EntityPersister concreteDescriptor; - if ( discriminatorAssembler != null ) { - concreteDescriptor = determineConcreteEntityDescriptor( - rowProcessingState, - discriminatorAssembler, - entityPersister - ); - if ( concreteDescriptor == null ) { - // If we find no discriminator it means there's no entity in the target table - if ( !referencedModelPart.isOptional() ) { - throw new FetchNotFoundException( entityPersister.getEntityName(), data.entityIdentifier ); - } - data.setInstance( null ); - data.setState( State.MISSING ); - return; + final EntityPersister concreteDescriptor = resolveConcreteEntityDescriptor( data ); + if ( concreteDescriptor == null ) { + // If we find no discriminator it means there's no entity in the target table + if ( !referencedModelPart.isOptional() ) { + throw new FetchNotFoundException( getEntityDescriptor().getEntityName(), data.entityIdentifier ); } - } - else { - concreteDescriptor = entityPersister; + data.setInstance( null ); + data.setState( State.MISSING ); + return; } initialize( data, null, concreteDescriptor ); } } + private @Nullable EntityPersister resolveConcreteEntityDescriptor(EntityDelayedFetchInitializerData data) { + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final EntityPersister entityPersister = getEntityDescriptor(); + final EntityPersister concreteDescriptor; + if ( discriminatorAssembler != null ) { + concreteDescriptor = determineConcreteEntityDescriptor( + rowProcessingState, + discriminatorAssembler, + entityPersister + ); + if ( concreteDescriptor == null ) { + // If we find no discriminator it means there's no entity in the target table + return null; + } + } + else { + concreteDescriptor = entityPersister; + } + return concreteDescriptor; + } + protected void initialize(EntityDelayedFetchInitializerData data, @Nullable EntityKey entityKey, EntityPersister concreteDescriptor) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final SharedSessionContractImplementor session = rowProcessingState.getSession(); @@ -266,7 +279,7 @@ else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) { false ); - final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( instance ); + final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); if ( lazyInitializer != null ) { lazyInitializer.setUnwrap( referencedModelPart.isUnwrapProxy() && concreteDescriptor.isInstrumented() ); } @@ -290,9 +303,28 @@ private boolean isLazyByGraph(RowProcessingState rowProcessingState) { @Override public void resolveInstance(Object instance, EntityDelayedFetchInitializerData data) { - if ( instance == null ) { - data.setState( State.MISSING ); + boolean identifierResolved; + if ( instance == null && !isReadOnly ) { data.entityIdentifier = null; + identifierResolved = true; + } + else if ( isReadOnly ) { + // When the mapping is read-only, we can't trust the state of the persistence context + resolveKey( data ); + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + data.entityIdentifier = identifierAssembler.assemble( rowProcessingState ); + identifierResolved = true; + } + else { + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var entityDescriptor = getEntityDescriptor(); + data.entityIdentifier = entityDescriptor.getIdentifier( instance, session ); + assert data.entityIdentifier != null; + identifierResolved = false; + } + if ( data.entityIdentifier == null ) { + data.setState( State.MISSING ); data.setInstance( null ); } else { @@ -301,15 +333,28 @@ public void resolveInstance(Object instance, EntityDelayedFetchInitializerData d data.setInstance( instance ); final var rowProcessingState = data.getRowProcessingState(); final var session = rowProcessingState.getSession(); - final var entityDescriptor = getEntityDescriptor(); - data.entityIdentifier = entityDescriptor.getIdentifier( instance, session ); + final EntityPersister entityDescriptor; + if ( isReadOnly ) { + entityDescriptor = resolveConcreteEntityDescriptor( data ); + } + else { + final var lazyInitializer = extractLazyInitializer( instance ); + if ( lazyInitializer == null ) { + entityDescriptor = session.getEntityPersister( null, instance ); + } + else if ( lazyInitializer.isUninitialized() ) { + entityDescriptor = resolveConcreteEntityDescriptor( data ); + } + else { + entityDescriptor = session.getEntityPersister( null, lazyInitializer.getImplementation() ); + } + } final var entityKey = new EntityKey( data.entityIdentifier, entityDescriptor ); - final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( - entityKey - ); + final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( entityKey ); - if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + if ( entityHolder == null || instance == null + || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { // the existing entity instance is detached or transient if ( entityHolder != null ) { final var managed = entityHolder.getManagedObject(); @@ -320,14 +365,20 @@ public void resolveInstance(Object instance, EntityDelayedFetchInitializerData d initialize( data, entityKey, entityDescriptor ); } } - if ( keyIsEager ) { + if ( keyIsEager && !identifierResolved ) { final Initializer initializer = identifierAssembler.getInitializer(); assert initializer != null; initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); + identifierResolved = true; } - else if ( rowProcessingState.needsResolveState() ) { + if ( rowProcessingState.needsResolveState() ) { // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit - identifierAssembler.resolveState( rowProcessingState ); + if ( !identifierResolved ) { + identifierAssembler.resolveState( rowProcessingState ); + } + if ( discriminatorAssembler != null ) { + discriminatorAssembler.resolveState( 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 7188f84f814a..2e4fbe53850e 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 @@ -51,7 +51,10 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.EntityVersionMapping; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.UniqueKeyEntry; @@ -116,6 +119,7 @@ public class EntityInitializerImpl extends AbstractInitializer initializer = keyAssembler.getInitializer(); + assert initializer != null; + initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); + } + else if ( rowProcessingState.needsResolveState() && !identifierResolved ) { + // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit + keyAssembler.resolveState( rowProcessingState ); + } + } + } + @Override protected void initialize(EntitySelectFetchInitializerData data) { final String entityName = concreteDescriptor.getEntityName(); 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 d9bbd27f8276..6f4337814aae 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 @@ -54,6 +54,7 @@ public class EntitySelectFetchInitializer initializer = keyAssembler.getInitializer(); assert initializer != null; initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); } - else if ( rowProcessingState.needsResolveState() ) { + else if ( rowProcessingState.needsResolveState() && !identifierResolved ) { // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit keyAssembler.resolveState( rowProcessingState ); } 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 index 09fd8d620957..b8b16e3b5d23 100644 --- 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 @@ -45,7 +45,33 @@ public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -59,7 +85,35 @@ public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -72,7 +126,33 @@ public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -85,7 +165,33 @@ public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -99,7 +205,35 @@ public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope s final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -112,7 +246,33 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -126,7 +286,35 @@ public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -141,7 +329,37 @@ public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -155,7 +373,35 @@ public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope s // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -176,17 +422,16 @@ public void tearDown(SessionFactoryScope scope) { } ); } - private void fetchQuery(EntityB entityB, SessionImplementor session) { + private void fetchQuery(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { final var entityA = new EntityA(); entityA.id = 1L; + entityA.bId = bId; + entityA.bType = bId == null ? null : "B"; 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 wasManagedInitialized = Hibernate.isInitialized( managedB ); final var result = session.createQuery( "from EntityA a", @@ -196,20 +441,28 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { 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 ); + if ( bId == null ) { + assertThat( Hibernate.isInitialized( managedB ) ).isSameAs( wasManagedInitialized ); + } + else { + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( managedB ) ).isEqualTo( wasManagedInitialized || !( managedB instanceof PrimeAmongSecondarySupertypes ) ); + } + assertThat( managedB ).isNotSameAs( entityB ); } @Entity(name = "EntityA") static class EntityA { @Id private Long id; - + @Column(name = "b_id") + private Long bId; + @Column(name = "b_type") + private String bType; @Any(fetch = FetchType.LAZY) @AnyKeyJavaClass(Long.class) - @JoinColumn(name = "b_id") //the foreign key column - @Column(name = "b_type") //the discriminator column + @JoinColumn(name = "b_id", insertable = false, updatable = false) //the foreign key column + @Column(name = "b_type", insertable = false, updatable = false) //the discriminator column @AnyDiscriminatorValue(discriminator = "B", entity = EntityB.class) private Object b; } 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 index faa21feafa00..d03fa8ec1c3f 100644 --- 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 @@ -4,6 +4,12 @@ */ package org.hibernate.orm.test.bytecode.enhancement.detached.reference; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + import org.hibernate.Hibernate; import org.hibernate.annotations.BatchSize; import org.hibernate.engine.spi.SessionImplementor; @@ -17,10 +23,6 @@ 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 = { @@ -40,7 +42,33 @@ public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -54,7 +82,35 @@ public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -67,7 +123,33 @@ public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -80,7 +162,33 @@ public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -94,7 +202,35 @@ public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope s final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -107,8 +243,33 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } - fetchQuery( entityB, session ); + @Test + public void testDetachedAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -116,13 +277,41 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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 ); + fetchQuery( 1L, null, ignored, session ); } ); } @@ -130,14 +319,44 @@ public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + 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 ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -145,13 +364,41 @@ public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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 ); + fetchQuery( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -172,9 +419,10 @@ public void tearDown(SessionFactoryScope scope) { } ); } - private void fetchQuery(EntityB entityB, SessionImplementor session) { + private void fetchQuery(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { final var entityA = new EntityA(); entityA.id = 1L; + entityA.bId = bId; entityA.b = entityB; session.persist( entityA ); final var entityA2 = new EntityA(); @@ -182,6 +430,7 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { session.persist( entityA2 ); final var wasInitialized = Hibernate.isInitialized( entityB ); + final var managedWasInitialized = Hibernate.isInitialized( managedB ); final var result = session.createQuery( "from EntityA a order by a.id", @@ -191,9 +440,14 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); assertThat( result.b ).isSameAs( entityB ); - final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( managedB ); final var reference = session.getReference( EntityB.class, id ); - assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + if ( bId == null ) { + assertThat( Hibernate.isInitialized( reference ) ).isSameAs( managedWasInitialized ); + } + else { + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } assertThat( reference ).isNotSameAs( entityB ); } @@ -201,12 +455,14 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { static class EntityA { @Id private Long id; - + @Column(name = "b_id") + private Long bId; @ManyToOne + @JoinColumn(name = "b_id", insertable = false, updatable = false) private EntityB b; } - @BatchSize( size = 10 ) + @BatchSize(size = 10) @Entity(name = "EntityB") static class EntityB { @Id 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 index 7f938114dc18..1615b3cf3e54 100644 --- 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 @@ -4,6 +4,13 @@ */ package org.hibernate.orm.test.bytecode.enhancement.detached.reference; +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 org.hibernate.Hibernate; import org.hibernate.engine.spi.SessionImplementor; @@ -16,11 +23,6 @@ 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 = { @@ -40,7 +42,33 @@ public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -54,7 +82,35 @@ public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -67,7 +123,33 @@ public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -80,7 +162,33 @@ public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -94,7 +202,35 @@ public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope s final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -108,7 +244,33 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -116,13 +278,41 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + 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 ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -130,14 +320,44 @@ public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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 ); + fetchQuery( 1L, null, ignored, session ); } ); } @@ -145,13 +365,40 @@ public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + 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 ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -172,17 +419,15 @@ public void tearDown(SessionFactoryScope scope) { } ); } - private void fetchQuery(EntityB entityB, SessionImplementor session) { + private void fetchQuery(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { final var entityA = new EntityA(); entityA.id = 1L; + entityA.bId = bId; 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 wasManagedInitialized = Hibernate.isInitialized( managedB ); final var result = session.createQuery( "from EntityA a", @@ -192,7 +437,8 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); assertThat( result.b ).isSameAs( entityB ); - + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( managedB ); + final var reference = session.getReference( EntityB.class, id ); assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized ); assertThat( reference ).isNotSameAs( entityB ); } @@ -201,8 +447,10 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { static class EntityA { @Id private Long id; - + @Column(name = "b_id") + private Long bId; @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "b_id", insertable = false, updatable = false) private EntityB b; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerAnyFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerAnyFetchTest.java new file mode 100644 index 000000000000..887054d75c2c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerAnyFetchTest.java @@ -0,0 +1,472 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import jakarta.persistence.*; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Any; +import org.hibernate.annotations.AnyDiscriminatorValue; +import org.hibernate.annotations.AnyKeyJavaClass; +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 static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationEagerAnyFetchTest.EntityA.class, + DetachedReferenceInitializationEagerAnyFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationEagerAnyFetchTest { + @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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, 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( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, 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(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.bId = bId; + entityA.bType = bId == null ? null : "B"; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + final var wasManagedInitialized = Hibernate.isInitialized( managedB ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + if ( bId == null ) { + assertThat( Hibernate.isInitialized( managedB ) ).isSameAs( wasManagedInitialized ); + } + else { + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( managedB ) ).isTrue(); + } + assertThat( managedB ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + @Column(name = "b_id") + private Long bId; + @Column(name = "b_type") + private String bType; + @Any(fetch = FetchType.EAGER) + @AnyKeyJavaClass(Long.class) + @JoinColumn(name = "b_id", insertable = false, updatable = false) //the foreign key column + @Column(name = "b_type", insertable = false, updatable = false) //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/DetachedReferenceInitializationEagerFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java index a7e88e517203..9e2617188b2e 100644 --- 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 @@ -4,6 +4,12 @@ */ package org.hibernate.orm.test.bytecode.enhancement.detached.reference; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + import org.hibernate.Hibernate; import org.hibernate.engine.spi.SessionImplementor; @@ -16,10 +22,6 @@ 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 = { @@ -39,7 +41,33 @@ public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -53,7 +81,35 @@ public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -66,7 +122,33 @@ public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -79,7 +161,33 @@ public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -93,7 +201,35 @@ public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope s final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -106,8 +242,33 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNullAssociation(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 ); + fetchQuery( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -121,7 +282,35 @@ public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -136,7 +325,37 @@ public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -150,7 +369,35 @@ public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope s // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -171,13 +418,15 @@ public void tearDown(SessionFactoryScope scope) { } ); } - private void fetchQuery(EntityB entityB, SessionImplementor session) { + private void fetchQuery(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { final var entityA = new EntityA(); entityA.id = 1L; + entityA.bId = bId; entityA.b = entityB; session.persist( entityA ); final var wasInitialized = Hibernate.isInitialized( entityB ); + final var managedWasInitialized = Hibernate.isInitialized( managedB ); final var result = session.createQuery( "from EntityA a", @@ -187,9 +436,14 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); assertThat( result.b ).isSameAs( entityB ); - final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( managedB ); final var reference = session.getReference( EntityB.class, id ); - assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + if ( bId == null ) { + assertThat( Hibernate.isInitialized( reference ) ).isSameAs( managedWasInitialized ); + } + else { + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } assertThat( reference ).isNotSameAs( entityB ); } @@ -197,8 +451,10 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { static class EntityA { @Id private Long id; - + @Column(name = "b_id") + private Long bId; @ManyToOne + @JoinColumn(name = "b_id", insertable = false, updatable = false) private EntityB b; } 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 index 63c9370111dc..b87cfb823aa3 100644 --- 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 @@ -43,7 +43,33 @@ public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -57,7 +83,35 @@ public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -70,7 +124,33 @@ public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -84,7 +164,35 @@ public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -99,7 +207,37 @@ public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -113,7 +251,35 @@ public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope s // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -122,6 +288,7 @@ public void setUp(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = new EntityB(); entityB.id = 1L; + entityB.uniqueValue = 1L; entityB.name = "b_1"; session.persist( entityB ); } ); @@ -134,17 +301,15 @@ public void tearDown(SessionFactoryScope scope) { } ); } - private void fetchQuery(EntityB entityB, SessionImplementor session) { + private void fetchQuery(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { final var entityA = new EntityA(); entityA.id = 1L; + entityA.bId = bId; 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 wasManagedInitialized = Hibernate.isInitialized( managedB ); final var result = session.createQuery( "from EntityA a", @@ -154,18 +319,24 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { 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 ); + if ( bId == null ) { + assertThat( Hibernate.isInitialized( managedB ) ).isSameAs( wasManagedInitialized ); + } + else { + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( managedB ) ).isEqualTo( wasManagedInitialized || !( managedB instanceof PrimeAmongSecondarySupertypes ) ); + } + assertThat( managedB ).isNotSameAs( entityB ); } @Entity(name = "EntityA") static class EntityA { @Id private Long id; - + @Column(name = "entityB_id") + private Long bId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "entityB_id", referencedColumnName = "uniqueValue") + @JoinColumn(name = "entityB_id", referencedColumnName = "uniqueValue", insertable = false, updatable = false) private EntityB b; } 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 index 45a6140c56a7..3697ea072642 100644 --- 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 @@ -4,6 +4,13 @@ */ package org.hibernate.orm.test.bytecode.enhancement.detached.reference; +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 org.hibernate.Hibernate; import org.hibernate.engine.spi.SessionImplementor; @@ -16,11 +23,6 @@ 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 = { @@ -40,7 +42,33 @@ public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -54,7 +82,35 @@ public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -67,7 +123,33 @@ public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -80,7 +162,33 @@ public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.find( EntityB.class, 1L ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -94,7 +202,35 @@ public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope s final var ignored = session.getReference( EntityB.class, 1L ); Hibernate.initialize( ignored ); - fetchQuery( entityB, session ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -107,8 +243,33 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { // put a different instance of EntityB in the persistence context final var ignored = session.getReference( EntityB.class, 1L ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxyInconsistentNonNullAssociation(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 ); + fetchQuery( 1L, null, ignored, session ); } ); } @@ -116,13 +277,41 @@ public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + 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 ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntityInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -130,14 +319,44 @@ public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + 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 ); + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNullAssociation(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( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -145,13 +364,41 @@ public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { scope.inTransaction( session -> { final var entityB = session.getReference( EntityB.class, 1L ); - Hibernate.initialize( entityB ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( 1L, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNullAssociation(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 ); + fetchQuery( null, entityB, ignored, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxyInconsistentNonNullAssociation(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( 1L, null, ignored, session ); } ); } @@ -172,13 +419,15 @@ public void tearDown(SessionFactoryScope scope) { } ); } - private void fetchQuery(EntityB entityB, SessionImplementor session) { + private void fetchQuery(Long bId, EntityB entityB, EntityB managedB, SessionImplementor session) { final var entityA = new EntityA(); entityA.id = 1L; + entityA.bId = bId; entityA.b = entityB; session.persist( entityA ); final var wasInitialized = Hibernate.isInitialized( entityB ); + final var managedWasInitialized = Hibernate.isInitialized( managedB ); final var result = session.createQuery( "from EntityA a left join fetch a.b", @@ -188,9 +437,14 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); assertThat( result.b ).isSameAs( entityB ); - final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( managedB ); final var reference = session.getReference( EntityB.class, id ); - assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + if ( bId == null ) { + assertThat( Hibernate.isInitialized( reference ) ).isSameAs( managedWasInitialized ); + } + else { + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } assertThat( reference ).isNotSameAs( entityB ); } @@ -198,8 +452,10 @@ private void fetchQuery(EntityB entityB, SessionImplementor session) { static class EntityA { @Id private Long id; - + @Column(name = "b_id") + private Long bId; @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "b_id", insertable = false, updatable = false) private EntityB b; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyAnyFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyAnyFetchTest.java new file mode 100644 index 000000000000..8ed2f23ee828 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyAnyFetchTest.java @@ -0,0 +1,118 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import org.hibernate.annotations.Any; +import org.hibernate.annotations.AnyDiscriminatorValue; +import org.hibernate.annotations.AnyKeyJavaClass; + +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.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 = { + ReloadInconsistentReadOnlyAnyFetchTest.EntityA.class, + ReloadInconsistentReadOnlyAnyFetchTest.EntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19273") +public class ReloadInconsistentReadOnlyAnyFetchTest { + + @Test + public void testSelectFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + newAEntity.bType = "B"; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @Test + public void testSelectFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + public static class EntityA { + @Id + private Long id; + @Column(name = "b_id") + private Long bId; + @Column(name = "b_type") + private String bType; + @Any(fetch = FetchType.LAZY) + @AnyKeyJavaClass(Long.class) + @JoinColumn(name = "b_id", insertable = false, updatable = false) //the foreign key column + @Column(name = "b_type", insertable = false, updatable = false) //the discriminator column + @AnyDiscriminatorValue(discriminator = "B", entity = EntityB.class) + private Object b; + } + + @Entity(name = "EntityB") + public static class EntityB { + @Id + private Long id; + private String data; + + public EntityB() { + } + + public EntityB(Long id, String data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyBatchFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyBatchFetchTest.java new file mode 100644 index 000000000000..7948f06c8d3e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyBatchFetchTest.java @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import java.util.Set; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; + +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.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ReloadInconsistentReadOnlyBatchFetchTest.EntityA.class, + ReloadInconsistentReadOnlyBatchFetchTest.EntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19273") +public class ReloadInconsistentReadOnlyBatchFetchTest { + + @Test + public void testSelectFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + session.persist( newAEntity ); + final var entityA2 = new EntityA(); + entityA2.id = 2L; + session.persist( entityA2 ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNotNull(); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } ); + } + + @Test + public void testSelectFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + final var entityA2 = new EntityA(); + entityA2.id = 2L; + session.persist( entityA2 ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + public static class EntityA { + @Id + private Long id; + @Column(name = "b_id") + private Long bId; + @ManyToOne + @JoinColumn(name = "b_id", insertable = false, updatable = false) + private EntityB b; + } + + @BatchSize(size = 10) + @Entity(name = "EntityB") + public static class EntityB { + @Id + private Long id; + private String data; + @OneToMany(mappedBy = "b") + private Set as; + + public EntityB() { + } + + public EntityB(Long id, String data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyDelayedFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyDelayedFetchTest.java new file mode 100644 index 000000000000..634ffc00acf0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyDelayedFetchTest.java @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import java.util.Set; + +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.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 jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ReloadInconsistentReadOnlyDelayedFetchTest.EntityA.class, + ReloadInconsistentReadOnlyDelayedFetchTest.EntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19273") +public class ReloadInconsistentReadOnlyDelayedFetchTest { + + @Test + public void testSelectFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @Test + public void testSelectFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + + // EntityInitializerImpl errors when seeing `null` for A#b in the + // entity of the persistence context, but an FK is fetched + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + public static class EntityA { + @Id + private Long id; + @Column(name = "b_id") + private Long bId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "b_id", insertable = false, updatable = false) + private EntityB b; + } + + @Entity(name = "EntityB") + public static class EntityB { + @Id + private Long id; + private String data; + @OneToMany(mappedBy = "b") + private Set as; + + public EntityB() { + } + + public EntityB(Long id, String data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerAnyFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerAnyFetchTest.java new file mode 100644 index 000000000000..32e9984aaf9a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerAnyFetchTest.java @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Any; +import org.hibernate.annotations.AnyDiscriminatorValue; +import org.hibernate.annotations.AnyKeyJavaClass; + +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.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 = { + ReloadInconsistentReadOnlyEagerAnyFetchTest.EntityA.class, + ReloadInconsistentReadOnlyEagerAnyFetchTest.EntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19273") +public class ReloadInconsistentReadOnlyEagerAnyFetchTest { + + @Test + public void testSelectFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + newAEntity.bType = "B"; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNotNull(); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } ); + } + + @Test + public void testSelectFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + public static class EntityA { + @Id + private Long id; + @Column(name = "b_id") + private Long bId; + @Column(name = "b_type") + private String bType; + @Any(fetch = FetchType.EAGER) + @AnyKeyJavaClass(Long.class) + @JoinColumn(name = "b_id", insertable = false, updatable = false) //the foreign key column + @Column(name = "b_type", insertable = false, updatable = false) //the discriminator column + @AnyDiscriminatorValue(discriminator = "B", entity = EntityB.class) + private Object b; + } + + @Entity(name = "EntityB") + public static class EntityB { + @Id + private Long id; + private String data; + + public EntityB() { + } + + public EntityB(Long id, String data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerToOneTest.java new file mode 100644 index 000000000000..a176c47c4917 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerToOneTest.java @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import java.util.Set; + +import org.hibernate.Hibernate; + +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.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ReloadInconsistentReadOnlyEagerToOneTest.EntityA.class, + ReloadInconsistentReadOnlyEagerToOneTest.EntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19273") +public class ReloadInconsistentReadOnlyEagerToOneTest { + + @Test + public void testJoinFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + session.persist( newAEntity ); + + // EntityInitializerImpl errors when seeing `null` for A#b in the + // entity of the persistence context, but an FK is fetched + session.createQuery( "from EntityA a join fetch a.b", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNotNull(); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } ); + } + + @Test + public void testJoinFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + + // EntityInitializerImpl errors when seeing `null` for A#b in the + // entity of the persistence context, but an FK is fetched + session.createQuery( "from EntityA a join fetch a.b", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @Test + public void testSelectFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNotNull(); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } ); + } + + @Test + public void testSelectFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + public static class EntityA { + @Id + private Long id; + @Column(name = "b_id") + private Long bId; + @ManyToOne + @JoinColumn(name = "b_id", insertable = false, updatable = false) + private EntityB b; + } + + @Entity(name = "EntityB") + public static class EntityB { + @Id + private Long id; + private String data; + @OneToMany(mappedBy = "b") + private Set as; + + public EntityB() { + } + + public EntityB(Long id, String data) { + this.id = id; + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerUniqueFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerUniqueFetchTest.java new file mode 100644 index 000000000000..72473f11b600 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ReloadInconsistentReadOnlyEagerUniqueFetchTest.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import java.util.Set; + +import org.hibernate.Hibernate; + +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.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ReloadInconsistentReadOnlyEagerUniqueFetchTest.EntityA.class, + ReloadInconsistentReadOnlyEagerUniqueFetchTest.EntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19273") +public class ReloadInconsistentReadOnlyEagerUniqueFetchTest { + + @Test + public void testSelectFetchInconsistentNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.bId = 1L; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + assertThat( newAEntity.b ).isNull(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNotNull(); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + } ); + } + + @Test + public void testSelectFetchInconsistentNonNullAssociation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var newB = new EntityB( 1L, "abc" ); + session.persist( newB ); + session.flush(); + session.clear(); + + // When + var newAEntity = new EntityA(); + newAEntity.id = 1L; + newAEntity.b = newB; + session.persist( newAEntity ); + + session.createQuery( "from EntityA a", EntityA.class ).getResultList(); + + final var persister = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + final var entityKey = session.generateEntityKey( 1L, persister ); + final var reference = session.getPersistenceContext().getEntity( entityKey ); + assertThat( reference ).isNull(); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + public static class EntityA { + @Id + private Long id; + @Column(name = "entityB_id") + private Long bId; + @ManyToOne + @JoinColumn(name = "entityB_id", referencedColumnName = "uniqueValue", insertable = false, updatable = false) + private EntityB b; + } + + @Entity(name = "EntityB") + public static class EntityB { + @Id + private Long id; + @Column(unique = true) + private Long uniqueValue; + private String data; + @OneToMany(mappedBy = "b") + private Set as; + + public EntityB() { + } + + public EntityB(Long id, String data) { + this.id = id; + this.uniqueValue = id; + this.data = data; + } + } +}