HHH-16955 - Better define how joins are handled with implicit Query select clause
This commit is contained in:
parent
3e1411f6c0
commit
fca25adde9
|
@ -1209,8 +1209,10 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> 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 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,11 @@ public class SqmPluralPartJoin<O,T> extends AbstractSqmJoin<O,T> implements SqmQ
|
|||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImplicitlySelectable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmPluralPartJoin<O, T> copy(SqmCopyContext context) {
|
||||
final SqmPluralPartJoin<O, T> existing = context.getCopy( this );
|
||||
|
|
|
@ -24,6 +24,11 @@ public interface SqmAttributeJoin<O,T> extends SqmQualifiedJoin<O,T>, JpaFetch<O
|
|||
@Override
|
||||
SqmFrom<?,O> getLhs();
|
||||
|
||||
@Override
|
||||
default boolean isImplicitlySelectable() {
|
||||
return !isFetched();
|
||||
}
|
||||
|
||||
@Override
|
||||
SqmPathSource<T> getReferencedPathSource();
|
||||
|
||||
|
|
|
@ -55,6 +55,11 @@ public class SqmCrossJoin<T> extends AbstractSqmFrom<T, T> implements JpaCrossJo
|
|||
this.sqmRoot = sqmRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImplicitlySelectable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmCrossJoin<T> copy(SqmCopyContext context) {
|
||||
final SqmCrossJoin<T> existing = context.getCopy( this );
|
||||
|
|
|
@ -61,6 +61,11 @@ public class SqmCteJoin<T> extends AbstractSqmQualifiedJoin<T, T> implements Jpa
|
|||
this.cte = cte;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImplicitlySelectable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmCteJoin<T> copy(SqmCopyContext context) {
|
||||
final SqmCteJoin<T> existing = context.getCopy( this );
|
||||
|
|
|
@ -85,6 +85,11 @@ public class SqmDerivedJoin<T> extends AbstractSqmQualifiedJoin<T, T> implements
|
|||
return joinType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImplicitlySelectable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmDerivedJoin<T> copy(SqmCopyContext context) {
|
||||
final SqmDerivedJoin<T> existing = context.getCopy( this );
|
||||
|
|
|
@ -59,6 +59,11 @@ public class SqmEntityJoin<T> extends AbstractSqmQualifiedJoin<T, T> implements
|
|||
this.sqmRoot = sqmRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImplicitlySelectable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmEntityJoin<T> copy(SqmCopyContext context) {
|
||||
final SqmEntityJoin<T> existing = context.getCopy( this );
|
||||
|
|
|
@ -15,8 +15,16 @@ import org.hibernate.query.sqm.tree.SqmJoinType;
|
|||
* @author Steve Ebersole
|
||||
*/
|
||||
public interface SqmJoin<O,T> extends SqmFrom<O,T> {
|
||||
/**
|
||||
* 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
|
||||
<X, Y> SqmAttributeJoin<X, Y> join(String attributeName);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue