Test and fix for HHH-9329

This commit is contained in:
Christian Beikov 2016-09-20 19:28:25 +02:00 committed by Steve Ebersole
parent de7ca7882b
commit 1d823ceb76
2 changed files with 157 additions and 16 deletions

View File

@ -6,12 +6,10 @@
*/
package org.hibernate.engine.internal;
import java.util.ArrayList;
import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SessionFactoryImplementor;
@ -169,7 +167,19 @@ public class JoinSequence {
boolean includeAllSubclassJoins,
String withClauseFragment,
String withClauseJoinAlias) throws MappingException {
return toJoinFragment( enabledFilters, includeAllSubclassJoins, true, withClauseFragment, withClauseJoinAlias );
}
public JoinFragment toJoinFragment(
Map enabledFilters,
boolean includeAllSubclassJoins,
boolean renderSubclassJoins,
String withClauseFragment,
String withClauseJoinAlias) throws MappingException {
final QueryJoinFragment joinFragment = new QueryJoinFragment( factory.getDialect(), useThetaStyle );
Iterator<Join> iter;
Join first;
Joinable last;
if ( rootJoinable != null ) {
joinFragment.addCrossJoin( rootJoinable.getTableName(), rootAlias );
final String filterCondition = rootJoinable.filterFragment( rootAlias, enabledFilters, treatAsDeclarations );
@ -178,10 +188,111 @@ public class JoinSequence {
// of that fact.
joinFragment.setHasFilterCondition( joinFragment.addCondition( filterCondition ) );
addSubclassJoins( joinFragment, rootAlias, rootJoinable, true, includeAllSubclassJoins, treatAsDeclarations );
last = rootJoinable;
}
else if (
withClauseFragment != null
&& joins.size() > 1
&& withClauseFragment.contains( withClauseJoinAlias )
&& ( first = ( iter = joins.iterator() ).next() ).joinType == JoinType.LEFT_OUTER_JOIN
) {
final QueryJoinFragment subqueryJoinFragment = new QueryJoinFragment( factory.getDialect(), useThetaStyle );
subqueryJoinFragment.addFromFragmentString( "(select " );
Joinable last = rootJoinable;
subqueryJoinFragment.addFromFragmentString( first.getAlias() );
subqueryJoinFragment.addFromFragmentString( ".*" );
// Re-alias columns of withClauseJoinAlias and rewrite withClauseFragment
Pattern p = Pattern.compile( Pattern.quote( withClauseJoinAlias + "." ) + "([a-zA-Z0-9_]+)");
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 column = matcher.group( 1 );
final String alias = "_" + aliasNumber + "_" + column;
withClauseSb.append( withClauseFragment, start, matcher.start() );
withClauseSb.append( first.getAlias() );
withClauseSb.append( '.' );
withClauseSb.append( alias );
subqueryJoinFragment.addFromFragmentString( ", " );
subqueryJoinFragment.addFromFragmentString( withClauseJoinAlias );
subqueryJoinFragment.addFromFragmentString( "." );
subqueryJoinFragment.addFromFragmentString( column );
subqueryJoinFragment.addFromFragmentString( " as " );
subqueryJoinFragment.addFromFragmentString( alias );
start = matcher.end();
aliasNumber++;
}
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
);
subqueryJoinFragment.addFragment( subFragment );
subqueryJoinFragment.addFromFragmentString( ")" );
joinFragment.addJoin(
subqueryJoinFragment.toFromFragmentString(),
first.getAlias(),
first.getLHSColumns(),
JoinHelper.getRHSColumnNames( first.getAssociationType(), factory ),
first.joinType,
withClauseSb.toString()
);
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(),
join.joinType == JoinType.INNER_JOIN,
includeAllSubclassJoins,
// ugh.. this is needed because of how HQL parser (FromElementFactory/SessionFactoryHelper)
// builds the JoinSequence for HQL joins
treatAsDeclarations
);
}
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.
@ -221,16 +332,19 @@ public class JoinSequence {
condition
);
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
);
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();
}

View File

@ -177,6 +177,27 @@ public class WithClauseTest extends BaseCoreFunctionalTestCase {
s.close();
}
@Test
@TestForIssue(jiraKey = "HHH-9329")
public void testWithClauseAsSubquery() {
TestData data = new TestData();
data.prepare();
Session s = openSession();
Transaction txn = s.beginTransaction();
// Since friends has a join table, we will first left join all friends and then do the WITH clause on the target entity table join
// Normally this produces 2 results which is wrong and can only be circumvented by converting the join table and target entity table join to a subquery
List list = s.createQuery( "from Human h left join h.friends as f with f.nickName like 'bubba' where h.description = 'father'" )
.list();
assertEquals( "subquery rewriting of join table did not take effect", 1, list.size() );
txn.commit();
s.close();
data.cleanup();
}
private class TestData {
public void prepare() {
Session session = openSession();
@ -202,6 +223,10 @@ public class WithClauseTest extends BaseCoreFunctionalTestCase {
friend.setBodyWeight( 20 );
friend.setDescription( "friend" );
Human friend2 = new Human();
friend2.setBodyWeight( 20 );
friend2.setDescription( "friend2" );
child1.setMother( mother );
child1.setFather( father );
mother.addOffspring( child1 );
@ -214,12 +239,14 @@ public class WithClauseTest extends BaseCoreFunctionalTestCase {
father.setFriends( new ArrayList() );
father.getFriends().add( friend );
father.getFriends().add( friend2 );
session.save( mother );
session.save( father );
session.save( child1 );
session.save( child2 );
session.save( friend );
session.save( friend2 );
txn.commit();
session.close();