From e04b8f3c1f913631961975604c8ce4cd3b36ad8f Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Fri, 10 Oct 2025 15:17:53 +0200 Subject: [PATCH 1/2] HHH-19857 Add test for issue --- .../lazy/LazyOneToOneWithCastTest.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyOneToOneWithCastTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyOneToOneWithCastTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyOneToOneWithCastTest.java new file mode 100644 index 000000000000..68e9fedfe6c7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyOneToOneWithCastTest.java @@ -0,0 +1,166 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.lazy; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import org.hibernate.Hibernate; +import org.hibernate.annotations.LazyGroup; +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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + LazyOneToOneWithCastTest.EntityA.class, + LazyOneToOneWithCastTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced +@EnhancementOptions(lazyLoading = true) +public class LazyOneToOneWithCastTest { + + @Test + void oneNullOneNotNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA entityA = new EntityA(); + entityA.setId( 1 ); + + final EntityB entityB = new EntityB(); + entityB.setId( 2 ); + entityB.setToOneWithCast( entityA ); + entityA.setToOneWithCast( entityB ); + entityB.setLazyString( "lazy" ); + + session.persist( entityB ); + session.persist( entityA ); + + } ); + + scope.inTransaction( session -> { + final EntityB entityB = session.find( EntityB.class, 2 ); + + EntityA toOne = entityB.getToOne(); + assertThat( toOne ).isNull(); + assertThat( Hibernate.isPropertyInitialized( entityB, "toOne" ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( entityB, "toOneWithCast" ) ).isFalse(); + assertThat( Hibernate.isPropertyInitialized( entityB, "lazyString" ) ).isFalse(); + + // update lazy-basic property in same lazy-group + entityB.setLazyString( "lazy_updated" ); + assertThat( Hibernate.isPropertyInitialized( entityB, "lazyString" ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( entityB, "toOneWithCast" ) ).isFalse(); + + // now access the lazy to-one with cast + final EntityA toOneWithCast = entityB.getToOneWithCast(); + assertThat( Hibernate.isPropertyInitialized( entityB, "toOneWithCast" ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( entityB, "lazyString" ) ).isTrue(); + assertThat( toOneWithCast ).isNotNull().extracting( EntityA::getId ).isEqualTo( 1 ); + assertThat( entityB.getLazyString() ).isEqualTo( "lazy_updated" ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Integer id; + + @OneToOne + @JoinColumn(name = "entity_b_id") + private EntityB toOne; + + @OneToOne(targetEntity = EntityB.class) + @JoinColumn(name = "entity_b_id_cast") + private EntityB toOneWithCast; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public EntityB getToOne() { + return toOne; + } + + public void setToOne(EntityB containedIndexedEmbedded) { + this.toOne = containedIndexedEmbedded; + } + + public Object getToOneWithCast() { + return toOneWithCast; + } + + public void setToOneWithCast(EntityB containedIndexedEmbeddedWithCast) { + this.toOneWithCast = containedIndexedEmbeddedWithCast; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Integer id; + + @OneToOne(mappedBy = "toOne", fetch = FetchType.LAZY) + @LazyGroup("toOneGroup") + private EntityA toOne; + + @OneToOne(mappedBy = "toOneWithCast", targetEntity = EntityA.class, fetch = FetchType.LAZY) + @LazyGroup("toOneWithCastGroup") + private EntityA toOneWithCast; + + @Basic(fetch = FetchType.LAZY) + @LazyGroup("toOneWithCastGroup") + private String lazyString; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public EntityA getToOne() { + return toOne; + } + + public void setToOne(EntityA toOne) { + this.toOne = toOne; + } + + public EntityA getToOneWithCast() { + return toOneWithCast; + } + + public void setToOneWithCast(EntityA toOneWithCast) { + this.toOneWithCast = toOneWithCast; + } + + public String getLazyString() { + return lazyString; + } + + public void setLazyString(String lazyString) { + this.lazyString = lazyString; + } + } +} From 9b2dbb881db328fde2c22e41d4ce8978895ce726 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Fri, 10 Oct 2025 15:20:19 +0200 Subject: [PATCH 2/2] HHH-19857 Account for nested initialization when loading lazy property --- .../entity/AbstractEntityPersister.java | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) 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 488239b5cba6..396f98b972a2 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 @@ -1605,7 +1605,6 @@ private Object initLazyProperties( final var interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); assert interceptor != null : "Expecting bytecode interceptor to be non-null"; - final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); final var lazyAttributesMetadata = getBytecodeEnhancementMetadata().getLazyAttributesMetadata(); final String fetchGroup = lazyAttributesMetadata.getFetchGroupName( fieldName ); @@ -1615,29 +1614,32 @@ private Object initLazyProperties( try { Object finalResult = null; final Object[] results = lazySelectLoadPlan.load( id, session ); + final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); int i = 0; for ( var fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { final String attributeName = fetchGroupAttributeDescriptor.getName(); - final boolean previousInitialized = initializedLazyAttributeNames.contains( attributeName ); - if ( previousInitialized ) { - // it's already been initialized (e.g. by a write) so we don't want to overwrite - i++; - // TODO: we should consider un-marking an attribute as dirty based on the selected value - // - we know the current value: - // getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); - // - we know the selected value (see selectedValue below) - // - we can use the attribute Type to tell us if they are the same - // - assuming entity is a SelfDirtinessTracker we can also know if the attribute is currently - // considered dirty, and if really not dirty we would do the un-marking - // - of course that would mean a new method on SelfDirtinessTracker to allow un-marking + if ( fieldName.equals( attributeName ) ) { + finalResult = results[i]; } - else { - final Object result = results[i++]; - if ( initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor, result ) ) { - finalResult = result; - interceptor.attributeInitialized( attributeName ); - } + if ( !initializedLazyAttributeNames.contains( attributeName ) ) { + initializeLazyProperty( + entity, + entry, + results[i], + getPropertyIndex( attributeName ), + fetchGroupAttributeDescriptor.getType() + ); } + // if the attribute has already been initialized (e.g. by a write) we don't want to overwrite + i++; + // TODO: we should consider un-marking an attribute as dirty based on the selected value + // - we know the current value: + // getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); + // - we know the selected value (see selectedValue below) + // - we can use the attribute Type to tell us if they are the same + // - assuming entity is a SelfDirtinessTracker we can also know if the attribute is currently + // considered dirty, and if really not dirty we would do the un-marking + // - of course that would mean a new method on SelfDirtinessTracker to allow un-marking } CORE_LOGGER.doneInitializingLazyProperties(); return finalResult; @@ -1755,7 +1757,11 @@ private Object copiedLazyPropertyValue(int index, Object propValue) { return lazyPropertyTypes[index].deepCopy( propValue, factory ); } - // Used by Hibernate Reactive + /** + * Used by Hibernate Reactive + * @deprecated + */ + @Deprecated(since = "7.2", forRemoval = true) protected boolean initializeLazyProperty( final String fieldName, final Object entity,