Introduced table group joins

This commit is contained in:
Christian Beikov 2017-02-04 12:29:07 +01:00 committed by Vlad Mihalcea
parent c87194c3fe
commit 753858ce73
6 changed files with 152 additions and 136 deletions

View File

@ -8,12 +8,14 @@ package org.hibernate.engine.internal;
import java.util.*;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.QueryableCollection;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.sql.JoinFragment;
import org.hibernate.sql.JoinType;
@ -127,6 +129,17 @@ public class JoinSequence {
return this;
}
/**
* Embedds an implied from element into this sequence
*
* @param fromElement The implied from element to embedd
* @return The Join memento
*/
public JoinSequence addJoin(ImpliedFromElement fromElement) {
joins.addAll( fromElement.getJoinSequence().joins );
return this;
}
/**
* Generate a JoinFragment
*
@ -149,7 +162,7 @@ public class JoinSequence {
* @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, null, null );
return toJoinFragment( enabledFilters, includeAllSubclassJoins, null );
}
/**
@ -158,7 +171,6 @@ public class JoinSequence {
* @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
* @param withClauseJoinAlias The
*
* @return The JoinFragment
*
@ -167,19 +179,15 @@ public class JoinSequence {
public JoinFragment toJoinFragment(
Map enabledFilters,
boolean includeAllSubclassJoins,
String withClauseFragment,
String withClauseJoinAlias,
String withClauseCollectionJoinAlias) throws MappingException {
return toJoinFragment( enabledFilters, includeAllSubclassJoins, true, withClauseFragment, withClauseJoinAlias, withClauseCollectionJoinAlias );
String withClauseFragment) throws MappingException {
return toJoinFragment( enabledFilters, includeAllSubclassJoins, true, withClauseFragment );
}
public JoinFragment toJoinFragment(
Map enabledFilters,
boolean includeAllSubclassJoins,
boolean renderSubclassJoins,
String withClauseFragment,
String withClauseJoinAlias,
String withClauseCollectionJoinAlias) throws MappingException {
String withClauseFragment) throws MappingException {
final QueryJoinFragment joinFragment = new QueryJoinFragment( factory.getDialect(), useThetaStyle );
Iterator<Join> iter;
Join first;
@ -195,98 +203,32 @@ public class JoinSequence {
last = rootJoinable;
}
else if (
collectionJoinSubquery
&& withClauseFragment != null
&& joins.size() > 1
&& ( withClauseFragment.contains( withClauseJoinAlias ) || ( withClauseCollectionJoinAlias != null && withClauseFragment.contains( withClauseCollectionJoinAlias ) ) )
&& ( first = ( iter = joins.iterator() ).next() ).joinType == JoinType.LEFT_OUTER_JOIN
) {
final QueryJoinFragment subqueryJoinFragment = new QueryJoinFragment( factory.getDialect(), useThetaStyle );
subqueryJoinFragment.addFromFragmentString( "(select " );
subqueryJoinFragment.addFromFragmentString( first.getAlias() );
subqueryJoinFragment.addFromFragmentString( ".*" );
// Re-alias columns of withClauseJoinAlias and rewrite withClauseFragment
// A list of possible delimited identifier types: https://en.wikibooks.org/wiki/SQL_Dialects_Reference/Data_structure_definition/Delimited_identifiers
String prefixPattern = "(" + Pattern.quote( withClauseJoinAlias );
if ( withClauseCollectionJoinAlias != null ) {
prefixPattern += "|" + Pattern.quote( withClauseCollectionJoinAlias );
}
prefixPattern += ")" + Pattern.quote( "." );
Pattern p = Pattern.compile( prefixPattern + "(" +
"([a-zA-Z0-9_]+)|" + // Normal identifiers
// Ignore single quoted identifiers to avoid possible clashes with string literals
// and since SQLLite is the only DB supporting that style, we simply decide to not support it
//"('[a-zA-Z0-9_]+'((''[a-zA-Z0-9_]+)+')?)|" + // Single quoted identifiers
"(\"[a-zA-Z0-9_]+\"((\"\"[a-zA-Z0-9_]+)+\")?)|" + // Double quoted identifiers
"(`[a-zA-Z0-9_]+`((``[a-zA-Z0-9_]+)+`)?)|" + // MySQL quoted identifiers
"(\\[[a-zA-Z0-9_\\s]+\\])" + // MSSQL quoted identifiers
")"
);
Matcher matcher = p.matcher( withClauseFragment );
StringBuilder withClauseSb = new StringBuilder( withClauseFragment.length() );
withClauseSb.append( " and " );
int start = 0;
int aliasNumber = 0;
while ( matcher.find() ) {
final String matchedTableName = matcher.group(1);
final String column = matcher.group( 2 );
// Replace non-valid simple identifier characters from the column name
final String alias = "c_" + aliasNumber + "_" + column.replaceAll( "[\\[\\]\\s\"']+", "" );
withClauseSb.append( withClauseFragment, start, matcher.start() );
withClauseSb.append( first.getAlias() );
withClauseSb.append( '.' );
withClauseSb.append( alias );
withClauseSb.append( ' ' );
subqueryJoinFragment.addFromFragmentString( ", " );
subqueryJoinFragment.addFromFragmentString( matchedTableName );
subqueryJoinFragment.addFromFragmentString( "." );
subqueryJoinFragment.addFromFragmentString( column );
subqueryJoinFragment.addFromFragmentString( " as " );
subqueryJoinFragment.addFromFragmentString( alias );
start = matcher.end();
aliasNumber++;
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");
}
withClauseSb.append( withClauseFragment, start, withClauseFragment.length() );
subqueryJoinFragment.addFromFragmentString( " from " );
subqueryJoinFragment.addFromFragmentString( first.joinable.getTableName() );
subqueryJoinFragment.addFromFragmentString( " " );
subqueryJoinFragment.addFromFragmentString( first.getAlias() );
// Render following join sequences in a sub-sequence
JoinSequence subSequence = new JoinSequence( factory );
while ( iter.hasNext() ) {
Join join = iter.next();
subSequence.joins.add( join );
}
JoinFragment subFragment = subSequence.toJoinFragment(
enabledFilters,
false,
true, // TODO: only join subclasses that are needed for ON clause
null,
null,
null
);
subqueryJoinFragment.addFragment( subFragment );
subqueryJoinFragment.addFromFragmentString( ")" );
joinFragment.addJoin(
subqueryJoinFragment.toFromFragmentString(),
first.getAlias(),
first.getLHSColumns(),
JoinHelper.getRHSColumnNames( first.getAssociationType(), factory ),
first.joinType,
withClauseSb.toString()
);
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
@ -304,6 +246,7 @@ public class JoinSequence {
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)
@ -312,6 +255,26 @@ public class JoinSequence {
);
}
joinFragment.addFromFragmentString( ")" );
joinFragment.addFromFragmentString( " on " );
final String rhsAlias = first.getAlias();
final String[] lhsColumns = first.getLHSColumns();
final String[] rhsColumns = JoinHelper.getRHSColumnNames( first.getAssociationType(), factory );
for ( int j=0; j < lhsColumns.length; j++) {
joinFragment.addFromFragmentString( lhsColumns[j] );
joinFragment.addFromFragmentString( "=" );
joinFragment.addFromFragmentString( rhsAlias );
joinFragment.addFromFragmentString( "." );
joinFragment.addFromFragmentString( rhsColumns[j] );
if ( j < lhsColumns.length - 1 ) {
joinFragment.addFromFragmentString( " and " );
}
}
joinFragment.addFromFragmentString( " and " );
joinFragment.addFromFragmentString( withClauseFragment );
return joinFragment;
}
else {
@ -385,6 +348,72 @@ public class JoinSequence {
return joinFragment;
}
private boolean needsTableGroupJoin(List<Join> 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) {
if ( join.getJoinable() instanceof AbstractEntityPersister ) {
AbstractEntityPersister persister = (AbstractEntityPersister) join.getJoinable();
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() ) {

View File

@ -499,23 +499,10 @@ public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, Par
NodeTraverser traverser = new NodeTraverser( visitor );
traverser.traverseDepthFirst( hqlSqlWithNode );
String withClauseJoinAlias = fromElement.getTableAlias();
String withClauseCollectionJoinAlias = fromElement.getCollectionTableAlias();
// else {
// FromElement referencedFromElement = visitor.getReferencedFromElement();
// if ( referencedFromElement != fromElement ) {
// LOG.warnf(
// "with-clause expressions do not reference the from-clause element to which the " +
// "with-clause was associated. The query may not work as expected [%s]",
// queryTranslatorImpl.getQueryString()
// );
// }
// }
SqlGenerator sql = new SqlGenerator( getSessionFactoryHelper().getFactory() );
sql.whereExpr( hqlSqlWithNode.getFirstChild() );
fromElement.setWithClauseFragment( withClauseJoinAlias, withClauseCollectionJoinAlias, "(" + sql.getSQL() + ")" );
fromElement.setWithClauseFragment( "(" + sql.getSQL() + ")" );
}
catch (SemanticException e) {
throw e;

View File

@ -106,9 +106,7 @@ public class EntityJoinFromElement extends FromElement {
public JoinFragment toJoinFragment(
Map enabledFilters,
boolean includeAllSubclassJoins,
String withClauseFragment,
String withClauseJoinAlias,
String withClauseCollectionJoinAlias) throws MappingException {
String withClauseFragment) throws MappingException {
final String joinString;
switch ( joinType ) {
case INNER_JOIN:

View File

@ -70,8 +70,6 @@ public class FromElement extends HqlSqlWalkerNode implements DisplayableNode, Pa
private List<FromElement> destinations;
private boolean manyToMany;
private String withClauseFragment;
private String withClauseJoinAlias;
private String withClauseCollectionJoinAlias;
private boolean dereferencedBySuperclassProperty;
private boolean dereferencedBySubclassProperty;
@ -627,17 +625,7 @@ public class FromElement extends HqlSqlWalkerNode implements DisplayableNode, Pa
return withClauseFragment;
}
public String getWithClauseJoinAlias() {
return withClauseJoinAlias;
}
public String getWithClauseCollectionJoinAlias() {
return withClauseCollectionJoinAlias;
}
public void setWithClauseFragment(String withClauseJoinAlias, String withClauseCollectionJoinAlias, String withClauseFragment) {
this.withClauseJoinAlias = withClauseJoinAlias;
this.withClauseCollectionJoinAlias = withClauseCollectionJoinAlias;
public void setWithClauseFragment(String withClauseFragment) {
this.withClauseFragment = withClauseFragment;
}

View File

@ -22,6 +22,7 @@ import org.hibernate.hql.internal.ast.HqlSqlWalker;
import org.hibernate.hql.internal.ast.tree.DotNode;
import org.hibernate.hql.internal.ast.tree.FromClause;
import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.ImpliedFromElement;
import org.hibernate.hql.internal.ast.tree.ParameterContainer;
import org.hibernate.hql.internal.ast.tree.QueryNode;
import org.hibernate.hql.internal.classic.ParserHelper;
@ -106,7 +107,22 @@ public class JoinProcessor implements SqlTokenTypes {
}
}
else {
fromElements = fromClause.getFromElements();
fromElements = new ArrayList( fromClause.getFromElements().size() );
ListIterator<FromElement> liter = fromClause.getFromElements().listIterator();
while ( liter.hasNext() ) {
FromElement fromElement = liter.next();
// We found an implied from element that is used in the WITH clause of another from element, so it need to become part of it's join sequence
if ( fromElement instanceof ImpliedFromElement
&& fromElement.getOrigin().getWithClauseFragment() != null
&& fromElement.getOrigin().getWithClauseFragment().contains( fromElement.getTableAlias() ) ) {
fromElement.getOrigin().getJoinSequence().addJoin( (ImpliedFromElement) fromElement );
// This from element will be rendered as part of the origins join sequence
fromElement.setText("");
} else {
fromElements.add( fromElement );
}
}
}
// Iterate through the alias,JoinSequence pairs and generate SQL token nodes.
@ -147,9 +163,7 @@ public class JoinProcessor implements SqlTokenTypes {
JoinFragment joinFragment = join.toJoinFragment(
walker.getEnabledFilters(),
fromElement.useFromFragment() || fromElement.isDereferencedBySuperclassOrSubclassProperty(),
fromElement.getWithClauseFragment(),
fromElement.getWithClauseJoinAlias(),
fromElement.getWithClauseCollectionJoinAlias()
fromElement.getWithClauseFragment()
);
String frag = joinFragment.toFromFragmentString();

View File

@ -297,7 +297,7 @@ public abstract class AbstractEntityPersister
protected abstract boolean isClassOrSuperclassTable(int j);
protected abstract int getSubclassTableSpan();
public abstract int getSubclassTableSpan();
protected abstract int getTableSpan();