HHH-16955 - Better define how joins are handled with implicit Query select clause

This commit is contained in:
Steve Ebersole 2023-07-19 16:53:54 -05:00
parent 3e1411f6c0
commit fca25adde9
9 changed files with 185 additions and 2 deletions

View File

@ -1209,8 +1209,10 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
private void applyJoinsToInferredSelectClause(SqmFrom<?,?> sqm, SqmSelectClause selectClause) { private void applyJoinsToInferredSelectClause(SqmFrom<?,?> sqm, SqmSelectClause selectClause) {
sqm.visitSqmJoins( (sqmJoin) -> { sqm.visitSqmJoins( (sqmJoin) -> {
selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) ); if ( sqmJoin.isImplicitlySelectable() ) {
applyJoinsToInferredSelectClause( sqmJoin, selectClause ); selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) );
applyJoinsToInferredSelectClause( sqmJoin, selectClause );
}
} ); } );
} }

View File

@ -58,6 +58,11 @@ public class SqmPluralPartJoin<O,T> extends AbstractSqmJoin<O,T> implements SqmQ
); );
} }
@Override
public boolean isImplicitlySelectable() {
return false;
}
@Override @Override
public SqmPluralPartJoin<O, T> copy(SqmCopyContext context) { public SqmPluralPartJoin<O, T> copy(SqmCopyContext context) {
final SqmPluralPartJoin<O, T> existing = context.getCopy( this ); final SqmPluralPartJoin<O, T> existing = context.getCopy( this );

View File

@ -24,6 +24,11 @@ public interface SqmAttributeJoin<O,T> extends SqmQualifiedJoin<O,T>, JpaFetch<O
@Override @Override
SqmFrom<?,O> getLhs(); SqmFrom<?,O> getLhs();
@Override
default boolean isImplicitlySelectable() {
return !isFetched();
}
@Override @Override
SqmPathSource<T> getReferencedPathSource(); SqmPathSource<T> getReferencedPathSource();

View File

@ -55,6 +55,11 @@ public class SqmCrossJoin<T> extends AbstractSqmFrom<T, T> implements JpaCrossJo
this.sqmRoot = sqmRoot; this.sqmRoot = sqmRoot;
} }
@Override
public boolean isImplicitlySelectable() {
return true;
}
@Override @Override
public SqmCrossJoin<T> copy(SqmCopyContext context) { public SqmCrossJoin<T> copy(SqmCopyContext context) {
final SqmCrossJoin<T> existing = context.getCopy( this ); final SqmCrossJoin<T> existing = context.getCopy( this );

View File

@ -61,6 +61,11 @@ public class SqmCteJoin<T> extends AbstractSqmQualifiedJoin<T, T> implements Jpa
this.cte = cte; this.cte = cte;
} }
@Override
public boolean isImplicitlySelectable() {
return false;
}
@Override @Override
public SqmCteJoin<T> copy(SqmCopyContext context) { public SqmCteJoin<T> copy(SqmCopyContext context) {
final SqmCteJoin<T> existing = context.getCopy( this ); final SqmCteJoin<T> existing = context.getCopy( this );

View File

@ -85,6 +85,11 @@ public class SqmDerivedJoin<T> extends AbstractSqmQualifiedJoin<T, T> implements
return joinType; return joinType;
} }
@Override
public boolean isImplicitlySelectable() {
return false;
}
@Override @Override
public SqmDerivedJoin<T> copy(SqmCopyContext context) { public SqmDerivedJoin<T> copy(SqmCopyContext context) {
final SqmDerivedJoin<T> existing = context.getCopy( this ); final SqmDerivedJoin<T> existing = context.getCopy( this );

View File

@ -59,6 +59,11 @@ public class SqmEntityJoin<T> extends AbstractSqmQualifiedJoin<T, T> implements
this.sqmRoot = sqmRoot; this.sqmRoot = sqmRoot;
} }
@Override
public boolean isImplicitlySelectable() {
return true;
}
@Override @Override
public SqmEntityJoin<T> copy(SqmCopyContext context) { public SqmEntityJoin<T> copy(SqmCopyContext context) {
final SqmEntityJoin<T> existing = context.getCopy( this ); final SqmEntityJoin<T> existing = context.getCopy( this );

View File

@ -15,8 +15,16 @@ import org.hibernate.query.sqm.tree.SqmJoinType;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public interface SqmJoin<O,T> extends SqmFrom<O,T> { public interface SqmJoin<O,T> extends SqmFrom<O,T> {
/**
* The type of join - inner, cross, etc
*/
SqmJoinType getSqmJoinType(); SqmJoinType getSqmJoinType();
/**
* When applicable, whether this join should be included in an implicit select clause
*/
boolean isImplicitlySelectable();
@Override @Override
<X, Y> SqmAttributeJoin<X, Y> join(String attributeName); <X, Y> SqmAttributeJoin<X, Y> join(String attributeName);

View File

@ -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;
}
}