From c9659fb9d971ef2ad0ca9d55664c6eb7637a6192 Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Mon, 3 Nov 2025 16:59:29 +0100 Subject: [PATCH 1/2] HHH-19905 Add test for issue --- .../query/hql/ImplicitNestedJoinTest.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java new file mode 100644 index 000000000000..9ffcf25827dd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java @@ -0,0 +1,141 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +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.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = { + ImplicitNestedJoinTest.RootEntity.class, + ImplicitNestedJoinTest.FirstLevelReferencedEntity.class, + ImplicitNestedJoinTest.SecondLevelReferencedEntityA.class, + ImplicitNestedJoinTest.SecondLevelReferencedEntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19905") +public class ImplicitNestedJoinTest { + + @Test + public void testInnerAndLeftJoin(SessionFactoryScope scope) { + scope.inSession( session -> { + final var resultList = session.createQuery( + "select r.id from RootEntity r" + + " join r.firstLevelReference.secondLevelReferenceA sa " + + " left join r.firstLevelReference.secondLevelReferenceB sb", + Long.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ).containsExactlyInAnyOrder( 1L, 2L ); + } ); + } + + @Test + public void testLeftAndInnerJoin(SessionFactoryScope scope) { + scope.inSession( session -> { + final var resultList = session.createQuery( + "select r.id from RootEntity r" + + " left join r.firstLevelReference.secondLevelReferenceA sa " + + " join r.firstLevelReference.secondLevelReferenceB sb", + Long.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ).containsExactly( 1L ); + } ); + } + + @Test + public void testBothInnerJoins(SessionFactoryScope scope) { + scope.inSession( session -> { + final var resultList = session.createQuery( + "select r.id from RootEntity r" + + " join r.firstLevelReference.secondLevelReferenceA sa " + + " join r.firstLevelReference.secondLevelReferenceB sb", + Long.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ).containsExactly( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // create some test data : one first level reference with both second level references, and + // another first level reference with only one second level reference + SecondLevelReferencedEntityA secondLevelA = new SecondLevelReferencedEntityA(); + secondLevelA.id = 1L; + secondLevelA.name = "Second Level A"; + session.persist( secondLevelA ); + SecondLevelReferencedEntityB secondLevelB = new SecondLevelReferencedEntityB(); + secondLevelB.id = 1L; + session.persist( secondLevelB ); + FirstLevelReferencedEntity firstLevel1 = new FirstLevelReferencedEntity(); + firstLevel1.id = 1L; + firstLevel1.secondLevelReferenceA = secondLevelA; + firstLevel1.secondLevelReferenceB = secondLevelB; + session.persist( firstLevel1 ); + RootEntity root1 = new RootEntity(); + root1.id = 1L; + root1.firstLevelReference = firstLevel1; + session.persist( root1 ); + FirstLevelReferencedEntity firstLevel2 = new FirstLevelReferencedEntity(); + firstLevel2.id = 2L; + firstLevel2.secondLevelReferenceA = secondLevelA; + session.persist( firstLevel2 ); + RootEntity root2 = new RootEntity(); + root2.id = 2L; + root2.firstLevelReference = firstLevel2; + session.persist( root2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "RootEntity") + public static class RootEntity { + @Id + private Long id; + + @ManyToOne + private FirstLevelReferencedEntity firstLevelReference; + } + + @Entity(name = "FirstLevelReferencedEntity") + public static class FirstLevelReferencedEntity { + @Id + private Long id; + @ManyToOne + private SecondLevelReferencedEntityA secondLevelReferenceA; + @ManyToOne + private SecondLevelReferencedEntityB secondLevelReferenceB; + + } + + @Entity(name = "SecondLevelReferencedEntityA") + public static class SecondLevelReferencedEntityA { + @Id + private Long id; + private String name; + } + + @Entity(name = "SecondLevelReferencedEntityB") + public static class SecondLevelReferencedEntityB { + @Id + private Long id; + } +} From ca08ba0ea9b63de51a24b0435fbdd19836379a55 Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Mon, 17 Nov 2025 17:06:53 +0100 Subject: [PATCH 2/2] HHH-19905 Re-use existing implicit joins regardless of type --- .../query/hql/internal/QualifiedJoinPathConsumer.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index 8cd9b83edaaa..83c66cb01139 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java @@ -203,12 +203,11 @@ private AttributeJoinDelegate resolveAlias(String identifier, boolean isTerminal if ( allowReuse ) { if ( !isTerminal ) { for ( SqmJoin sqmJoin : lhs.getSqmJoins() ) { - // In order for an HQL join to be reusable, is must have the same path source, + // In order for an HQL join to be reusable, it must have the same path source, if ( sqmJoin.getModel() == subPathSource - // must not have a join condition - && sqmJoin.getJoinPredicate() == null - // and the same join type - && sqmJoin.getSqmJoinType() == joinType ) { + // and must not have a join condition. + && sqmJoin.getJoinPredicate() == null ) { + // We explicitly allow reusing implicit joins of any type return sqmJoin; } }