diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java index 89d64b029f0b..d873ce18bd62 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java @@ -404,29 +404,20 @@ public EntityHolder claimEntityHolderIfPossible( EntityHolderImpl holder = getOrInitializeNewHolder().withEntity( key, key.getPersister(), entity ); final EntityHolderImpl oldHolder = entityHolderMap.putIfAbsent( key, newEntityHolder ); if ( oldHolder != null ) { + // An initializer can't claim an entity holder if it's already initialized + if ( oldHolder.isInitialized() ) { + return oldHolder; + } if ( entity != null ) { assert oldHolder.entity == null || oldHolder.entity == entity; oldHolder.entity = entity; } - // Skip setting a new entity initializer if there already is one owner - // Also skip if an entity exists which is different from the effective optional object. - // The effective optional object is the current object to be refreshed, - // which always needs re-initialization, even if already initialized - if ( oldHolder.entityInitializer != null - || oldHolder.entity != null && oldHolder.state != EntityHolderState.ENHANCED_PROXY && ( - processingState.getProcessingOptions().getEffectiveOptionalObject() == null - || oldHolder.entity != processingState.getProcessingOptions().getEffectiveOptionalObject() ) - ) { - return oldHolder; - } holder = oldHolder; } else { newEntityHolder = null; } - assert holder.entityInitializer == null || holder.entityInitializer == initializer; holder.entityInitializer = initializer; - processingState.registerLoadingEntityHolder( holder ); return holder; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/ImmutableBitSet.java b/hibernate-core/src/main/java/org/hibernate/internal/util/ImmutableBitSet.java index c0f280152aa8..19f2ed5fec9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/ImmutableBitSet.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/ImmutableBitSet.java @@ -4,6 +4,8 @@ */ package org.hibernate.internal.util; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.Arrays; import java.util.BitSet; @@ -26,6 +28,10 @@ private ImmutableBitSet(long[] words) { this.words = words; } + public static ImmutableBitSet valueOfOrEmpty(@Nullable BitSet bitSet) { + return bitSet == null ? EMPTY : valueOf( bitSet ); + } + public static ImmutableBitSet valueOf(BitSet bitSet) { final long[] words = bitSet.toLongArray(); return words.length == 0 ? EMPTY : new ImmutableBitSet( words ); @@ -70,4 +76,7 @@ public boolean equals(Object obj) { return Arrays.equals( words, set.words ); } + public BitSet toBitSet() { + return BitSet.valueOf( words ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java index dbb0c052da59..6e6525df9f14 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java @@ -598,7 +598,7 @@ private Fetch createDelayedCollectionFetch( // returning a domain result assembler that returns LazyPropertyInitializer.UNFETCHED_PROPERTY final EntityMappingType containingEntityMapping = findContainingEntityMapping(); final boolean unfetched; - if ( fetchParent.getReferencedModePart() == containingEntityMapping + if ( fetchParent.getReferencedMappingContainer() == containingEntityMapping && containingEntityMapping.getEntityPersister().getPropertyLaziness()[getStateArrayPosition()] ) { collectionKeyDomainResult = null; unfetched = 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 3217e9650e34..4bfed0fa59c1 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 @@ -84,6 +84,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.FilterAliasGenerator; import org.hibernate.internal.FilterHelper; +import org.hibernate.internal.util.ImmutableBitSet; import org.hibernate.internal.util.IndexedConsumer; import org.hibernate.internal.util.MarkerObject; import org.hibernate.internal.util.StringHelper; @@ -1734,14 +1735,21 @@ protected boolean initializeLazyProperty( final EntityEntry entry, final int index, final Object propValue) { - setPropertyValue( entity, lazyPropertyNumbers[index], propValue ); - if ( entry.getLoadedState() != null ) { + final int propertyNumber = lazyPropertyNumbers[index]; + setPropertyValue( entity, propertyNumber, propValue ); + if ( entry.getMaybeLazySet() != null ) { + var bitSet = entry.getMaybeLazySet().toBitSet(); + bitSet.set( propertyNumber ); + entry.setMaybeLazySet( ImmutableBitSet.valueOf( bitSet ) ); + } + final var loadedState = entry.getLoadedState(); + if ( loadedState != null ) { // object have been loaded with setReadOnly(true); HHH-2236 - entry.getLoadedState()[lazyPropertyNumbers[index]] = lazyPropertyTypes[index].deepCopy( propValue, factory ); + loadedState[propertyNumber] = lazyPropertyTypes[index].deepCopy( propValue, factory ); } // If the entity has deleted state, then update that as well if ( entry.getDeletedState() != null ) { - entry.getDeletedState()[lazyPropertyNumbers[index]] = lazyPropertyTypes[index].deepCopy( propValue, factory ); + entry.getDeletedState()[propertyNumber] = lazyPropertyTypes[index].deepCopy( propValue, factory ); } return fieldName.equals( lazyPropertyNames[index] ); } @@ -1763,6 +1771,11 @@ protected boolean initializeLazyProperty( // Used by Hibernate Reactive protected void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { setPropertyValue( entity, index, propValue ); + if ( entry.getMaybeLazySet() != null ) { + var bitSet = entry.getMaybeLazySet().toBitSet(); + bitSet.set( index ); + entry.setMaybeLazySet( ImmutableBitSet.valueOf( bitSet ) ); + } final Object[] loadedState = entry.getLoadedState(); if ( loadedState != null ) { // object have been loaded with setReadOnly(true); HHH-2236 diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index 35f17c1a6b89..382d7b5abaab 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 @@ -23,6 +23,7 @@ import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.entry.CacheEntry; +import org.hibernate.engine.FetchTiming; import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityHolder; @@ -65,9 +66,11 @@ import org.hibernate.sql.results.graph.Initializer; import org.hibernate.sql.results.graph.InitializerData; import org.hibernate.sql.results.graph.InitializerParent; +import org.hibernate.sql.results.graph.UnfetchedResultAssembler; import org.hibernate.sql.results.graph.basic.BasicResultAssembler; import org.hibernate.sql.results.graph.collection.internal.AbstractImmediateCollectionInitializer; import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; +import org.hibernate.sql.results.graph.collection.internal.UnfetchedCollectionAssembler; import org.hibernate.sql.results.graph.entity.EntityInitializer; import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; import org.hibernate.sql.results.graph.internal.AbstractInitializer; @@ -133,12 +136,13 @@ public class EntityInitializerImpl extends AbstractInitializer[][] assemblers; private final @Nullable Initializer[] allInitializers; private final @Nullable Initializer[][] subInitializers; - private final @Nullable Initializer[][] subInitializersForResolveFromInitialized; + private final @Nullable Initializer[][] eagerSubInitializers; private final @Nullable Initializer[][] collectionContainingSubInitializers; private final MutabilityPlan[][] updatableAttributeMutabilityPlans; private final ImmutableBitSet[] lazySets; private final ImmutableBitSet[] maybeLazySets; private final boolean hasLazySubInitializers; + private final boolean hasLazyInitializingSubAssemblers; public static class EntityInitializerData extends InitializerData { @@ -307,6 +311,7 @@ public EntityInitializerImpl( } boolean hasLazySubInitializers = false; + boolean hasLazyInitializingSubAssemblers = false; for ( int i = 0; i < fetchableCount; i++ ) { final AttributeMapping attributeMapping = entityDescriptor.getFetchable( i ).asAttributeMapping(); final Fetch fetch = resultDescriptor.findFetch( attributeMapping ); @@ -325,8 +330,10 @@ public EntityInitializerImpl( subInitializers[subclassId] = new Initializer[fetchableCount]; eagerSubInitializers[subclassId] = new Initializer[fetchableCount]; collectionContainingSubInitializers[subclassId] = new Initializer[fetchableCount]; - lazySets[subclassId] = new BitSet( fetchableCount ); - maybeLazySets[subclassId] = new BitSet( fetchableCount ); + if ( lazySets[subclassId] == null ) { + lazySets[subclassId] = new BitSet( fetchableCount ); + maybeLazySets[subclassId] = new BitSet( fetchableCount ); + } } subInitializers[subclassId][stateArrayPosition] = subInitializer; if ( subInitializer.isEager() ) { @@ -349,6 +356,20 @@ public EntityInitializerImpl( hasLazySubInitializers = true; } } + else if ( stateAssembler instanceof UnfetchedResultAssembler + || stateAssembler instanceof UnfetchedCollectionAssembler ) { + if ( lazySets[subclassId] == null ) { + lazySets[subclassId] = new BitSet( fetchableCount ); + maybeLazySets[subclassId] = new BitSet( fetchableCount ); + } + // Lazy basic attribute + lazySets[subclassId].set( stateArrayPosition ); + maybeLazySets[subclassId].set( stateArrayPosition ); + } + else if ( attributeMapping.getMappedFetchOptions().getTiming() == FetchTiming.DELAYED ) { + // The state assembler for this lazy basic attribute will initialize the attribute + hasLazyInitializingSubAssemblers = true; + } assemblers[subclassId][stateArrayPosition] = stateAssembler; final AttributeMetadata attributeMetadata = attributeMapping.getAttributeMetadata(); @@ -364,14 +385,19 @@ public EntityInitializerImpl( subInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; eagerSubInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; collectionContainingSubInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; - lazySets[subMappingType.getSubclassId()] = new BitSet(fetchableCount); - maybeLazySets[subMappingType.getSubclassId()] = new BitSet(fetchableCount); } subInitializers[subMappingType.getSubclassId()][stateArrayPosition] = subInitializer; eagerSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] = eagerSubInitializers[subclassId][stateArrayPosition]; collectionContainingSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] = collectionContainingSubInitializers[subclassId][stateArrayPosition]; + } + if ( lazySets[subclassId] != null ) { + assert maybeLazySets[subclassId] != null; + if ( lazySets[subMappingType.getSubclassId()] == null ) { + lazySets[subMappingType.getSubclassId()] = new BitSet( fetchableCount ); + maybeLazySets[subMappingType.getSubclassId()] = new BitSet( fetchableCount ); + } if ( lazySets[subclassId].get( stateArrayPosition ) ) { lazySets[subMappingType.getSubclassId()].set( stateArrayPosition ); } @@ -381,7 +407,6 @@ public EntityInitializerImpl( } } } - final BitSet emptyBitSet = new BitSet(); for ( int i = 0; i < subInitializers.length; i++ ) { boolean emptySubInitializers = true; if ( subInitializers[i] != null ) { @@ -394,8 +419,6 @@ public EntityInitializerImpl( } if ( emptySubInitializers ) { subInitializers[i] = Initializer.EMPTY_ARRAY; - lazySets[i] = emptyBitSet; - maybeLazySets[i] = emptyBitSet; } boolean emptyContainingSubInitializers = true; @@ -427,16 +450,14 @@ public EntityInitializerImpl( this.assemblers = assemblers; this.allInitializers = allInitializers; this.subInitializers = subInitializers; - this.subInitializersForResolveFromInitialized = - rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() - ? subInitializers - : eagerSubInitializers; + this.eagerSubInitializers = eagerSubInitializers; this.collectionContainingSubInitializers = collectionContainingSubInitializers; this.lazySets = Arrays.stream( lazySets ).map( ImmutableBitSet::valueOf ).toArray( ImmutableBitSet[]::new ); this.maybeLazySets = Arrays.stream( maybeLazySets ) .map( ImmutableBitSet::valueOf ) .toArray( ImmutableBitSet[]::new ); this.hasLazySubInitializers = hasLazySubInitializers; + this.hasLazyInitializingSubAssemblers = hasLazyInitializingSubAssemblers; this.updatableAttributeMutabilityPlans = updatableAttributeMutabilityPlans; this.notFoundAction = notFoundAction; @@ -444,6 +465,12 @@ public EntityInitializerImpl( this.affectedByFilter = affectedByFilter; } + private static ImmutableBitSet[] toBitSetArray(BitSet[] lazySets) { + return Arrays.stream( lazySets ) + .map( ImmutableBitSet::valueOfOrEmpty ) + .toArray( ImmutableBitSet[]::new ); + } + private static boolean isPreviousRowReuse(@Nullable InitializerParent parent) { // Traverse up the parents to find out if one of our parents has row reuse enabled while ( parent != null ) { @@ -510,9 +537,6 @@ public void resetResolvedEntityRegistrations(RowProcessingState rowProcessingSta rowProcessingState.getSession() .getPersistenceContextInternal() .removeEntityHolder( data.entityKey ); - rowProcessingState.getJdbcValuesSourceProcessingState() - .getLoadingEntityHolders() - .remove( data.entityHolder ); data.entityKey = null; data.entityHolder = null; data.entityInstanceForNotify = null; @@ -633,16 +657,24 @@ protected void resolveInstanceSubInitializers(EntityInitializerData data) { final Initializer[] initializers; final ImmutableBitSet maybeLazySet; if ( data.entityHolder.getEntityInitializer() == this ) { - // When a previous row initialized this entity already, we only need to process collections + // When this entity is already initialized, but this initializer runs anyway, + // we only need to process collection containing initializers initializers = collectionContainingSubInitializers[subclassId]; maybeLazySet = null; } else { - initializers = subInitializersForResolveFromInitialized[subclassId]; + // If an entity has unfetched attributes, we should probably also invoke non-eager initializers. + // Non-eager initializers only set proxies, but since that contains the FK information, + // it would be wasteful not to set that information on the bytecode enhanced entity + var subInitializersToUse = rootEntityDescriptor.getBytecodeEnhancementMetadata().hasUnFetchedAttributes( data.entityInstanceForNotify ) + ? subInitializers + : eagerSubInitializers; + initializers = subInitializersToUse[subclassId]; maybeLazySet = entityEntry.getMaybeLazySet(); // Skip resolving if this initializer has no sub-initializers // or the lazy set of this initializer is a superset/contains the entity entry maybeLazySet - if ( initializers.length == 0 || maybeLazySet != null && lazySets[subclassId].contains( maybeLazySet ) ) { + if ( initializers.length == 0 && !hasLazyInitializingSubAssemblers + || maybeLazySet != null && lazySets[subclassId].contains( maybeLazySet ) ) { return; } } @@ -667,19 +699,56 @@ protected void resolveInstanceSubInitializers(EntityInitializerData data) { else { state = loadedState; } - for ( int i = 0; i < initializers.length; i++ ) { - final Initializer initializer = initializers[i]; - if ( initializer != null && ( maybeLazySet == null || maybeLazySet.get( i ) ) ) { - final Object subInstance = state[i]; - if ( subInstance == UNFETCHED_PROPERTY ) { - // Go through the normal initializer process - initializer.resolveKey( rowProcessingState ); + boolean needsLoadedValuesUpdate = false; + if ( initializers.length == 0 ) { + if ( hasLazyInitializingSubAssemblers ) { + var subAssemblers = assemblers[subclassId]; + for ( int i = 0; i < state.length; i++ ) { + final Object subInstance = state[i]; + final var assembler = subAssemblers[i]; + if ( subInstance == UNFETCHED_PROPERTY + && !(assembler instanceof UnfetchedResultAssembler) + && !(assembler instanceof UnfetchedCollectionAssembler) ) { + // This assembler will produce a value when the underlying entity property is still lazy + needsLoadedValuesUpdate = true; + break; + } } - else { - initializer.resolveInstance( subInstance, rowProcessingState ); + } + } + else { + var eagerInitializers = eagerSubInitializers[subclassId]; + for ( int i = 0; i < state.length; i++ ) { + final var initializer = initializers[i]; + if ( maybeLazySet == null || maybeLazySet.get( i ) ) { + final Object subInstance = state[i]; + if ( initializer != null ) { + if ( subInstance == UNFETCHED_PROPERTY ) { + // Go through the normal initializer process + initializer.resolveKey( rowProcessingState ); + // Assume that the initializer will produce a proxy or the real value + needsLoadedValuesUpdate = true; + } + // Avoid resolving initializers that are not lazy when the property isn't unfetched + else if ( eagerInitializers.length != 0 && eagerInitializers[i] != null ) { + initializer.resolveInstance( subInstance, rowProcessingState ); + } + } + else if ( !needsLoadedValuesUpdate && hasLazyInitializingSubAssemblers && subInstance == UNFETCHED_PROPERTY ) { + final var assembler = assemblers[subclassId][i]; + if ( !( assembler instanceof UnfetchedResultAssembler ) + && !( assembler instanceof UnfetchedCollectionAssembler ) ) { + // This assembler will produce a value when the underlying entity property is still lazy + needsLoadedValuesUpdate = true; + } + } } } } + if ( needsLoadedValuesUpdate ) { + // Mark as resolved to update the state of the entity during initialization phase + data.setState( State.RESOLVED ); + } } private void notifySubInitializersToReusePreviousRowInstance(EntityInitializerData data) { @@ -692,7 +761,11 @@ private void notifySubInitializersToReusePreviousRowInstance(EntityInitializerDa maybeLazySet = null; } else { - subInitializer = subInitializersForResolveFromInitialized[data.concreteDescriptor.getSubclassId()]; + // The entity is already initialized, and we saw it in the previous row, + // so the only sensible thing to do is to notify all eager sub-initializers about this. + // The eager sub-initializers can then potentially initialize already set proxies or + // continue resolving data for collections that ought to be loaded through this initializer + subInitializer = eagerSubInitializers[data.concreteDescriptor.getSubclassId()]; maybeLazySet = entityEntry == null ? null : entityEntry.getMaybeLazySet(); } final RowProcessingState rowProcessingState = data.getRowProcessingState(); @@ -1005,7 +1078,7 @@ else if ( lazyInitializer.isUninitialized() ) { if ( data.getState() == State.INITIALIZED ) { registerReloadedEntity( data ); resolveInstanceSubInitializers( data ); - if ( rowProcessingState.needsResolveState() ) { + if ( data.getState() == State.INITIALIZED && rowProcessingState.needsResolveState() ) { // We need to read result set values to correctly populate the query cache resolveEntityState( data ); } @@ -1091,7 +1164,9 @@ protected void resolveEntityInstance1(EntityInitializerData data) { else { data.setInstance( proxy ); if ( Hibernate.isInitialized( proxy ) ) { - data.setState( State.INITIALIZED ); + if ( data.entityHolder.isInitialized() ) { + data.setState( State.INITIALIZED ); + } data.entityInstanceForNotify = Hibernate.unproxy( proxy ); } else { @@ -1242,7 +1317,7 @@ protected Object resolveEntityInstance(EntityInitializerData data) { assert data.entityHolder.getEntityInitializer() == this; // If this initializer owns the entity, we have to remove the entity holder, // because the subsequent loading process will claim the entity - rowProcessingState.getJdbcValuesSourceProcessingState().getLoadingEntityHolders().remove( data.entityHolder ); + session.getPersistenceContextInternal().removeEntityHolder( data.entityKey ); return session.internalLoad( data.concreteDescriptor.getEntityName(), @@ -1330,10 +1405,55 @@ public void initializeInstance(EntityInitializerData data) { assert consistentInstance( data ); initializeEntityInstance( data ); } + else { + if ( data.getRowProcessingState().needsResolveState() ) { + // A sub-initializer might have taken responsibility for this entity, + // but we still need to resolve the state to correctly populate a query cache + resolveState( data ); + } + if ( rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && data.entityHolder.getEntityInitializer() != this + && data.entityHolder.isInitialized() ) { + updateInitializedEntityInstance( data ); + } + } data.setState( State.INITIALIZED ); } } + protected void updateInitializedEntityInstance(EntityInitializerData data) { + assert rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading(); + + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var entityEntry = data.entityHolder.getEntityEntry(); + final var loadedState = entityEntry.getLoadedState(); + final DomainResultAssembler[] concreteAssemblers = assemblers[data.concreteDescriptor.getSubclassId()]; + + for ( int i = 0; i < loadedState.length; i++ ) { + final var subInstance = loadedState[i]; + final DomainResultAssembler assembler = concreteAssemblers[i]; + if ( subInstance == UNFETCHED_PROPERTY + && assembler != null + && !(assembler instanceof UnfetchedResultAssembler) + && !(assembler instanceof UnfetchedCollectionAssembler) ) { + final var value = assembler.assemble( rowProcessingState ); + if ( value != UNFETCHED_PROPERTY ) { + loadedState[i] = value; + data.concreteDescriptor.setValue( data.entityInstanceForNotify, i, value ); + } + } + } + + final SharedSessionContractImplementor session = rowProcessingState.getSession(); + updateCaches( + data, + session, + session.getPersistenceContextInternal(), + loadedState, + entityEntry.getVersion() + ); + } + protected boolean consistentInstance(EntityInitializerData data) { final PersistenceContext persistenceContextInternal = data.getRowProcessingState().getSession().getPersistenceContextInternal(); @@ -1352,6 +1472,7 @@ protected void initializeEntityInstance(EntityInitializerData data) { final Object entityIdentifier = entityKey.getIdentifier(); final Object[] resolvedEntityState = extractConcreteTypeStateValues( data ); + rowProcessingState.getJdbcValuesSourceProcessingState().registerLoadingEntityHolder( data.entityHolder ); preLoad( data, resolvedEntityState ); final Object entityInstanceForNotify = data.entityInstanceForNotify; @@ -1642,10 +1763,11 @@ public void resolveState(EntityInitializerData data) { if ( identifierAssembler != null ) { identifierAssembler.resolveState( data.getRowProcessingState() ); } - if ( discriminatorAssembler != null ) { + if ( discriminatorAssembler != null + && (!data.shallowCached || rootEntityDescriptor.storeDiscriminatorInShallowQueryCacheLayout()) ) { discriminatorAssembler.resolveState( data.getRowProcessingState() ); } - if ( keyAssembler != null ) { + if ( !data.shallowCached ) {if ( keyAssembler != null ) { keyAssembler.resolveState( data.getRowProcessingState() ); } if ( versionAssembler != null ) { @@ -1668,7 +1790,7 @@ public void resolveState(EntityInitializerData data) { } } } - resolveEntityState( data ); + resolveEntityState( data );} } protected void resolveEntityState(EntityInitializerData data) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/FetchGraphTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/FetchGraphTest.java index 55980b7fe9b4..573b6c56103c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/FetchGraphTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/FetchGraphTest.java @@ -48,8 +48,9 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOAD_GRAPH; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Steve Ebersole @@ -674,6 +675,32 @@ public void testQueryAndDeleteEEntity(SessionFactoryScope scope) { ); } + @Test + public void testEntityGraphForExistingLazyBasic(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // Load an entity with a lazy basic attribute + var dEntity = session.find( DEntity.class, 1L ); + assertFalse( Hibernate.isPropertyInitialized( dEntity, "blob" ) ); + assertFalse( Hibernate.isPropertyInitialized( dEntity, "lazyString" ) ); + assertFalse( Hibernate.isPropertyInitialized( dEntity, "lazyStringBlobGroup" ) ); + assertTrue( Hibernate.isPropertyInitialized( dEntity, "nonLazyString" ) ); + + // Query the same entity but fetch the lazy basic + var graph = session.createEntityGraph( DEntity.class ); + graph.addAttributeNode( "lazyString" ); + session.createSelectionQuery( "where id = 1", DEntity.class) + .setHint( HINT_SPEC_LOAD_GRAPH, graph ) + .list(); + assertFalse( Hibernate.isPropertyInitialized( dEntity, "blob" ) ); + // Ensure that the lazy basic attribute was initialized by this + assertTrue( Hibernate.isPropertyInitialized( dEntity, "lazyString" ) ); + assertFalse( Hibernate.isPropertyInitialized( dEntity, "lazyStringBlobGroup" ) ); + assertTrue( Hibernate.isPropertyInitialized( dEntity, "nonLazyString" ) ); + } + ); + } + @BeforeEach public void prepareTestData(SessionFactoryScope scope) { scope.inTransaction( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/set/PersistentSetRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/set/PersistentSetRemoveTest.java new file mode 100644 index 000000000000..471ec183c256 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/set/PersistentSetRemoveTest.java @@ -0,0 +1,174 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.collection.set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +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.Test; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel(annotatedClasses = { + PersistentSetRemoveTest.Parent.class, + PersistentSetRemoveTest.Child.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19805") +public class PersistentSetRemoveTest { + + @Test + public void testRemove(SessionFactoryScope scope) { + Parent p1 = new Parent( 1L, "p1" ); + Child c1 = new Child( 1L, "c1" ); + Child c2 = new Child( 2L, "c2" ); + Child c3 = new Child( 3L, "c3" ); + p1.getChildren().add( c1 ); + c1.setParent( p1 ); + p1.getChildren().add( c2 ); + c2.setParent( p1 ); + p1.getChildren().add( c3 ); + c3.setParent( p1 ); + + scope.inTransaction( + session -> session.persist( p1 ) + ); + + scope.inTransaction( + session -> { + Child child = session.createQuery( "from Child c where c.name = :name", Child.class ) + .setParameter( "name", "c1" ) + .getSingleResult(); + + boolean removed = child.getParent().getChildren().remove( child ); + assertTrue( removed ); + } + ); + + scope.inTransaction( + session -> { + Parent parent = session.find( Parent.class, p1.getId() ); + Child childToRemove = parent.getChildren().stream() + .filter( book -> "c2".equals( book.getName() ) ) + .findFirst() + .orElseThrow(); + boolean removed = parent.getChildren().remove( childToRemove ); + assertTrue( removed ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Long id; + private String name; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private Set children; + + public Parent() { + this.children = new HashSet<>(); + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + this.children = new HashSet<>(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getChildren() { + return children; + } + + public void setChildren(Set children) { + this.children = children; + } + } + + + @Entity(name = "Child") + public static class Child { + @Id + private Long id; + private String name; + + @ManyToOne + private Parent parent; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + + @Override + public final boolean equals(Object o) { + if ( !(o instanceof Child child) ) { + return false; + } + + return Objects.equals( name, child.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + } +}