diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 5706924d97..a10d1fb07e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -1209,8 +1209,10 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem private void applyJoinsToInferredSelectClause(SqmFrom sqm, SqmSelectClause selectClause) { sqm.visitSqmJoins( (sqmJoin) -> { - selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) ); - applyJoinsToInferredSelectClause( sqmJoin, selectClause ); + if ( sqmJoin.isImplicitlySelectable() ) { + selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) ); + applyJoinsToInferredSelectClause( sqmJoin, selectClause ); + } } ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralPartJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralPartJoin.java index 5905acc396..1f1b04a364 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralPartJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralPartJoin.java @@ -58,6 +58,11 @@ public class SqmPluralPartJoin extends AbstractSqmJoin implements SqmQ ); } + @Override + public boolean isImplicitlySelectable() { + return false; + } + @Override public SqmPluralPartJoin copy(SqmCopyContext context) { final SqmPluralPartJoin existing = context.getCopy( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java index ad2e5a2884..0ce76f7b8e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java @@ -24,6 +24,11 @@ public interface SqmAttributeJoin extends SqmQualifiedJoin, JpaFetch getLhs(); + @Override + default boolean isImplicitlySelectable() { + return !isFetched(); + } + @Override SqmPathSource getReferencedPathSource(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java index 267b2f2e02..08c786bec1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java @@ -55,6 +55,11 @@ public class SqmCrossJoin extends AbstractSqmFrom implements JpaCrossJo this.sqmRoot = sqmRoot; } + @Override + public boolean isImplicitlySelectable() { + return true; + } + @Override public SqmCrossJoin copy(SqmCopyContext context) { final SqmCrossJoin existing = context.getCopy( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java index 50554862b0..8f392bc896 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java @@ -61,6 +61,11 @@ public class SqmCteJoin extends AbstractSqmQualifiedJoin implements Jpa this.cte = cte; } + @Override + public boolean isImplicitlySelectable() { + return false; + } + @Override public SqmCteJoin copy(SqmCopyContext context) { final SqmCteJoin existing = context.getCopy( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java index 7c5c7bad1a..bb533a0840 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java @@ -85,6 +85,11 @@ public class SqmDerivedJoin extends AbstractSqmQualifiedJoin implements return joinType; } + @Override + public boolean isImplicitlySelectable() { + return false; + } + @Override public SqmDerivedJoin copy(SqmCopyContext context) { final SqmDerivedJoin existing = context.getCopy( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java index a611c2fd53..d42d5a7041 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java @@ -59,6 +59,11 @@ public class SqmEntityJoin extends AbstractSqmQualifiedJoin implements this.sqmRoot = sqmRoot; } + @Override + public boolean isImplicitlySelectable() { + return true; + } + @Override public SqmEntityJoin copy(SqmCopyContext context) { final SqmEntityJoin existing = context.getCopy( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmJoin.java index a7b2afa4ee..07ec2925ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmJoin.java @@ -15,8 +15,16 @@ import org.hibernate.query.sqm.tree.SqmJoinType; * @author Steve Ebersole */ public interface SqmJoin extends SqmFrom { + /** + * The type of join - inner, cross, etc + */ SqmJoinType getSqmJoinType(); + /** + * When applicable, whether this join should be included in an implicit select clause + */ + boolean isImplicitlySelectable(); + @Override SqmAttributeJoin join(String attributeName); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinResultTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinResultTests.java new file mode 100644 index 0000000000..bc056569d5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinResultTests.java @@ -0,0 +1,143 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.query.joinfetch; + +import java.util.UUID; + +import org.hibernate.internal.util.StringHelper; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.retail.Product; +import org.hibernate.testing.orm.domain.retail.Vendor; +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.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests covering how joins and return-type affect the "shape" of the domain result + * + * @author Steve Ebersole + */ +@DomainModel(standardModels = StandardDomainModel.RETAIL) +@SessionFactory(useCollectingStatementInspector = true) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16955" ) +public class JoinResultTests { + @Test + void testSimpleJoin(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + final String query = "from Product join vendor"; + + // supports `Product.class` - the single root + scope.inTransaction( (session) -> { + statementInspector.clear(); + final Product product = session + .createQuery( query, Product.class ) + .getSingleResult(); + assertThat( product ).isNotNull(); + // only the Product columns should be selected + assertThat( extractNumberOfSelections( statementInspector.getSqlQueries().get( 0 ) ) ).isEqualTo( 5 ); + } ); + + // supports `Object[].class` - array[0] == the root && array[1] == the join + scope.inTransaction( (session) -> { + statementInspector.clear(); + final Object[] result = session + .createQuery( query, Object[].class ) + .getSingleResult(); + assertThat( result ).isNotNull(); + assertThat( result.length ).isEqualTo( 2 ); + // both the Product and Vendor columns should be selected + assertThat( extractNumberOfSelections( statementInspector.getSqlQueries().get( 0 ) ) ).isGreaterThan( 5 ); + assertThat( ( (Product) result[0] ).getVendor() ).isSameAs( result[1] ); + } ); + } + + @Test + void testSimpleJoinFetch(SessionFactoryScope scope) { + final String query = "from Product join fetch vendor"; + + // supports `Product.class` - the single root + scope.inTransaction( (session) -> { + final Product product = session + .createQuery( query, Product.class ) + .getSingleResult(); + assertThat( product ).isNotNull(); + } ); + + // supports `Object[].class` - array[0] == the root + scope.inTransaction( (session) -> { + final Object[] result = session + .createQuery( query, Object[].class ) + .getSingleResult(); + assertThat( result ).isNotNull(); + assertThat( result.length ).isEqualTo( 1 ); + } ); + } + + @Test + void testSimpleCrossJoin(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + final String query = "from Product cross join vendor"; + + // supports `Product.class` - the single root + scope.inTransaction( (session) -> { + statementInspector.clear(); + final Product product = session + .createQuery( query, Product.class ) + .getSingleResult(); + assertThat( product ).isNotNull(); + // both the Product and Vendor columns should be selected + assertThat( extractNumberOfSelections( statementInspector.getSqlQueries().get( 0 ) ) ).isEqualTo( 5 ); + } ); + + // supports `Object[].class` - array[0] == the root && array[1] == the join + scope.inTransaction( (session) -> { + statementInspector.clear(); + final Object[] result = session + .createQuery( query, Object[].class ) + .getSingleResult(); + assertThat( result ).isNotNull(); + assertThat( result.length ).isEqualTo( 2 ); + // both the Product and Vendor columns should be selected + assertThat( extractNumberOfSelections( statementInspector.getSqlQueries().get( 0 ) ) ).isGreaterThan( 5 ); + assertThat( ( (Product) result[0] ).getVendor() ).isSameAs( result[1] ); + } ); + } + + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Vendor vendor = new Vendor( 1, "ACME", "acme", "Some notes" ); + final Product product = new Product( 1, UUID.randomUUID(), vendor ); + session.persist( vendor ); + session.persist( product ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createMutationQuery( "delete Product" ).executeUpdate(); + session.createMutationQuery( "delete Vendor" ).executeUpdate(); + } ); + } + + private static int extractNumberOfSelections(String sql) { + final int fromClauseStartPosition = sql.indexOf( " from " ); + final String selectClause = sql.substring( 0, fromClauseStartPosition ); + return StringHelper.count( selectClause, "," ) + 1; + } +}