HHH-17830 Fix rendering custom predicate for element collection joins

This commit is contained in:
Christian Beikov 2024-03-15 13:43:00 +01:00
parent e3f069a5cd
commit fcf722a9f8
5 changed files with 89 additions and 27 deletions

View File

@ -48,6 +48,7 @@ import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.spi.EntityIdentifierNavigablePath;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.internal.TableGroupJoinHelper;
import org.hibernate.sql.ast.spi.AliasCollector;
import org.hibernate.sql.ast.spi.FromClauseAccess;
import org.hibernate.sql.ast.spi.SimpleFromClauseAccessImpl;
@ -710,13 +711,7 @@ public class LoaderSelectBuilder {
final TableGroupJoin pluralTableGroupJoin = parentTableGroup.findTableGroupJoin( tableGroup );
assert pluralTableGroupJoin != null;
final TableGroupJoin joinForPredicate;
if ( !tableGroup.getNestedTableGroupJoins().isEmpty() || tableGroup.getTableGroupJoins().isEmpty() ) {
joinForPredicate = pluralTableGroupJoin;
}
else {
joinForPredicate = tableGroup.getTableGroupJoins().get( tableGroup.getTableGroupJoins().size() - 1 );
}
final TableGroupJoin joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( pluralTableGroupJoin );
pluralAttributeMapping.applyBaseRestrictions(
joinForPredicate::applyPredicate,

View File

@ -49,6 +49,7 @@ import org.hibernate.persister.entity.Joinable;
import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.internal.TableGroupJoinHelper;
import org.hibernate.sql.ast.spi.FromClauseAccess;
import org.hibernate.sql.ast.spi.SqlAliasBase;
import org.hibernate.sql.ast.spi.SqlAliasStemHelper;
@ -763,13 +764,7 @@ public class PluralAttributeMappingImpl
collectionPredicateCollector.getPredicate()
);
if ( predicateCollector != collectionPredicateCollector ) {
final TableGroupJoin joinForPredicate;
if ( !tableGroup.getNestedTableGroupJoins().isEmpty() || tableGroup.getTableGroupJoins().isEmpty() ) {
joinForPredicate = tableGroupJoin;
}
else {
joinForPredicate = tableGroup.getTableGroupJoins().get( tableGroup.getTableGroupJoins().size() - 1 );
}
final TableGroupJoin joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( tableGroupJoin );
joinForPredicate.applyPredicate( predicateCollector.getPredicate() );
}
return tableGroupJoin;

View File

@ -268,6 +268,7 @@ import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.SqlTreeCreationException;
import org.hibernate.sql.ast.SqlTreeCreationLogger;
import org.hibernate.sql.ast.internal.TableGroupJoinHelper;
import org.hibernate.sql.ast.spi.FromClauseAccess;
import org.hibernate.sql.ast.spi.SqlAliasBase;
import org.hibernate.sql.ast.spi.SqlAliasBaseConstant;
@ -3381,13 +3382,9 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
}
// Implicit joins in the predicate might alter the nested table group joins,
// so defer determination of the join for predicate until after the predicate was visited
final TableGroupJoin joinForPredicate;
if ( !joinedTableGroup.getNestedTableGroupJoins().isEmpty() || joinedTableGroup.getTableGroupJoins().isEmpty() ) {
joinForPredicate = joinedTableGroupJoin;
}
else {
joinForPredicate = joinedTableGroup.getTableGroupJoins().get( joinedTableGroup.getTableGroupJoins().size() - 1 );
}
// add any additional join restrictions
if ( sqmJoin.getJoinPredicate() != null ) {
@ -3398,6 +3395,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final SqmJoin<?, ?> oldJoin = currentlyProcessingJoin;
currentlyProcessingJoin = sqmJoin;
final Predicate predicate = visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() );
joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( joinedTableGroupJoin );
// If translating the join predicate didn't initialize the table group,
// we can safely apply it on the collection table group instead
if ( joinForPredicate.getJoinedGroup().isInitialized() ) {
@ -3408,6 +3406,9 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
currentlyProcessingJoin = oldJoin;
}
else {
joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( joinedTableGroupJoin );
}
// Since joins on treated paths will never cause table pruning, we need to add a join condition for the treat
if ( sqmJoin.getLhs() instanceof SqmTreatedPath<?, ?> ) {
final SqmTreatedPath<?, ?> treatedPath = (SqmTreatedPath<?, ?>) sqmJoin.getLhs();
@ -8338,13 +8339,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final TableGroupJoin pluralTableGroupJoin = parentTableGroup.findTableGroupJoin( tableGroup );
assert pluralTableGroupJoin != null;
final TableGroupJoin joinForPredicate;
if ( !tableGroup.getNestedTableGroupJoins().isEmpty() || tableGroup.getTableGroupJoins().isEmpty() ) {
joinForPredicate = pluralTableGroupJoin;
}
else {
joinForPredicate = tableGroup.getTableGroupJoins().get( tableGroup.getTableGroupJoins().size() - 1 );
}
final TableGroupJoin joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( pluralTableGroupJoin );
joinForPredicate.applyPredicate( predicate );
},
tableGroup,

View File

@ -0,0 +1,46 @@
/*
* 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.sql.ast.internal;
import org.hibernate.metamodel.mapping.internal.EmbeddedCollectionPart;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.VirtualTableGroup;
public class TableGroupJoinHelper {
/**
* Determine the {@link TableGroupJoin} to which a custom {@code ON} clause predicate should be applied to.
* This is supposed to be called right after construction of a {@link TableGroupJoin}.
* This should also be called after a {@link org.hibernate.query.sqm.tree.predicate.SqmPredicate} is translated to a
* {@link org.hibernate.sql.ast.tree.predicate.Predicate}, because that translation might cause nested joins to be
* added to the table group of the join.
*/
public static TableGroupJoin determineJoinForPredicateApply(TableGroupJoin mainTableGroupJoin) {
final TableGroup mainTableGroup = mainTableGroupJoin.getJoinedGroup();
if ( !mainTableGroup.getNestedTableGroupJoins().isEmpty() || mainTableGroup.getTableGroupJoins().isEmpty() ) {
// Always apply a predicate on the main table group join if it has nested table group joins or no joins
return mainTableGroupJoin;
}
else {
// If the main table group has just regular table group joins,
// prefer to apply predicates on the last table group join
final TableGroupJoin lastTableGroupJoin = mainTableGroup.getTableGroupJoins()
.get( mainTableGroup.getTableGroupJoins().size() - 1 );
if ( lastTableGroupJoin.getJoinedGroup().getModelPart() instanceof EmbeddedCollectionPart ) {
// If the table group join refers to an embedded collection part,
// then the underlying table group *is* the main table group.
// Applying predicates on the join referring to the virtual table group would be a problem though,
// because these predicates will never be rendered. So use the main table group join in that case
assert lastTableGroupJoin.getJoinedGroup() instanceof VirtualTableGroup
&& ( (VirtualTableGroup) lastTableGroupJoin.getJoinedGroup() ).getUnderlyingTableGroup() == mainTableGroup;
return mainTableGroupJoin;
}
return lastTableGroupJoin;
}
}
}

View File

@ -11,11 +11,15 @@ import java.util.Set;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinTable;
@ -123,6 +127,26 @@ public class JoinTableOptimizationTest {
);
}
@Test
@JiraKey("HHH-17830")
public void testElementCollectionJoinCustomOnClause(SessionFactoryScope scope) {
SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction(
s -> {
s.createQuery( "select p.text from Document d join d.pages p on p.text is not null" ).list();
statementInspector.assertExecutedCount( 1 );
Assertions.assertEquals(
"select p1_0.text " +
"from Document d1_0 " +
"join document_pages p1_0 on d1_0.id=p1_0.Document_id and p1_0.text is not null",
statementInspector.getSqlQueries().get( 0 ),
"Join condition was wrongly removed"
);
}
);
}
@Entity(name = "Document")
public static class Document {
@Id
@ -131,6 +155,9 @@ public class JoinTableOptimizationTest {
@OneToMany
@JoinTable(name = "people")
Set<Person> people;
@ElementCollection
@CollectionTable(name = "document_pages")
Set<Page> pages;
}
@Entity(name = "Person")
public static class Person {
@ -138,4 +165,8 @@ public class JoinTableOptimizationTest {
Long id;
String name;
}
@Embeddable
public static class Page {
String text;
}
}