diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index 4cae5391943a..cca8ee9a45c2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -246,7 +246,7 @@ public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { * or one that has an explicit on clause predicate. */ public static boolean isFkOptimizationAllowed(SqmPath sqmPath, EntityAssociationMapping associationMapping) { - if ( sqmPath instanceof SqmJoin sqmJoin ) { + if ( associationMapping.isFkOptimizationAllowed() && sqmPath instanceof SqmJoin sqmJoin ) { switch ( sqmJoin.getSqmJoinType() ) { case LEFT: if ( isFiltered( associationMapping ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 5212a591a53e..9fd2dfcfbeaa 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -4138,12 +4138,11 @@ private Expression visitTableGroup(TableGroup tableGroup, SqmPath path) { if ( inferredEntityMapping == null ) { // When the inferred mapping is null, we try to resolve to the FK by default, which is fine because // expansion to all target columns for select and group by clauses is handled in EntityValuedPathInterpretation - if ( entityValuedModelPart instanceof EntityAssociationMapping - && isFkOptimizationAllowed( path, (EntityAssociationMapping) entityValuedModelPart ) ) { + if ( entityValuedModelPart instanceof EntityAssociationMapping associationMapping + && isFkOptimizationAllowed( path, associationMapping ) ) { // If the table group uses an association mapping that is not a one-to-many, // we make use of the FK model part - unless the path is a non-optimizable join, // for which we should always use the target's identifier to preserve semantics - final EntityAssociationMapping associationMapping = (EntityAssociationMapping) entityValuedModelPart; final ModelPart targetPart = associationMapping.getForeignKeyDescriptor().getPart( associationMapping.getSideNature() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java new file mode 100644 index 000000000000..cf086f3e7b3e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria; + +import java.util.List; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + CriteriaSelectOneToOneUnownedTest.Parent.class, + CriteriaSelectOneToOneUnownedTest.Child.class, + } +) +@JiraKey("HHH-18628") +public class CriteriaSelectOneToOneUnownedTest { + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Child child = new Child( 1l, "child" ); + Parent parent = new Parent( 1l, "parent", child ); + entityManager.persist( parent ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + entityManager.createQuery( "delete from Child" ).executeUpdate(); + entityManager.createQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testCriteriaInnerJoin(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( Child.class ); + Root parent = query.from( Parent.class ); + Join child = parent.join( "child", JoinType.INNER ); + query.select( child ); + + List children = entityManager.createQuery( query ).getResultList(); + assertThat( children ).isNotNull(); + assertThat( children.size() ).isEqualTo( 1 ); + Child c = children.get( 0 ); + assertThat( c ).isNotNull(); + } ); + } + + @Test + public void testCriteriaLeftJoin(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( Child.class ); + Root parent = query.from( Parent.class ); + Join child = parent.join( "child", JoinType.LEFT ); + query.select( child ); + + List children = entityManager.createQuery( query ).getResultList(); + assertThat( children ).isNotNull(); + assertThat( children.size() ).isEqualTo( 1 ); + Child c = children.get( 0 ); + assertThat( c ).isNotNull(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Long id; + + private String name; + + @OneToOne(mappedBy = "parent", cascade = CascadeType.PERSIST) + private Child child; + + public Parent() { + } + + public Parent(Long id, String name, Child child) { + this.id = id; + this.name = name; + this.child = child; + child.parent = this; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + @OneToOne(optional = false, fetch = FetchType.LAZY) + private Parent parent; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + } + +}