diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java index 59538be184f7..a2c0dc587537 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java @@ -190,16 +190,18 @@ static class CollectionAreCollectionFieldsDirty { @Advice.Return(readOnly = false) boolean returned, @FieldName String fieldName, @FieldValue Collection collection, - @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker) { - if ( !returned && $$_hibernate_collectionTracker != null ) { - final int size = $$_hibernate_collectionTracker.getSize( fieldName ); - if ( collection == null && size != -1 ) { + @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker, + @AttributeInterceptor PersistentAttributeInterceptor $$_hibernate_attributeInterceptor) { + // Only look at initialized attributes, since value sameness is tracked via InlineDirtyCheckingHandler + if ( !returned && $$_hibernate_collectionTracker != null && ( $$_hibernate_attributeInterceptor == null + || $$_hibernate_attributeInterceptor.isAttributeLoaded( fieldName ) ) ) { + if ( collection == null && $$_hibernate_collectionTracker.getSize( fieldName ) != -1 ) { returned = true; } else if ( collection != null ) { // We only check sizes of non-persistent or initialized persistent collections if ( ( !( collection instanceof PersistentCollection ) || ( (PersistentCollection) collection ).wasInitialized() ) - && size != collection.size() ) { + && $$_hibernate_collectionTracker.getSize( fieldName ) != collection.size() ) { returned = true; } } @@ -213,16 +215,18 @@ static class MapAreCollectionFieldsDirty { @Advice.Return(readOnly = false) boolean returned, @FieldName String fieldName, @FieldValue Map map, - @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker) { - if ( !returned && $$_hibernate_collectionTracker != null ) { - final int size = $$_hibernate_collectionTracker.getSize( fieldName ); - if ( map == null && size != -1 ) { + @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker, + @AttributeInterceptor PersistentAttributeInterceptor $$_hibernate_attributeInterceptor) { + // Only look at initialized attributes, since value sameness is tracked via InlineDirtyCheckingHandler + if ( !returned && $$_hibernate_collectionTracker != null && ( $$_hibernate_attributeInterceptor == null + || $$_hibernate_attributeInterceptor.isAttributeLoaded( fieldName ) ) ) { + if ( map == null && $$_hibernate_collectionTracker.getSize( fieldName ) != -1 ) { returned = true; } else if ( map != null ) { // We only check sizes of non-persistent or initialized persistent collections if ( ( !( map instanceof PersistentCollection ) || ( (PersistentCollection) map ).wasInitialized() ) - && size != map.size() ) { + && $$_hibernate_collectionTracker.getSize( fieldName ) != map.size() ) { returned = true; } } @@ -232,20 +236,22 @@ else if ( map != null ) { static class CollectionGetCollectionFieldDirtyNames { @Advice.OnMethodExit - static void $$_hibernate_areCollectionFieldsDirty( + static void $$_hibernate_getCollectionFieldDirtyNames( @FieldName String fieldName, @FieldValue Collection collection, @Advice.Argument(0) DirtyTracker tracker, - @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker) { - if ( $$_hibernate_collectionTracker != null ) { - final int size = $$_hibernate_collectionTracker.getSize( fieldName ); - if ( collection == null && size != -1 ) { + @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker, + @AttributeInterceptor PersistentAttributeInterceptor $$_hibernate_attributeInterceptor) { + // Only look at initialized attributes, since value sameness is tracked via InlineDirtyCheckingHandler + if ( $$_hibernate_collectionTracker != null && ( $$_hibernate_attributeInterceptor == null + || $$_hibernate_attributeInterceptor.isAttributeLoaded( fieldName ) ) ) { + if ( collection == null && $$_hibernate_collectionTracker.getSize( fieldName ) != -1 ) { tracker.add( fieldName ); } else if ( collection != null ) { // We only check sizes of non-persistent or initialized persistent collections if ( ( !( collection instanceof PersistentCollection ) || ( (PersistentCollection) collection ).wasInitialized() ) - && size != collection.size() ) { + && $$_hibernate_collectionTracker.getSize( fieldName ) != collection.size() ) { tracker.add( fieldName ); } } @@ -255,20 +261,22 @@ else if ( collection != null ) { static class MapGetCollectionFieldDirtyNames { @Advice.OnMethodExit - static void $$_hibernate_areCollectionFieldsDirty( + static void $$_hibernate_getCollectionFieldDirtyNames( @FieldName String fieldName, @FieldValue Map map, @Advice.Argument(0) DirtyTracker tracker, - @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker) { - if ( $$_hibernate_collectionTracker != null ) { - final int size = $$_hibernate_collectionTracker.getSize( fieldName ); - if ( map == null && size != -1 ) { + @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker, + @AttributeInterceptor PersistentAttributeInterceptor $$_hibernate_attributeInterceptor) { + // Only look at initialized attributes, since value sameness is tracked via InlineDirtyCheckingHandler + if ( $$_hibernate_collectionTracker != null && ( $$_hibernate_attributeInterceptor == null + || $$_hibernate_attributeInterceptor.isAttributeLoaded( fieldName ) ) ) { + if ( map == null && $$_hibernate_collectionTracker.getSize( fieldName ) != -1 ) { tracker.add( fieldName ); } else if ( map != null ) { // We only check sizes of non-persistent or initialized persistent collections if ( ( !( map instanceof PersistentCollection ) || ( (PersistentCollection) map ).wasInitialized() ) - && size != map.size() ) { + && $$_hibernate_collectionTracker.getSize( fieldName ) != map.size() ) { tracker.add( fieldName ); } } @@ -278,16 +286,14 @@ else if ( map != null ) { static class CollectionGetCollectionClearDirtyNames { @Advice.OnMethodExit - static void $$_hibernate_clearDirtyCollectionNames( + static void $$_hibernate_removeDirtyFields( @FieldName String fieldName, @FieldValue Collection collection, @Advice.Argument(value = 0, readOnly = false) LazyAttributeLoadingInterceptor lazyInterceptor, @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker) { - if ( lazyInterceptor == null || lazyInterceptor.isAttributeLoaded( fieldName ) ) { - if ( collection == null || collection instanceof PersistentCollection && !( (PersistentCollection) collection ).wasInitialized() ) { - $$_hibernate_collectionTracker.add( fieldName, -1 ); - } - else { + // Only look at initialized attributes, since value sameness is tracked via InlineDirtyCheckingHandler + if ( ( lazyInterceptor == null || lazyInterceptor.isAttributeLoaded( fieldName ) ) && collection != null ) { + if ( !( collection instanceof PersistentCollection ) || ( (PersistentCollection) collection ).wasInitialized() ) { $$_hibernate_collectionTracker.add( fieldName, collection.size() ); } } @@ -296,16 +302,14 @@ static class CollectionGetCollectionClearDirtyNames { static class MapGetCollectionClearDirtyNames { @Advice.OnMethodExit - static void $$_hibernate_clearDirtyCollectionNames( + static void $$_hibernate_removeDirtyFields( @FieldName String fieldName, @FieldValue Map map, @Advice.Argument(value = 0, readOnly = false) LazyAttributeLoadingInterceptor lazyInterceptor, @Advice.FieldValue(EnhancerConstants.TRACKER_COLLECTION_NAME) CollectionTracker $$_hibernate_collectionTracker) { - if ( lazyInterceptor == null || lazyInterceptor.isAttributeLoaded( fieldName ) ) { - if ( map == null || map instanceof PersistentCollection && !( (PersistentCollection) map ).wasInitialized() ) { - $$_hibernate_collectionTracker.add( fieldName, -1 ); - } - else { + // Only look at initialized attributes, since value sameness is tracked via InlineDirtyCheckingHandler + if ( ( lazyInterceptor == null || lazyInterceptor.isAttributeLoaded( fieldName ) ) && map != null ) { + if ( !( map instanceof PersistentCollection ) || ( (PersistentCollection) map ).wasInitialized() ) { $$_hibernate_collectionTracker.add( fieldName, map.size() ); } } @@ -594,6 +598,11 @@ static Object getterSelf() { } + @Retention(RetentionPolicy.RUNTIME) + @interface AttributeInterceptor { + + } + // mapping to get private field from superclass by calling the enhanced reader, for use when field is not visible static class GetterMapping implements Advice.OffsetMapping { diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java index 99508db43d38..f5252ab5b403 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java @@ -290,6 +290,25 @@ private DynamicType.Builder doEnhance(Supplier> builde .defineMethod( EnhancerConstants.TRACKER_COLLECTION_GET_NAME, constants.TypeCollectionTracker, constants.modifierPUBLIC ) .intercept( FieldAccessor.ofField( EnhancerConstants.TRACKER_COLLECTION_NAME ) ); + final Advice.OffsetMapping.Factory attributeInterceptor; + if ( enhancementContext.hasLazyLoadableAttributes( managedCtClass ) ) { + attributeInterceptor = new Advice.OffsetMapping.ForField.Resolved.Factory<>( + CodeTemplates.AttributeInterceptor.class, + new FieldDescription.Latent( + managedCtClass, + EnhancerConstants.INTERCEPTOR_FIELD_NAME, + constants.modifierPRIVATE_TRANSIENT, + constants.TypePersistentAttributeInterceptor.asGenericType(), + Collections.emptyList() + ) + ); + } + else { + attributeInterceptor = Advice.OffsetMapping.ForStackManipulation.Factory.of( + CodeTemplates.AttributeInterceptor.class, + null + ); + } Implementation isDirty = StubMethod.INSTANCE, getDirtyNames = StubMethod.INSTANCE, clearDirtyNames = StubMethod.INSTANCE; for ( AnnotatedFieldDescription collectionField : collectionFields ) { String collectionFieldName = collectionField.getName(); @@ -311,11 +330,13 @@ private DynamicType.Builder doEnhance(Supplier> builde isDirty = Advice.withCustomMapping() .bind( CodeTemplates.FieldName.class, collectionFieldName ) .bind( CodeTemplates.FieldValue.class, fieldDescription ) + .bind( attributeInterceptor ) .to( adviceIsDirty, constants.adviceLocator ) .wrap( isDirty ); getDirtyNames = Advice.withCustomMapping() .bind( CodeTemplates.FieldName.class, collectionFieldName ) .bind( CodeTemplates.FieldValue.class, fieldDescription ) + .bind( attributeInterceptor ) .to( adviceGetDirtyNames, constants.adviceLocator ) .wrap( getDirtyNames ); clearDirtyNames = Advice.withCustomMapping() @@ -330,11 +351,13 @@ private DynamicType.Builder doEnhance(Supplier> builde isDirty = Advice.withCustomMapping() .bind( CodeTemplates.FieldName.class, collectionFieldName ) .bind( CodeTemplates.FieldValue.class, getterMapping ) + .bind( attributeInterceptor ) .to( adviceIsDirty, constants.adviceLocator ) .wrap( isDirty ); getDirtyNames = Advice.withCustomMapping() .bind( CodeTemplates.FieldName.class, collectionFieldName ) .bind( CodeTemplates.FieldValue.class, getterMapping ) + .bind( attributeInterceptor ) .to( adviceGetDirtyNames, constants.adviceLocator ) .wrap( getDirtyNames ); clearDirtyNames = Advice.withCustomMapping() diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckerEqualsHelper.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckerEqualsHelper.java index bb5b0e8b97f0..6e7cc22d29e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckerEqualsHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckerEqualsHelper.java @@ -24,6 +24,19 @@ public static boolean areEquals( return Objects.deepEquals( a, b ); } + public static boolean areSame( + PersistentAttributeInterceptable persistentAttributeInterceptable, + String fieldName, + Object a, + Object b) { + final PersistentAttributeInterceptor persistentAttributeInterceptor = persistentAttributeInterceptable.$$_hibernate_getInterceptor(); + if ( persistentAttributeInterceptor != null + && !persistentAttributeInterceptor.isAttributeLoaded( fieldName ) ) { + return false; + } + return a == b; + } + public static boolean areEquals( PersistentAttributeInterceptable persistentAttributeInterceptable, String fieldName, diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java index 3eaede6d814f..abc78987711f 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java @@ -5,6 +5,7 @@ package org.hibernate.bytecode.enhance.internal.bytebuddy; import java.util.Collection; +import java.util.Map; import java.util.Objects; import jakarta.persistence.EmbeddedId; @@ -40,16 +41,19 @@ final class InlineDirtyCheckingHandler implements Implementation, ByteCodeAppend private final FieldDescription.InDefinedShape persistentField; private final boolean applyLazyCheck; + private final boolean applySamenessCheck; private InlineDirtyCheckingHandler( Implementation delegate, TypeDescription managedCtClass, FieldDescription.InDefinedShape persistentField, - boolean applyLazyCheck) { + boolean applyLazyCheck, + boolean applySamenessCheck) { this.delegate = delegate; this.managedCtClass = managedCtClass; this.persistentField = persistentField; this.applyLazyCheck = applyLazyCheck; + this.applySamenessCheck = applySamenessCheck; } static Implementation wrap( @@ -63,14 +67,16 @@ static Implementation wrap( implementation = Advice.to( CodeTemplates.CompositeDirtyCheckingHandler.class ).wrap( implementation ); } else if ( !persistentField.hasAnnotation( Id.class ) - && !persistentField.hasAnnotation( EmbeddedId.class ) - && !( persistentField.getType().asErasure().isAssignableTo( Collection.class ) - && enhancementContext.isMappedCollection( persistentField ) ) ) { + && !persistentField.hasAnnotation( EmbeddedId.class ) ) { implementation = new InlineDirtyCheckingHandler( implementation, managedCtClass, persistentField.asDefined(), - enhancementContext.hasLazyLoadableAttributes( managedCtClass ) + enhancementContext.hasLazyLoadableAttributes( managedCtClass ), + // Also track value changes (object identity) for persistent collection attributes + ( persistentField.getType().asErasure().isAssignableTo( Collection.class ) + || persistentField.getType().asErasure().isAssignableTo( Map.class ) ) + && enhancementContext.isMappedCollection( persistentField ) ); } @@ -158,7 +164,7 @@ public Size apply( methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, HELPER_TYPE_NAME, - "areEquals", + applySamenessCheck ? "areSame" : "areEquals", Type.getMethodDescriptor( Type.BOOLEAN_TYPE, PE_INTERCEPTABLE_TYPE, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/CollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/CollectionPart.java index 6c6ee8f869d8..7dd2b246453d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/CollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/CollectionPart.java @@ -90,4 +90,9 @@ default String getPartName() { default ModelPart getInclusionCheckPart() { return this; } + + @Override + default boolean isReadOnly() { + return getCollectionAttribute().isReadOnly(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java index 3220cfcfe067..293e45996d08 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java @@ -220,4 +220,10 @@ default boolean isPluralAttributeMapping() { return true; } + @Override + default boolean isReadOnly() { + return getCollectionDescriptor().getMappedByProperty() != null + || getKeyDescriptor().getKeyPart().isReadOnly(); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SelectableMappings.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SelectableMappings.java index 3715015477fa..ae7d3d23d00b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SelectableMappings.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SelectableMappings.java @@ -40,4 +40,15 @@ default int forEachSelectable(SelectableConsumer consumer) { return forEachSelectable( 0, consumer ); } + default boolean isReadOnly() { + final int jdbcTypeCount = getJdbcTypeCount(); + for ( int i = 0; i < jdbcTypeCount; i++ ) { + final var selectableMapping = getSelectable( i ); + if ( selectableMapping.isInsertable() || selectableMapping.isUpdateable() ) { + return false; + } + } + return true; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 76d9652c8e94..f28832e6609d 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -2455,7 +2455,7 @@ public int[] resolveDirtyAttributeIndexes( } if ( attributeNames.length != 0 ) { - final boolean[] propertyUpdateability = getPropertyUpdateability(); + final boolean[] propertyCheckability = getPropertyCheckability(); if ( superMappingType == null ) { /* Sort attribute names so that we can traverse mappings efficiently @@ -2482,7 +2482,7 @@ class ChildEntity extends SuperEntity { final String attributeName = attributeMapping.getAttributeName(); if ( isPrefix( attributeMapping, attributeNames[index] ) ) { final int position = attributeMapping.getStateArrayPosition(); - if ( propertyUpdateability[position] && !fields.contains( position ) ) { + if ( propertyCheckability[position] && !fields.contains( position ) ) { fields.add( position ); } index++; @@ -2506,7 +2506,7 @@ class ChildEntity extends SuperEntity { else { for ( String attributeName : attributeNames ) { final Integer index = getPropertyIndexOrNull( attributeName ); - if ( index != null && propertyUpdateability[index] && !fields.contains( index ) ) { + if ( index != null && propertyCheckability[index] && !fields.contains( index ) ) { fields.add( index ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/AbstractImmediateCollectionInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/AbstractImmediateCollectionInitializer.java index c9b700fcd633..250d23dc0f53 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/AbstractImmediateCollectionInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/AbstractImmediateCollectionInitializer.java @@ -37,6 +37,7 @@ public abstract class AbstractImmediateCollectionInitializer extends AbstractCollectionInitializer implements BiConsumer> { + private final boolean isReadOnly; /** * refers to the rows entry in the collection. null indicates that the collection is empty */ @@ -82,6 +83,7 @@ public AbstractImmediateCollectionInitializer( collectionKeyResult == collectionValueKeyResult ? null : collectionValueKeyResult.createResultAssembler( this, creationState ); + this.isReadOnly = collectionAttributeMapping.isReadOnly(); } @Override @@ -319,7 +321,13 @@ protected void setMissing(Data data) { public void resolveInstance(Object instance, Data data) { assert data.getState() == State.UNINITIALIZED || instance == data.getCollectionInstance(); if ( instance == null ) { - setMissing( data ); + if ( isReadOnly ) { + // When the mapping is read-only, we can't trust the state of the persistence context + resolveKey( data ); + } + else { + setMissing( data ); + } } else { final var rowProcessingState = data.getRowProcessingState(); 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 9b9894c307f0..f8c3e1ad127a 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 @@ -129,104 +129,87 @@ 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 ); } else { - resolve( instance, data ); - } - } - - private void resolve(Object instance, Data data) { - 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 var lazyInitializer = extractLazyInitializer( instance ); - data.entityIdentifier = null; - if ( lazyInitializer == null ) { - // Entity is most probably initialized - data.setInstance( instance ); - if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); + final var lazyInitializer = extractLazyInitializer( instance ); + if ( lazyInitializer == null ) { + // Entity is most probably initialized + if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() && isPersistentAttributeInterceptable( instance ) && getAttributeInterceptor( instance ) instanceof EnhancementAsProxyLazinessInterceptor enhancementInterceptor ) { - if ( enhancementInterceptor.isInitialized() ) { - data.setState( State.INITIALIZED ); + 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(); - } - if ( data.entityIdentifier == null ) { - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); } } - 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 ); } - } - 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() ); - } + else { + // Entity is initialized + data.setState( State.INITIALIZED ); + } - 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 var 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 var 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 645ad6417c6c..293a978974a5 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 @@ -9,9 +9,12 @@ import org.hibernate.Hibernate; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; +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; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; import org.hibernate.sql.results.graph.DomainResultAssembler; @@ -46,6 +49,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; @@ -78,6 +82,16 @@ public DiscriminatedEntityInitializer( keyIsEager = keyValueAssembler.isEager(); hasLazySubInitializer = keyValueAssembler.hasLazySubInitializers(); + 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 @@ -202,74 +216,93 @@ 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 { - resolve( instance, data ); final var rowProcessingState = data.getRowProcessingState(); - if ( keyIsEager ) { - final var initializer = keyValueAssembler.getInitializer(); - assert initializer != null; - initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); + final var session = rowProcessingState.getSession(); + final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); + if ( lazyInitializer == null ) { + data.setState( State.INITIALIZED ); + data.concreteDescriptor = session.getEntityPersister( null, instance ); + data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); + identifierResolved = false; } - else if ( rowProcessingState.needsResolveState() ) { - // 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 ); + else if ( lazyInitializer.isUninitialized() ) { + data.setState( eager ? State.RESOLVED : State.INITIALIZED ); + // Read the discriminator from the result set if necessary + final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); + data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + 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; } - private void resolve( - Object instance, - DiscriminatedEntityInitializerData data) { - final var rowProcessingState = data.getRowProcessingState(); - final var session = rowProcessingState.getSession(); - final var lazyInitializer = extractLazyInitializer( instance ); - if ( lazyInitializer == null ) { - data.setState( State.INITIALIZED ); - data.concreteDescriptor = session.getEntityPersister( null, instance ); - data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); - } - else if ( lazyInitializer.isUninitialized() ) { - data.setState( eager ? State.RESOLVED : State.INITIALIZED ); - // Read the discriminator from the result set if necessary - final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); - data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + @Override + public void resolveInstance(Object instance, DiscriminatedEntityInitializerData data) { + final boolean identifierResolved = resolveIdentifier( instance, data ); + if ( data.entityIdentifier == null ) { + data.setState( Initializer.State.MISSING ); + data.concreteDescriptor = null; + data.setInstance( null ); } else { - data.setState( State.INITIALIZED ); - data.concreteDescriptor = session.getEntityPersister( null, lazyInitializer.getImplementation() ); - data.entityIdentifier = lazyInitializer.getInternalIdentifier(); - } - - final var entityKey = new EntityKey( data.entityIdentifier, data.concreteDescriptor ); - final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( - entityKey - ); - - if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { - // the existing entity instance is detached or transient - if ( entityHolder != null ) { - final var managed = entityHolder.getManagedObject(); - data.setInstance( managed ); - data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); - data.setState( !eager || entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + 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 + ); + + 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.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setState( !eager || entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else { + data.setState( State.RESOLVED ); + initializeInstance( data ); + } } else { - data.setState( State.RESOLVED ); - initializeInstance( data ); + data.setInstance( instance ); + } + + 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() && !identifierResolved ) { + // Resolve the state of the identifier if result caching is enabled and this is not a query cache hit + keyValueAssembler.resolveState( rowProcessingState ); + discriminatorValueAssembler.resolveState( rowProcessingState ); } - } - else { - data.setInstance( instance ); } } 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 09cdf9e9f43c..294c5c01441d 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 @@ -55,6 +55,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 @@ -100,6 +101,7 @@ public EntityDelayedFetchInitializer( keyIsEager = identifierAssembler.isEager(); hasLazySubInitializer = identifierAssembler.hasLazySubInitializers(); } + this.isReadOnly = referencedModelPart.isReadOnly(); } @Override @@ -147,26 +149,16 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { data.setState( State.MISSING ); } else { - final var 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 var 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 ); @@ -174,6 +166,27 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { } } + 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, @@ -313,9 +326,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 { @@ -324,15 +356,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(); @@ -344,14 +389,20 @@ public void resolveInstance(Object instance, EntityDelayedFetchInitializerData d } } - if ( keyIsEager ) { + if ( keyIsEager && !identifierResolved ) { final var 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 626dc69f9bcf..f2a9f23f389c 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 @@ -37,6 +37,7 @@ import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl; @@ -100,6 +101,7 @@ public class EntityInitializerImpl private final boolean isPartOfKey; private final boolean isResultInitializer; private final boolean hasKeyManyToOne; + private final boolean isReadOnly; /** * Indicates whether there is a high chance of the previous row to have the same entity key as the current row * and hence enable a check in the {@link #resolveKey(RowProcessingState)} phase which compare the previously read @@ -232,6 +234,7 @@ public EntityInitializerImpl( navigablePath = resultDescriptor.getNavigablePath(); isPartOfKey = Initializer.isPartOfKey( navigablePath, parent ); + this.isReadOnly = isReadOnly( referencedModelPart ); // If the parent already has previous row reuse enabled, we can skip that here previousRowReuse = !isPreviousRowReuse( parent ) && ( // If this entity domain result contains a collection join fetch, this usually means that the entity data is @@ -984,20 +987,32 @@ protected boolean useEmbeddedIdentifierInstanceAsEntity(EntityInitializerData da @Override public void resolveInstance(Object instance, EntityInitializerData data) { if ( instance == null ) { - setMissing( data ); + if ( isReadOnly ) { + // When the mapping is read-only, we can't trust the state of the persistence context + resolveKey( data ); + } + else { + setMissing( data ); + } } else { - final var lazyInitializer = extractLazyInitializer( instance ); + final var lazyInitializer = extractLazyInitializer( instance ); final var rowProcessingState = data.getRowProcessingState(); final var session = rowProcessingState.getSession(); final var persistenceContext = session.getPersistenceContextInternal(); if ( lazyInitializer == null ) { - // Entity is most probably initialized - data.concreteDescriptor = session.getEntityPersister( null, instance ); - resolveEntityKey( - data, - data.concreteDescriptor.getIdentifier( instance, session ) - ); + if ( isReadOnly ) { + // Read-only associations might be inconsistent + resolveKey( data, true ); + if ( data.getState() == State.MISSING ) { + return; + } + } + else { + // Entity is most probably initialized + data.concreteDescriptor = session.getEntityPersister( null, instance ); + resolveEntityKey( data, data.concreteDescriptor.getIdentifier( instance, session ) ); + } data.entityHolder = persistenceContext.claimEntityHolderIfPossible( data.entityKey, null, @@ -1005,8 +1020,7 @@ public void resolveInstance(Object instance, EntityInitializerData data) { this ); if ( data.entityHolder.getManagedObject() == null ) { - final EntityEntry entry = persistenceContext.getEntry( - instance ); // make sure an EntityEntry exists + final EntityEntry entry = persistenceContext.getEntry( instance ); // make sure an EntityEntry exists if ( entry == null ) { // We cannot reuse an entity instance that has no entry in the PC, // this can happen if the parent entity contained a detached instance. @@ -1030,8 +1044,7 @@ else if ( data.entityHolder.getEntity() == null ) { instance = resolveEntityInstance( data ); data.entityKey = data.entityHolder.getEntityKey(); if ( data.entityHolder.getProxy() != null ) { - castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ) - .setImplementation( instance ); + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( instance ); } } else if ( data.entityHolder.getEntity() != instance ) { @@ -1066,7 +1079,15 @@ && getAttributeInterceptor( data.entityInstanceForNotify ) } } else if ( lazyInitializer.isUninitialized() ) { + if ( isReadOnly ) { + // Read-only associations might be inconsistent + resolveKey( data, true ); + if ( data.getState() == State.MISSING ) { + return; + } data.setState( State.RESOLVED ); + } + else {data.setState( State.RESOLVED ); // Read the discriminator from the result set if necessary data.concreteDescriptor = discriminatorAssembler == null @@ -1075,7 +1096,7 @@ else if ( lazyInitializer.isUninitialized() ) { discriminatorAssembler, entityDescriptor ); assert data.concreteDescriptor != null; resolveEntityKey( data, lazyInitializer.getInternalIdentifier() ); - data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + }data.entityHolder = persistenceContext.claimEntityHolderIfPossible( data.entityKey, null, rowProcessingState.getJdbcValuesSourceProcessingState(), @@ -1100,8 +1121,15 @@ else if ( data.entityHolder.getEntity() == null ) { } else { final var implementation = lazyInitializer.getImplementation(); - data.concreteDescriptor = session.getEntityPersister( null, implementation ); - resolveEntityKey( data, lazyInitializer.getInternalIdentifier() ); + if ( isReadOnly ) { + // Read-only associations might be inconsistent + resolveKey( data, true ); + if ( data.getState() == State.MISSING ) { + return; + } + } + else {data.concreteDescriptor = session.getEntityPersister( null, implementation ); + resolveEntityKey( data, lazyInitializer.getInternalIdentifier() );} data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); if ( data.entityHolder.getProxy() == instance ) { data.entityInstanceForNotify = implementation; @@ -1148,6 +1176,11 @@ else if ( data.entityHolder.getEntity() == null ) { } } + private static boolean isReadOnly(EntityValuedModelPart entityValuedModelPart) { + return entityValuedModelPart instanceof ValuedModelPart + && ( (ValuedModelPart) entityValuedModelPart ).isReadOnly(); + } + @Override public void resolveInstance(EntityInitializerData data) { if ( data.getState() == State.KEY_RESOLVED ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchByUniqueKeyInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchByUniqueKeyInitializer.java index 86b70a4f92a6..eff3a6439703 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchByUniqueKeyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchByUniqueKeyInitializer.java @@ -4,13 +4,19 @@ */ package org.hibernate.sql.results.graph.entity.internal; +import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.EntityUniqueKey; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.LazyInitializer; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.Initializer; import org.hibernate.sql.results.graph.InitializerParent; +import org.hibernate.sql.results.jdbc.spi.RowProcessingState; + +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; import static org.hibernate.internal.log.LoggingHelper.toLoggableString; @@ -33,6 +39,88 @@ public EntitySelectFetchByUniqueKeyInitializer( this.fetchedAttribute = fetchedAttribute; } + @Override + public void resolveInstance(Object instance, EntitySelectFetchInitializerData data) { + if ( instance == null && !isReadOnly ) { + data.entityIdentifier = null; + data.setState( State.MISSING ); + data.setInstance( null ); + } + 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 = keyAssembler.assemble( rowProcessingState ); + if ( data.entityIdentifier == null ) { + data.setState( State.MISSING ); + data.setInstance( null ); + } + else { + initialize( data ); + } + } + else { + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var entityDescriptor = getEntityDescriptor(); + final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); + final boolean identifierResolved; + final Object primaryKey; + if ( lazyInitializer == null ) { + data.setState( State.INITIALIZED ); + data.entityIdentifier = + entityDescriptor.getPropertyValue( instance, fetchedAttribute.getReferencedPropertyName() ); + primaryKey = entityDescriptor.getIdentifier( instance, session ); + identifierResolved = false; + } + else if ( lazyInitializer.isUninitialized() ) { + data.setState( State.RESOLVED ); + data.entityIdentifier = keyAssembler.assemble( rowProcessingState ); + primaryKey = lazyInitializer.getIdentifier(); + identifierResolved = true; + } + else { + data.setState( State.INITIALIZED ); + data.entityIdentifier = + entityDescriptor.getPropertyValue( instance, fetchedAttribute.getReferencedPropertyName() ); + primaryKey = lazyInitializer.getIdentifier(); + identifierResolved = false; + } + assert data.entityIdentifier != null; + + final var persistenceContext = session.getPersistenceContextInternal(); + final var entityKey = new EntityKey( primaryKey, concreteDescriptor ); + final var entityHolder = persistenceContext.getEntityHolder( entityKey ); + + if ( entityHolder == null + || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setState( entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else { + initialize( data, null, session, persistenceContext ); + } + } + else { + data.setInstance( instance ); + } + + 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 ); + } + } + } + @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 c13dc3fd144e..c3df93d79585 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 @@ -49,6 +49,7 @@ public class EntitySelectFetchInitializer { + final Map managedB = getEntityBMap( session ); + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentEmpty(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + + fetchQuery( null, managedB, session ); + } ); + } + + @Test + public void testEmptyInconsistentInitialized(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.find( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentInitialized(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.find( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( null, managedB, session ); + } ); + } + + @Test + public void testEmptyInconsistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.getReference( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.getReference( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( null, managedB, session ); + } ); + } + + @Test + public void testEmptyInconsistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + EntityB entity = session.getReference( EntityB.class, entry.getValue().id ); + Hibernate.initialize( entity ); + entry.setValue( entity ); + } + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + EntityB entity = session.getReference( EntityB.class, entry.getValue().id ); + Hibernate.initialize( entity ); + entry.setValue( entity ); + } + + fetchQuery( null, managedB, session ); + } ); + } + + private Map getEntityBMap(SessionImplementor session) { + final var result = session.createQuery( + "from EntityA a left join fetch a.b where a.id = 1", + EntityA.class + ).getSingleResult(); + session.clear(); + return result.getB(); + } + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = new EntityA(); + entityA.id = 1L; + session.persist( entityA ); + + final Map managedB = new HashMap<>(); + managedB.put( "b1", new EntityB( 1L, "b1" ) ); + managedB.put( "b2", new EntityB( 2L, "b2" ) ); + + for ( EntityB entityB : managedB.values() ) { + entityB.a = entityA; + session.persist( entityB ); + } + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + private void fetchQuery(Map b, Map managedB, SessionImplementor session) { + final var persistenceContext = session.getPersistenceContext(); + final var descriptor = session.getFactory().getMappingMetamodel().getEntityDescriptor( EntityB.class ); + + final var entityA = session.find( EntityA.class, 1L ); + entityA.setB( b ); + + final var originalSize = b == null ? -1 : b.size(); + final var firstB = managedB.values().iterator().next(); + final var wasManaged = persistenceContext.getEntityHolder( entityKey( session, descriptor, firstB ) ) != null; + session.flush(); + final var originalB = entityA.getB(); + + final var result = session.createQuery( + "from EntityA a left join fetch a.b where a.id = 1", + EntityA.class + ).getSingleResult(); + + assertThat( persistenceContext.getEntitiesByKey().size() ).isEqualTo( 1 + managedB.size() ); + assertThat( entityA.getB() ).isSameAs( originalB ); + assertThat( originalB == null ? -1 : originalB.size() ).isSameAs( originalSize ); + + for ( EntityB entityB : managedB.values() ) { + final var entityHolder = persistenceContext.getEntityHolder( entityKey( session, descriptor, entityB ) ); + assertThat( entityHolder ).isNotNull(); + if ( wasManaged ) { + assertThat( entityHolder.getManagedObject() ).isSameAs( entityB ); + } + else { + assertThat( entityHolder.getManagedObject() ).isNotSameAs( entityB ); + } + assertThat( Hibernate.isInitialized( entityHolder.getManagedObject() ) ).isTrue(); + } + } + + private static EntityKey entityKey(SessionImplementor session, EntityPersister descriptor, EntityB entityB) { + return session.generateEntityKey( session.getFactory().getPersistenceUnitUtil().getIdentifier( entityB ), descriptor ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + private String name; + @OneToMany(mappedBy = "a") + @MapKey(name = "name") + private Map b = new HashMap<>(); + + public Map getB() { + return b; + } + + public void setB(Map b) { + this.b = b; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + private String name; + @ManyToOne(fetch = FetchType.LAZY) + private EntityA a; + + public EntityB() { + } + + public EntityB(Long id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/InconsistentUnownedCollectionSelectFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/InconsistentUnownedCollectionSelectFetchTest.java new file mode 100644 index 000000000000..3f4e774a0a03 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/InconsistentUnownedCollectionSelectFetchTest.java @@ -0,0 +1,222 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.collection; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.jpa.AvailableHints; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapKey; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + InconsistentUnownedCollectionSelectFetchTest.EntityA.class, + InconsistentUnownedCollectionSelectFetchTest.EntityB.class, +}) +@SessionFactory +@EnhancementOptions(inlineDirtyChecking = true, lazyLoading = true, extendedEnhancement = true) +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class InconsistentUnownedCollectionSelectFetchTest { + @Test + public void testEmptyInconsistentEmpty(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentEmpty(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + + fetchQuery( null, managedB, session ); + } ); + } + + @Test + public void testEmptyInconsistentInitialized(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.find( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentInitialized(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.find( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( null, managedB, session ); + } ); + } + + @Test + public void testEmptyInconsistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.getReference( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + entry.setValue( session.getReference( EntityB.class, entry.getValue().id ) ); + } + + fetchQuery( null, managedB, session ); + } ); + } + + @Test + public void testEmptyInconsistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + EntityB entity = session.getReference( EntityB.class, entry.getValue().id ); + Hibernate.initialize( entity ); + entry.setValue( entity ); + } + + fetchQuery( new HashMap<>(), managedB, session ); + } ); + } + + @Test + public void testNullInconsistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Map managedB = getEntityBMap( session ); + for ( Map.Entry entry : managedB.entrySet() ) { + EntityB entity = session.getReference( EntityB.class, entry.getValue().id ); + Hibernate.initialize( entity ); + entry.setValue( entity ); + } + + fetchQuery( null, managedB, session ); + } ); + } + + private Map getEntityBMap(SessionImplementor session) { + final var result = session.createQuery( + "from EntityA a left join fetch a.b where a.id = 1", + EntityA.class + ).getSingleResult(); + session.clear(); + return result.getB(); + } + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = new EntityA(); + entityA.id = 1L; + session.persist( entityA ); + + final Map managedB = new HashMap<>(); + managedB.put( "b1", new EntityB( 1L, "b1" ) ); + managedB.put( "b2", new EntityB( 2L, "b2" ) ); + + for ( EntityB entityB : managedB.values() ) { + entityB.a = entityA; + session.persist( entityB ); + } + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + private void fetchQuery(Map b, Map managedB, SessionImplementor session) { + final var persistenceContext = session.getPersistenceContext(); + + // Make the collection lazy, even if it is marked as EAGER + final var graph = session.createEntityGraph( EntityA.class ); + final var entityA = session.find( EntityA.class, 1L, Map.of( AvailableHints.HINT_SPEC_FETCH_GRAPH, graph ) ); + entityA.setB( b ); + + final var previousSize = persistenceContext.getEntitiesByKey().size(); + + final var result = session.createQuery( + "from EntityA a where a.id = 1", + EntityA.class + ).getSingleResult(); + + // Ensure that the EAGER timing of the collection does not trigger SELECT fetching, + // because the collection is already "initialized", even if set to a wrong value + assertThat( persistenceContext.getEntitiesByKey().size() ).isEqualTo( previousSize ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + private String name; + @OneToMany(mappedBy = "a", fetch = FetchType.EAGER) + @MapKey(name = "name") + private Map b = new HashMap<>(); + + public Map getB() { + return b; + } + + public void setB(Map b) { + this.b = b; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + private String name; + @ManyToOne(fetch = FetchType.LAZY) + private EntityA a; + + public EntityB() { + } + + public EntityB(Long id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java index e593c994b239..dd91aa8ccfee 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 79670834ca68..5e96761ef630 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 e6e9fbb3ce4e..0cf0c482595a 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..e719a1a441f6 --- /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: Apache-2.0 + * 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 26571d8cac87..b8ce38122d09 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 6a4b7e7b7b88..99a9cb7e1015 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 b7f54991f575..4ad0bc1c6419 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; + } + } +}