org.hibernate.engine.internal.JoinSequence Maven / Gradle / Ivy
/*
* 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 .
*/
package org.hibernate.engine.internal;
import java.util.*;
import java.util.Collections;
import org.hibernate.AssertionFailure;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.internal.ast.tree.ImpliedFromElement;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.persister.collection.AbstractCollectionPersister;
import org.hibernate.persister.collection.QueryableCollection;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.sql.JoinFragment;
import org.hibernate.sql.JoinType;
import org.hibernate.sql.QueryJoinFragment;
import org.hibernate.type.AssociationType;
/**
* A sequence of {@link Join} delegates to make it "easier" to work with joins. The "easier" part is obviously
* subjective ;)
*
* Additionally JoinSequence is a directed graph of other JoinSequence instances, as represented by the
* {@link #next} ({@link #setNext(JoinSequence)}) pointer.
*
* @author Gavin King
* @author Steve Ebersole
*
* @see JoinFragment
*/
public class JoinSequence {
private final SessionFactoryImplementor factory;
private final boolean collectionJoinSubquery;
private final StringBuilder conditions = new StringBuilder();
private final List joins = new ArrayList();
private boolean useThetaStyle;
private String rootAlias;
private Joinable rootJoinable;
private Selector selector;
private JoinSequence next;
private boolean isFromPart;
private Set queryReferencedTables;
/**
* Constructs a JoinSequence
*
* @param factory The SessionFactory
*/
public JoinSequence(SessionFactoryImplementor factory) {
this.factory = factory;
this.collectionJoinSubquery = factory.getSessionFactoryOptions().isCollectionJoinSubqueryRewriteEnabled();
}
/**
* Retrieve a JoinSequence that represents just the FROM clause parts
*
* @return The JoinSequence that represents just the FROM clause parts
*/
public JoinSequence getFromPart() {
final JoinSequence fromPart = new JoinSequence( factory );
fromPart.joins.addAll( this.joins );
fromPart.useThetaStyle = this.useThetaStyle;
fromPart.rootAlias = this.rootAlias;
fromPart.rootJoinable = this.rootJoinable;
fromPart.selector = this.selector;
fromPart.next = this.next == null ? null : this.next.getFromPart();
fromPart.isFromPart = true;
return fromPart;
}
private Set treatAsDeclarations;
public void applyTreatAsDeclarations(Set treatAsDeclarations) {
if ( treatAsDeclarations == null || treatAsDeclarations.isEmpty() ) {
return;
}
if ( this.treatAsDeclarations == null ) {
this.treatAsDeclarations = new HashSet();
}
this.treatAsDeclarations.addAll( treatAsDeclarations );
}
protected Set getTreatAsDeclarations() {
return treatAsDeclarations;
}
/**
* Create a full, although shallow, copy.
*
* @return The copy
*/
public JoinSequence copy() {
final JoinSequence copy = new JoinSequence( factory );
copy.joins.addAll( this.joins );
copy.useThetaStyle = this.useThetaStyle;
copy.rootAlias = this.rootAlias;
copy.rootJoinable = this.rootJoinable;
copy.selector = this.selector;
copy.next = this.next == null ? null : this.next.copy();
copy.isFromPart = this.isFromPart;
copy.conditions.append( this.conditions.toString() );
return copy;
}
/**
* Add a join to this sequence
*
* @param associationType The type of the association representing the join
* @param alias The RHS alias for the join
* @param joinType The type of join (INNER, etc)
* @param referencingKey The LHS columns for the join condition
*
* @return The Join memento
*
* @throws MappingException Generally indicates a problem resolving the associationType to a {@link Joinable}
*/
public JoinSequence addJoin(
AssociationType associationType,
String alias,
JoinType joinType,
String[] referencingKey) throws MappingException {
joins.add( new Join( factory, associationType, alias, joinType, new String[][] { referencingKey } ) );
return this;
}
/**
* Add a join to this sequence
*
* @param associationType The type of the association representing the join
* @param alias The RHS alias for the join
* @param joinType The type of join (INNER, etc)
* @param referencingKeys The LHS columns for the join condition
*
* @return The Join memento
*
* @throws MappingException Generally indicates a problem resolving the associationType to a {@link Joinable}
*/
public JoinSequence addJoin(
AssociationType associationType,
String alias,
JoinType joinType,
String[][] referencingKeys) throws MappingException {
joins.add( new Join( factory, associationType, alias, joinType, referencingKeys ) );
return this;
}
/**
* Embeds an implied from element into this sequence
*
* @param fromElement The implied from element to embed
* @return The Join memento
*/
public JoinSequence addJoin(ImpliedFromElement fromElement) {
joins.addAll( fromElement.getJoinSequence().joins );
return this;
}
/**
* Generate a JoinFragment
*
* @return The JoinFragment
*
* @throws MappingException Indicates a problem access the provided metadata, or incorrect metadata
*/
public JoinFragment toJoinFragment() throws MappingException {
return toJoinFragment( Collections.EMPTY_MAP, true );
}
/**
* Generate a JoinFragment
*
* @param enabledFilters The filters associated with the originating session to properly define join conditions
* @param includeAllSubclassJoins Should all subclass joins be added to the rendered JoinFragment?
*
* @return The JoinFragment
*
* @throws MappingException Indicates a problem access the provided metadata, or incorrect metadata
*/
public JoinFragment toJoinFragment(Map enabledFilters, boolean includeAllSubclassJoins) throws MappingException {
return toJoinFragment( enabledFilters, includeAllSubclassJoins, null );
}
/**
* Generate a JoinFragment
*
* @param enabledFilters The filters associated with the originating session to properly define join conditions
* @param includeAllSubclassJoins Should all subclass joins be added to the rendered JoinFragment?
* @param withClauseFragment The with clause (which represents additional join restrictions) fragment
*
* @return The JoinFragment
*
* @throws MappingException Indicates a problem access the provided metadata, or incorrect metadata
*/
public JoinFragment toJoinFragment(
Map enabledFilters,
boolean includeAllSubclassJoins,
String withClauseFragment) throws MappingException {
return toJoinFragment( enabledFilters, includeAllSubclassJoins, true, withClauseFragment );
}
public JoinFragment toJoinFragment(
Map enabledFilters,
boolean includeAllSubclassJoins,
boolean renderSubclassJoins,
String withClauseFragment) throws MappingException {
final QueryJoinFragment joinFragment = new QueryJoinFragment( factory.getDialect(), useThetaStyle );
Iterator iter;
Join first;
Joinable last;
if ( rootJoinable != null ) {
joinFragment.addCrossJoin( rootJoinable.getTableName(), rootAlias );
final String filterCondition = rootJoinable.filterFragment( rootAlias, enabledFilters, treatAsDeclarations );
// JoinProcessor needs to know if the where clause fragment came from a dynamic filter or not so it
// can put the where clause fragment in the right place in the SQL AST. 'hasFilterCondition' keeps track
// of that fact.
joinFragment.setHasFilterCondition( joinFragment.addCondition( filterCondition ) );
addSubclassJoins( joinFragment, rootAlias, rootJoinable, true, includeAllSubclassJoins, treatAsDeclarations );
last = rootJoinable;
}
else if ( needsTableGroupJoin( joins, withClauseFragment ) ) {
iter = joins.iterator();
first = iter.next();
final String joinString;
switch (first.joinType) {
case INNER_JOIN:
joinString = " inner join ";
break;
case LEFT_OUTER_JOIN:
joinString = " left outer join ";
break;
case RIGHT_OUTER_JOIN:
joinString = " right outer join ";
break;
case FULL_JOIN:
joinString = " full outer join ";
break;
default:
throw new AssertionFailure("undefined join type");
}
joinFragment.addFromFragmentString( joinString );
joinFragment.addFromFragmentString( " (" );
joinFragment.addFromFragmentString( first.joinable.getTableName() );
joinFragment.addFromFragmentString( " " );
joinFragment.addFromFragmentString( first.getAlias() );
for ( Join join : joins ) {
// Skip joining the first join node as it is contained in the subquery
if ( join != first ) {
joinFragment.addJoin(
join.getJoinable().getTableName(),
join.getAlias(),
join.getLHSColumns(),
JoinHelper.getRHSColumnNames( join.getAssociationType(), factory ),
join.joinType
);
}
addSubclassJoins(
joinFragment,
join.getAlias(),
join.getJoinable(),
// TODO: Think about if this could be made always true
join.joinType == JoinType.INNER_JOIN,
includeAllSubclassJoins,
// ugh.. this is needed because of how HQL parser (FromElementFactory/SessionFactoryHelper)
// builds the JoinSequence for HQL joins
treatAsDeclarations
);
}
joinFragment.addFromFragmentString( ")" );
joinFragment.addFromFragmentString( " on " );
final String rhsAlias = first.getAlias();
final String[][] lhsColumns = first.getLHSColumns();
final String[] rhsColumns = JoinHelper.getRHSColumnNames( first.getAssociationType(), factory );
if ( lhsColumns.length > 1 ) {
joinFragment.addFromFragmentString( "(" );
}
for ( int i = 0; i < lhsColumns.length; i++ ) {
for ( int j = 0; j < lhsColumns[i].length; j++ ) {
joinFragment.addFromFragmentString( lhsColumns[i][j] );
joinFragment.addFromFragmentString( "=" );
joinFragment.addFromFragmentString( rhsAlias );
joinFragment.addFromFragmentString( "." );
joinFragment.addFromFragmentString( rhsColumns[j] );
if ( j < lhsColumns[i].length - 1 ) {
joinFragment.addFromFragmentString( " and " );
}
}
if ( i < lhsColumns.length - 1 ) {
joinFragment.addFromFragmentString( " or " );
}
}
if ( lhsColumns.length > 1 ) {
joinFragment.addFromFragmentString( ")" );
}
joinFragment.addFromFragmentString( " and " );
joinFragment.addFromFragmentString( withClauseFragment );
return joinFragment;
}
else {
last = null;
}
for ( Join join : joins ) {
// technically the treatAsDeclarations should only apply to rootJoinable or to a single Join,
// but that is not possible atm given how these JoinSequence and Join objects are built.
// However, it is generally ok given how the HQL parser builds these JoinSequences (a HQL join
// results in a JoinSequence with an empty rootJoinable and a single Join). So we use that here
// as an assumption
final String on = join.getAssociationType().getOnCondition( join.getAlias(), factory, enabledFilters, treatAsDeclarations );
String condition;
if ( last != null
&& isManyToManyRoot( last )
&& ((QueryableCollection) last).getElementType() == join.getAssociationType() ) {
// the current join represents the join between a many-to-many association table
// and its "target" table. Here we need to apply any additional filters
// defined specifically on the many-to-many
final String manyToManyFilter = ( (QueryableCollection) last ).getManyToManyFilterFragment(
join.getAlias(),
enabledFilters
);
condition = (manyToManyFilter != null && manyToManyFilter.isEmpty())
? on
: (on != null && on.isEmpty()) ? manyToManyFilter : on + " and " + manyToManyFilter;
}
else {
condition = on;
}
if ( withClauseFragment != null && !isManyToManyRoot( join.joinable )) {
condition += " and " + withClauseFragment;
}
joinFragment.addJoin(
join.getJoinable().getTableName(),
join.getAlias(),
join.getLHSColumns(),
JoinHelper.getRHSColumnNames( join.getAssociationType(), factory ),
join.joinType,
condition
);
if (renderSubclassJoins) {
addSubclassJoins(
joinFragment,
join.getAlias(),
join.getJoinable(),
join.joinType == JoinType.INNER_JOIN,
includeAllSubclassJoins,
// ugh.. this is needed because of how HQL parser (FromElementFactory/SessionFactoryHelper)
// builds the JoinSequence for HQL joins
treatAsDeclarations
);
}
last = join.getJoinable();
}
if ( next != null ) {
joinFragment.addFragment( next.toJoinFragment( enabledFilters, includeAllSubclassJoins ) );
}
joinFragment.addCondition( conditions.toString() );
if ( isFromPart ) {
joinFragment.clearWherePart();
}
return joinFragment;
}
private boolean needsTableGroupJoin(List joins, String withClauseFragment) {
// If the rewrite is disabled or we don't have a with clause, we don't need a table group join
if ( !collectionJoinSubquery || StringHelper.isEmpty( withClauseFragment ) ) {
return false;
}
// If we only have one join, a table group join is only necessary if subclass columns are used in the with clause
if ( joins.size() < 2 ) {
return isSubclassAliasDereferenced( joins.get( 0 ), withClauseFragment );
}
// If more than one table is involved and this is not an inner join, we definitely need a table group join
// i.e. a left join has to be made for the table group to retain the join semantics
if ( joins.get( 0 ).getJoinType() != JoinType.INNER_JOIN ) {
return true;
}
// If a subclass columns is used, we need a table group, otherwise we generate wrong SQL by putting the ON condition to the first join
if ( isSubclassAliasDereferenced( joins.get( 0 ), withClauseFragment ) ) {
return true;
}
// Normally, the ON condition of a HQL join is put on the ON clause of the first SQL join
// Since the ON condition could refer to columns from subsequently joined tables i.e. joins with index > 0
// or could refer to columns of subclass tables, the SQL could be wrong
// To avoid generating wrong SQL, we detect these cases here i.e. a subsequent join alias is used in the ON condition
// If we find out that this is the case, we return true and generate a table group join
// Skip the first since that is the driving join
for ( int i = 1; i < joins.size(); i++ ) {
Join join = joins.get( i );
if ( isAliasDereferenced( withClauseFragment, join.getAlias() ) || isSubclassAliasDereferenced( join, withClauseFragment ) ) {
return true;
}
}
return false;
}
private boolean isSubclassAliasDereferenced(Join join, String withClauseFragment) {
Object joinable = join.getJoinable();
if ( joinable instanceof AbstractCollectionPersister ) {
final AbstractCollectionPersister collectionPersister = (AbstractCollectionPersister) joinable;
if ( collectionPersister.getElementType().isEntityType() ) {
joinable = ( collectionPersister ).getElementPersister();
}
}
if ( joinable instanceof AbstractEntityPersister ) {
final AbstractEntityPersister persister = (AbstractEntityPersister) joinable;
final int subclassTableSpan = persister.getSubclassTableSpan();
for ( int j = 1; j < subclassTableSpan; j++ ) {
String subclassAlias = AbstractEntityPersister.generateTableAlias( join.getAlias(), j );
if ( isAliasDereferenced( withClauseFragment, subclassAlias ) ) {
return true;
}
}
}
return false;
}
private boolean isAliasDereferenced(String withClauseFragment, String alias) {
// See if the with clause contains the join alias
int index = withClauseFragment.indexOf( alias );
int dotIndex = index + alias.length();
if ( index != -1
// Check that the join alias is not a suffix
&& ( index == 0 || !Character.isLetterOrDigit( withClauseFragment.charAt( index - 1 ) ) )
// Check that the join alias gets de-referenced i.e. the next char is a dot
&& dotIndex < withClauseFragment.length() && withClauseFragment.charAt( dotIndex ) == '.' ) {
return true;
}
return false;
}
@SuppressWarnings("SimplifiableIfStatement")
private boolean isManyToManyRoot(Joinable joinable) {
if ( joinable != null && joinable.isCollection() ) {
return ( (QueryableCollection) joinable ).isManyToMany();
}
return false;
}
private void addSubclassJoins(
JoinFragment joinFragment,
String alias,
Joinable joinable,
boolean innerJoin,
boolean includeSubclassJoins,
Set treatAsDeclarations) {
final boolean include = includeSubclassJoins && isIncluded( alias );
joinFragment.addJoins(
joinable.fromJoinFragment( alias, innerJoin, include, treatAsDeclarations, queryReferencedTables ),
joinable.whereJoinFragment( alias, innerJoin, include, treatAsDeclarations )
);
}
protected boolean isIncluded(String alias) {
return selector != null && selector.includeSubclasses( alias );
}
/**
* Add a condition to this sequence.
*
* @param condition The condition
*
* @return {@link this}, for method chaining
*/
public JoinSequence addCondition(String condition) {
if ( !StringHelper.isBlank( condition ) ) {
if ( !condition.startsWith( " and " ) ) {
conditions.append( " and " );
}
conditions.append( condition );
}
return this;
}
/**
* Add a condition to this sequence. Typical usage here might be:
*
* addCondition( "a", {"c1", "c2"}, "?" )
*
* to represent:
*
* "... a.c1 = ? and a.c2 = ? ..."
*
*
* @param alias The alias to apply to the columns
* @param columns The columns to add checks for
* @param condition The conditions to check against the columns
*
* @return {@link this}, for method chaining
*/
public JoinSequence addCondition(String alias, String[] columns, String condition) {
for ( String column : columns ) {
conditions.append( " and " )
.append( alias )
.append( '.' )
.append( column )
.append( condition );
}
return this;
}
/**
* Set the root of this JoinSequence. In SQL terms, this would be the driving table.
*
* @param joinable The entity/collection that is the root of this JoinSequence
* @param alias The alias associated with that joinable.
*
* @return {@link this}, for method chaining
*/
public JoinSequence setRoot(Joinable joinable, String alias) {
this.rootAlias = alias;
this.rootJoinable = joinable;
return this;
}
/**
* Sets the next join sequence
*
* @param next The next JoinSequence in the directed graph
*
* @return {@code this}, for method chaining
*/
public JoinSequence setNext(JoinSequence next) {
this.next = next;
return this;
}
/**
* Set the Selector to use to determine how subclass joins should be applied.
*
* @param selector The selector to apply
*
* @return {@code this}, for method chaining
*/
public JoinSequence setSelector(Selector selector) {
this.selector = selector;
return this;
}
/**
* Should this JoinSequence use theta-style joining (both a FROM and WHERE component) in the rendered SQL?
*
* @param useThetaStyle {@code true} indicates that theta-style joins should be used.
*
* @return {@code this}, for method chaining
*/
public JoinSequence setUseThetaStyle(boolean useThetaStyle) {
this.useThetaStyle = useThetaStyle;
return this;
}
public boolean isThetaStyle() {
return useThetaStyle;
}
/**
* Set all tables the query refers to. It allows to optimize the query.
*
* @param queryReferencedTables
*/
public void setQueryReferencedTables(Set queryReferencedTables) {
this.queryReferencedTables = queryReferencedTables;
}
public Join getFirstJoin() {
return joins.get( 0 );
}
/**
* A subclass join selector
*/
public static interface Selector {
/**
* Should subclasses be included in the rendered join sequence?
*
* @param alias The alias
*
* @return {@code true} if the subclass joins should be included
*/
public boolean includeSubclasses(String alias);
}
/**
* Represents a join
*/
public static final class Join {
private final AssociationType associationType;
private final Joinable joinable;
private final JoinType joinType;
private final String alias;
private final String[][] lhsColumns;
Join(
SessionFactoryImplementor factory,
AssociationType associationType,
String alias,
JoinType joinType,
String[][] lhsColumns) throws MappingException {
this.associationType = associationType;
this.joinable = associationType.getAssociatedJoinable( factory );
this.alias = alias;
this.joinType = joinType;
this.lhsColumns = lhsColumns;
}
public String getAlias() {
return alias;
}
public AssociationType getAssociationType() {
return associationType;
}
public Joinable getJoinable() {
return joinable;
}
public JoinType getJoinType() {
return joinType;
}
public String[][] getLHSColumns() {
return lhsColumns;
}
@Override
public String toString() {
return joinable.toString() + '[' + alias + ']';
}
}
public JoinSequence copyForCollectionProperty() {
JoinSequence copy = this.copy();
copy.joins.clear();
Iterator joinIterator = this.joins.iterator();
while ( joinIterator.hasNext() ) {
Join join = joinIterator.next();
copy.addJoin(
join.getAssociationType(),
join.getAlias(),
JoinType.INNER_JOIN,
join.getLHSColumns()
);
}
return copy;
}
@Override
public String toString() {
final StringBuilder buf = new StringBuilder();
buf.append( "JoinSequence{" );
if ( rootJoinable != null ) {
buf.append( rootJoinable )
.append( '[' )
.append( rootAlias )
.append( ']' );
}
for ( Join join : joins ) {
buf.append( "->" ).append( join );
}
return buf.append( '}' ).toString();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy