diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/JoinSequence.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/JoinSequence.java index 1e8c204825..3982c465f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/JoinSequence.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/JoinSequence.java @@ -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 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(); } diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/WithClauseTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/WithClauseTest.java index 2c873e7424..1970eb4e76 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/hql/WithClauseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/WithClauseTest.java @@ -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();