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) {
|
private void applyJoinsToInferredSelectClause(SqmFrom<?,?> sqm, SqmSelectClause selectClause) {
|
||||||
sqm.visitSqmJoins( (sqmJoin) -> {
|
sqm.visitSqmJoins( (sqmJoin) -> {
|
||||||
|
if ( sqmJoin.isImplicitlySelectable() ) {
|
||||||
selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) );
|
selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) );
|
||||||
applyJoinsToInferredSelectClause( sqmJoin, selectClause );
|
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
|
@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 );
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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