HHH-18932 Use target table columns for SQM joins, except for join table associations

This commit is contained in:
Christian Beikov 2024-12-12 14:09:36 +01:00
parent 58d2239d85
commit 301c38bf7d
5 changed files with 42 additions and 7 deletions

View File

@ -914,6 +914,10 @@ public class ToOneAttributeMapping
return cardinality; return cardinality;
} }
public boolean hasJoinTable() {
return hasJoinTable;
}
@Override @Override
public EntityMappingType getMappedType() { public EntityMappingType getMappedType() {
return getEntityMappingType(); return getEntityMappingType();

View File

@ -34,6 +34,7 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.metamodel.model.domain.IdentifiableDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType;
@ -257,7 +258,13 @@ public class SqmUtil {
* or one that has an explicit on clause predicate. * or one that has an explicit on clause predicate.
*/ */
public static boolean isFkOptimizationAllowed(SqmPath<?> sqmPath, EntityAssociationMapping associationMapping) { public static boolean isFkOptimizationAllowed(SqmPath<?> sqmPath, EntityAssociationMapping associationMapping) {
if ( associationMapping.isFkOptimizationAllowed() && sqmPath instanceof SqmJoin<?, ?> sqmJoin ) { // By default, never allow the FK optimization if the path is a join, unless the association has a join table
// Hibernate ORM has no way for users to refer to collection/join table rows,
// so referring the columns of these rows by default when requesting FK column attributes is sensible.
// Users that need to refer to the actual target table columns will have to add an explicit entity join.
if ( associationMapping.isFkOptimizationAllowed()
&& sqmPath instanceof SqmJoin<?, ?> sqmJoin
&& hasJoinTable( associationMapping ) ) {
switch ( sqmJoin.getSqmJoinType() ) { switch ( sqmJoin.getSqmJoinType() ) {
case LEFT: case LEFT:
if ( isFiltered( associationMapping ) ) { if ( isFiltered( associationMapping ) ) {
@ -273,6 +280,16 @@ public class SqmUtil {
return false; return false;
} }
private static boolean hasJoinTable(EntityAssociationMapping associationMapping) {
if ( associationMapping instanceof CollectionPart collectionPart ) {
return !collectionPart.getCollectionAttribute().getCollectionDescriptor().isOneToMany();
}
else if ( associationMapping instanceof ToOneAttributeMapping toOneAttributeMapping ) {
return toOneAttributeMapping.hasJoinTable();
}
return false;
}
private static boolean isFiltered(EntityAssociationMapping associationMapping) { private static boolean isFiltered(EntityAssociationMapping associationMapping) {
final EntityMappingType entityMappingType = associationMapping.getAssociatedEntityMappingType(); final EntityMappingType entityMappingType = associationMapping.getAssociatedEntityMappingType();
return !associationMapping.isFkOptimizationAllowed() return !associationMapping.isFkOptimizationAllowed()

View File

@ -75,7 +75,7 @@ public class ManyToOneJoinReuseTest {
query.where( query.where(
cb.and( cb.and(
root.get( "book" ).isNotNull(), root.get( "book" ).isNotNull(),
join.isNotNull() cb.fk( root.get( "book" ) ).isNotNull()
) )
); );

View File

@ -150,8 +150,8 @@ public class InheritanceQueryGroupByTest {
Long.class Long.class
).getSingleResult(); ).getSingleResult();
assertThat( sum ).isEqualTo( 3L ); assertThat( sum ).isEqualTo( 3L );
// When not selected, group by should only use the foreign key (parent_id) // Association is joined, so every use of the join alias will make use of target table columns
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 2 ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 1 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount );
} ); } );
@ -234,8 +234,8 @@ public class InheritanceQueryGroupByTest {
Long.class Long.class
).getSingleResult(); ).getSingleResult();
assertThat( sum ).isEqualTo( 3L ); assertThat( sum ).isEqualTo( 3L );
// When not selected, group by should only use the foreign key (parent_id) // Association is joined, so every use of the join alias will make use of target table columns
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 3 ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 1 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount );
} ); } );

View File

@ -50,13 +50,27 @@ public class MapIssueTest {
} }
@Test @Test
public void testMapKeyJoinIsOmitted(SessionFactoryScope scope) { public void testMapKeyJoinIsNotOmitted(SessionFactoryScope scope) {
SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
scope.inTransaction( scope.inTransaction(
s -> { s -> {
s.createQuery( "select c from MapOwner as o join o.contents c join c.relationship r where r.id is not null" ).list(); s.createQuery( "select c from MapOwner as o join o.contents c join c.relationship r where r.id is not null" ).list();
statementInspector.assertExecutedCount( 1 ); statementInspector.assertExecutedCount( 1 );
// Assert 3 joins, collection table, collection element and collection key (relationship)
statementInspector.assertNumberOfJoins( 0, 3 );
}
);
}
@Test
public void testMapKeyJoinIsOmitted2(SessionFactoryScope scope) {
SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction(
s -> {
s.createQuery( "select c from MapOwner as o join o.contents c where c.relationship.id is not null" ).list();
statementInspector.assertExecutedCount( 1 );
// Assert 2 joins, collection table and collection element. No need to join the relationship because it is not nullable // Assert 2 joins, collection table and collection element. No need to join the relationship because it is not nullable
statementInspector.assertNumberOfJoins( 0, 2 ); statementInspector.assertNumberOfJoins( 0, 2 );
} }