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 8e2554586ecb..6b6ce4105c80 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 @@ -405,7 +405,10 @@ public EntityHolder claimEntityHolderIfPossible( else { newEntityHolder = null; } - holder.entityInitializer = initializer; + if ( holder.processingState != processingState ) { + holder.entityInitializer = initializer; + holder.processingState = processingState; + } return holder; } @@ -2174,7 +2177,8 @@ private static class EntityHolderImpl implements EntityHolder, Serializable { private Object entity; private Object proxy; private @Nullable EntityEntry entityEntry; - private EntityInitializer entityInitializer; + private @Nullable EntityInitializer entityInitializer; + private @Nullable JdbcValuesSourceProcessingState processingState; private EntityHolderState state; private EntityHolderImpl() { @@ -2212,10 +2216,15 @@ public Object getProxy() { } @Override - public EntityInitializer getEntityInitializer() { + public @Nullable EntityInitializer getEntityInitializer() { return entityInitializer; } + @Override + public @Nullable JdbcValuesSourceProcessingState getJdbcValuesProcessingState() { + return processingState; + } + @Override public void markAsReloaded(JdbcValuesSourceProcessingState processingState) { processingState.registerReloadedEntityHolder( this ); @@ -2237,8 +2246,9 @@ public boolean isDetached() { } @Override - public void resetEntityInitialier(){ + public void resetEntityInitialier() { entityInitializer = null; + processingState = null; } public EntityHolderImpl withEntity(EntityKey entityKey, EntityPersister descriptor, Object entity) { @@ -2253,6 +2263,7 @@ public EntityHolderImpl withData(EntityKey entityKey, EntityPersister descriptor assert entityKey != null && descriptor != null && entityInitializer == null + && processingState == null && state == EntityHolderState.UNINITIALIZED; this.entityKey = entityKey; this.descriptor = descriptor; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java index b631f2610ab0..5aef737fd40a 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java @@ -35,6 +35,11 @@ public interface EntityHolder { * Will be {@code null} if entity is initialized already or the entity holder is not claimed yet. */ @Nullable EntityInitializer getEntityInitializer(); + /** + * The {@link JdbcValuesSourceProcessingState} for the entity initializer that claims to initialize the entity for this holder. + * Will be {@code null} if entity is initialized already or the entity holder is not claimed yet. + */ + @Nullable JdbcValuesSourceProcessingState getJdbcValuesProcessingState(); /** * The proxy if there is one and otherwise the entity. 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 a35cc96f4eec..dc1b31f55bf0 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 @@ -1187,7 +1187,12 @@ else if ( isResultInitializer() ) { } } else if ( entityHolder.getEntityInitializer() != this ) { - data.setState( State.INITIALIZED ); + // The other initializer will take care of initialization + if ( !hasLazyInitializingSubAssemblers ) { + // but we can only skip the initialization phase of this initializer, + // if this initializer does not initialize lazy basic attributes + data.setState( State.INITIALIZED ); + } } else if ( data.shallowCached ) { // For shallow cached entities, only the id is available, so ensure we load the data immediately 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 f714c945312f..bee4967d5e19 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 @@ -204,8 +204,10 @@ protected void initialize(EntitySelectFetchInitializerData data) { } } else if ( holder.getEntityInitializer() != this ) { - // the entity is already being loaded elsewhere - data.setState( State.INITIALIZED ); + // the entity is already being loaded elsewhere in this processing level + if ( holder.getJdbcValuesProcessingState() == rowProcessingState.getJdbcValuesSourceProcessingState() ) { + data.setState( State.INITIALIZED ); + } return; } else if ( data.getInstance() == null ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubselectFetch2Test.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubselectFetch2Test.java new file mode 100644 index 000000000000..204378c07788 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubselectFetch2Test.java @@ -0,0 +1,228 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static jakarta.persistence.CascadeType.PERSIST; +import static jakarta.persistence.FetchType.EAGER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.annotations.FetchMode.JOIN; +import static org.hibernate.annotations.FetchMode.SUBSELECT; + +@DomainModel( + annotatedClasses = { + SubselectFetch2Test.NodeHolder.class, + SubselectFetch2Test.NodeIntermediateHolder.class, + SubselectFetch2Test.Element.class, + SubselectFetch2Test.Node.class, + } +) +@SessionFactory +public class SubselectFetch2Test { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Node basik = new Node( "Child" ); + Element n1, n2; + basik.elements.add( n1 = new Element( basik ) ); + basik.elements.add( n2 = new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + + Node node2 = new Node( "Child2" ); + node2.parent = new NodeIntermediateHolder( basik ); + node2.parent.strings = new ArrayList<>( Arrays.asList("s1", "s2")); + node2.elements.add( n1 ); + node2.elements.add( n2 ); + node2.elements.add( new Element( basik ) ); + + NodeHolder root = new NodeHolder( basik, node2 ); + session.persist( root ); + + session.persist( new NodeHolder( null, null ) ); + } + ); + } + + @AfterEach + @JiraKey("HHH-19868") + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + void test2(SessionFactoryScope scope) { + scope.inTransaction( session -> { + NodeHolder holder = session.createSelectionQuery( "from NodeHolder nh join fetch nh.node1 join fetch nh.node2", NodeHolder.class ).getSingleResult(); + assertThat( holder.node1.elements ).hasSize( 3 ); + assertThat( holder.node2.elements ).hasSize( 3 ); + } ); + } + + @Test + void test3(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createSelectionQuery( "select nh, n1, n2 from NodeHolder nh join nh.node1 n1 join nh.node2 n2" ).getResultList(); + } ); + } + + @Entity(name = "NodeHolder") + public static class NodeHolder { + @Id + @GeneratedValue + Integer id; + + @ManyToOne(cascade = PERSIST) + Node node1; + @ManyToOne(cascade = PERSIST) + Node node2; + + public NodeHolder(Node node1, Node node2) { + this.node1 = node1; + this.node2 = node2; + } + + public NodeHolder() { + } + } + + + @Entity(name = "NodeIntermediateHolder") + public static class NodeIntermediateHolder { + @Id + @GeneratedValue + Integer id; + + @ManyToOne(cascade = PERSIST) + Node node; + + @ElementCollection(fetch = EAGER) + @Fetch( JOIN ) + List strings; + + public NodeIntermediateHolder(Node node) { + this.node = node; + } + + public NodeIntermediateHolder() { + } + } + + @Entity(name = "Element") + @Table(name = "Element") + public static class Element { + @Id + @GeneratedValue + Integer id; + + @ManyToOne + @Fetch(FetchMode.SELECT) + Node node; + + public Element(Node node) { + this.node = node; + } + + public Element() { + } + } + + @Entity(name = "Node") + @Table(name = "Node") + public static class Node { + + @Id + @GeneratedValue + Integer id; + @Version + Integer version; + String string; + @Transient + boolean loaded = false; + + @ManyToOne(fetch = EAGER, cascade = PERSIST) + NodeIntermediateHolder parent; + + @ManyToMany(fetch = EAGER, cascade = PERSIST) + @Fetch(SUBSELECT) + Set elements = new HashSet<>(); + + public Node(String string) { + this.string = string; + } + + public Node() { + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + +// @PostLoad +// void postLoad() { +// loaded = true; +// } + + @Override + public String toString() { + return id + ": " + string; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Node node = (Node) o; + return Objects.equals( string, node.string ); + } + + @Override + public int hashCode() { + return Objects.hash( string ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubselectFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubselectFetchTest.java new file mode 100644 index 000000000000..37eff2356e1c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubselectFetchTest.java @@ -0,0 +1,176 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; +import org.hibernate.Hibernate; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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 java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.CascadeType.PERSIST; +import static jakarta.persistence.FetchType.EAGER; +import static jakarta.persistence.FetchType.LAZY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.annotations.FetchMode.SUBSELECT; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + SubselectFetchTest.Element.class, + SubselectFetchTest.Node.class, + } +) +@SessionFactory +public class SubselectFetchTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Node basik = new Node( "Child" ); + basik.parent = new Node( "Parent" ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + + session.persist( basik ); + } + ); + } + + @AfterEach + @JiraKey("HHH-19868") + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + void testEagerFetchQuery(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List list = session.createSelectionQuery( "from Node order by id", Node.class ).getResultList(); + assertThat( list ).hasSize( 2 ); + assertThat( Hibernate.isInitialized( list.get( 0 ).elements ) ).isTrue(); + assertThat( list.get( 0 ).elements ).hasSize( 3 ); + assertThat( list.get( 1 ).elements ).isEmpty(); + } ); + + scope.inTransaction( session -> { + List list = session.createSelectionQuery( "select distinct n, e from Node n join n.elements e order by n.id", Object[].class ).getResultList(); + assertThat( list ).hasSize( 3 ); + Object[] tup = list.get( 0 ); + assertTrue( Hibernate.isInitialized( ( (Node) tup[0] ).elements ) ); + assertThat( ( (Node) tup[0] ).elements ).hasSize( 3 ); + } ); + } + + + @Entity(name = "Element") + @Table(name = "Element") + public static class Element { + @Id + @GeneratedValue + Integer id; + + @ManyToOne + @Fetch(FetchMode.SELECT) + Node node; + + public Element(Node node) { + this.node = node; + } + + public Element() { + } + } + + @Entity(name = "Node") + @Table(name = "Node") + public static class Node { + + @Id + @GeneratedValue + Integer id; + @Version + Integer version; + String string; + @Transient + boolean loaded = false; + + @ManyToOne(fetch = LAZY, cascade = PERSIST) + Node parent; + + @OneToMany(fetch = EAGER, cascade = PERSIST , mappedBy = "node") + @Fetch(SUBSELECT) + List elements = new ArrayList<>(); + + public Node(String string) { + this.string = string; + } + + public Node() { + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + +// @PostLoad +// void postLoad() { +// loaded = true; +// } + + @Override + public String toString() { + return id + ": " + string; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Node node = (Node) o; + return Objects.equals( string, node.string ); + } + + @Override + public int hashCode() { + return Objects.hash( string ); + } + } +}