From 2c4584a49385b7e09eb148ee0dbd5bc329b240c9 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Fri, 10 Oct 2025 15:17:53 +0200 Subject: [PATCH 1/3] HHH-19857 When one of the lazy attributes is `null` the other ones may not get initialized correctly --- .../lazytoone/LazyOneToOneWithCastTest.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java new file mode 100644 index 000000000000..dc13fa4aa693 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.lazytoone; + +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.ContainingEntity.class, + LazyOneToOneWithCastTest.ContainedEntity.class, + } +) +@SessionFactory +@BytecodeEnhanced +@EnhancementOptions(lazyLoading = true) +class LazyOneToOneWithCastTest { + + @Test + void oneNullOneNotNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + ContainingEntity containingEntity1 = new ContainingEntity(); + containingEntity1.setId( 2 ); + + ContainedEntity contained1 = new ContainedEntity(); + contained1.setId( 4 ); + contained1.setContainingAsIndexedEmbeddedWithCast( containingEntity1 ); + containingEntity1.setContainedIndexedEmbeddedWithCast( contained1 ); + + session.persist( contained1 ); + session.persist( containingEntity1 ); + + } ); + + scope.inTransaction( session -> { + ContainedEntity contained = session.find( ContainedEntity.class, 4 ); + + ContainingEntity containingAsIndexedEmbedded = contained.getContainingAsIndexedEmbedded(); + assertThat( containingAsIndexedEmbedded ).isNull(); + assertThat( Hibernate.isPropertyInitialized( contained, "containingAsIndexedEmbedded" ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( contained, "containingAsIndexedEmbeddedWithCast" ) ).isFalse(); + + Object containingAsIndexedEmbeddedWithCast = contained.getContainingAsIndexedEmbeddedWithCast(); + assertThat( Hibernate.isPropertyInitialized( contained, "containingAsIndexedEmbeddedWithCast" ) ).isTrue(); + assertThat( containingAsIndexedEmbeddedWithCast ).isNotNull(); + } ); + + scope.inTransaction( session -> { + ContainedEntity contained = session.find( ContainedEntity.class, 4 ); + assertThat( contained.getContainingAsIndexedEmbeddedWithCast() ).isNotNull(); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "containing") + public static class ContainingEntity { + + @Id + private Integer id; + + @OneToOne + private ContainedEntity containedIndexedEmbedded; + + @OneToOne(targetEntity = ContainedEntity.class) + @JoinColumn(name = "CIndexedEmbeddedCast") + private ContainedEntity containedIndexedEmbeddedWithCast; + + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public ContainedEntity getContainedIndexedEmbedded() { + return containedIndexedEmbedded; + } + + public Object getContainedIndexedEmbeddedWithCast() { + return containedIndexedEmbeddedWithCast; + } + + public void setContainedIndexedEmbeddedWithCast(ContainedEntity containedIndexedEmbeddedWithCast) { + this.containedIndexedEmbeddedWithCast = containedIndexedEmbeddedWithCast; + } + + public void setContainedIndexedEmbedded(ContainedEntity containedIndexedEmbedded) { + this.containedIndexedEmbedded = containedIndexedEmbedded; + } + } + + @Entity(name = "contained") + public static class ContainedEntity { + @Id + private Integer id; + + @OneToOne(mappedBy = "containedIndexedEmbeddedWithCast", targetEntity = ContainingEntity.class, + fetch = FetchType.LAZY) + @LazyGroup("containingAsIndexedEmbeddedWithCast") + private ContainingEntity containingAsIndexedEmbeddedWithCast; + + @OneToOne(mappedBy = "containedIndexedEmbedded", fetch = FetchType.LAZY) + @LazyGroup("containingAsIndexedEmbedded") + private ContainingEntity containingAsIndexedEmbedded; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public ContainingEntity getContainingAsIndexedEmbedded() { + return containingAsIndexedEmbedded; + } + + public Object getContainingAsIndexedEmbeddedWithCast() { + return containingAsIndexedEmbeddedWithCast; + } + + public void setContainingAsIndexedEmbeddedWithCast(ContainingEntity containingAsIndexedEmbeddedWithCast) { + this.containingAsIndexedEmbeddedWithCast = containingAsIndexedEmbeddedWithCast; + } + + public void setContainingAsIndexedEmbedded(ContainingEntity containingAsIndexedEmbedded) { + this.containingAsIndexedEmbedded = containingAsIndexedEmbedded; + } + } +} From 61707e3b4c6ee4dd66c575903aea79547518e6ae Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Fri, 10 Oct 2025 15:18:07 +0200 Subject: [PATCH 2/3] HHH-19856 @LazyGroup is not always respected --- .../lazytoone/LazyOneToOneWithCastTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java index dc13fa4aa693..c759b2178b53 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/lazytoone/LazyOneToOneWithCastTest.java @@ -67,6 +67,45 @@ void oneNullOneNotNull(SessionFactoryScope scope) { } ); } + @Test + void bothNotNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + ContainingEntity containingEntity1 = new ContainingEntity(); + containingEntity1.setId( 1 ); + + ContainedEntity contained1 = new ContainedEntity(); + contained1.setId( 4 ); + contained1.setContainingAsIndexedEmbeddedWithCast( containingEntity1 ); + containingEntity1.setContainedIndexedEmbeddedWithCast( contained1 ); + + contained1.setContainingAsIndexedEmbedded( containingEntity1 ); + containingEntity1.setContainedIndexedEmbedded( contained1 ); + + + session.persist( contained1 ); + session.persist( containingEntity1 ); + + } ); + + scope.inTransaction( session -> { + ContainedEntity contained = session.find( ContainedEntity.class, 4 ); + + ContainingEntity containingAsIndexedEmbedded = contained.getContainingAsIndexedEmbedded(); + assertThat( containingAsIndexedEmbedded ).isNotNull(); + assertThat( Hibernate.isPropertyInitialized( contained, "containingAsIndexedEmbedded" ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( contained, "containingAsIndexedEmbeddedWithCast" ) ).isFalse(); + + Object containingAsIndexedEmbeddedWithCast = contained.getContainingAsIndexedEmbeddedWithCast(); + assertThat( Hibernate.isPropertyInitialized( contained, "containingAsIndexedEmbeddedWithCast" ) ).isTrue(); + assertThat( containingAsIndexedEmbeddedWithCast ).isNotNull(); + } ); + + scope.inTransaction( session -> { + ContainedEntity contained = session.find( ContainedEntity.class, 4 ); + assertThat( contained.getContainingAsIndexedEmbeddedWithCast() ).isNotNull(); + } ); + } + @AfterEach void tearDown(SessionFactoryScope scope) { scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); From 27b0110a06557522fc6085130455732dbf921e95 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Fri, 10 Oct 2025 15:20:19 +0200 Subject: [PATCH 3/3] HHH-19856 Make a copy of initializedLazyAttributeNames to prevent it mutation within the init method --- .../hibernate/persister/entity/AbstractEntityPersister.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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..7e81335112f6 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,9 @@ 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(); + // Create a copy of init attrs, since lazySelectLoadPlan.load may update the set inside the interceptor, + // and we end up with the modified one here: + final Set initializedLazyAttributeNames = new HashSet<>( interceptor.getInitializedLazyAttributeNames() ); final var lazyAttributesMetadata = getBytecodeEnhancementMetadata().getLazyAttributesMetadata(); final String fetchGroup = lazyAttributesMetadata.getFetchGroupName( fieldName );