2929import java .util .List ;
3030import java .util .Locale ;
3131import java .util .Map ;
32+ import java .util .Objects ;
3233import java .util .Set ;
3334import java .util .regex .Matcher ;
3435import java .util .regex .Pattern ;
4142import javax .persistence .Query ;
4243import javax .persistence .criteria .CriteriaBuilder ;
4344import javax .persistence .criteria .Expression ;
44- import javax .persistence .criteria .Fetch ;
4545import javax .persistence .criteria .From ;
4646import javax .persistence .criteria .Join ;
4747import javax .persistence .criteria .JoinType ;
48- import javax .persistence .criteria .Path ;
4948import javax .persistence .metamodel .Attribute ;
5049import javax .persistence .metamodel .Attribute .PersistentAttributeType ;
5150import javax .persistence .metamodel .Bindable ;
5251import javax .persistence .metamodel .ManagedType ;
5352import javax .persistence .metamodel .PluralAttribute ;
53+ import javax .persistence .metamodel .SingularAttribute ;
5454
5555import org .springframework .core .annotation .AnnotationUtils ;
5656import org .springframework .dao .InvalidDataAccessApiUsageException ;
@@ -620,47 +620,96 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
620620 return toExpressionRecursively (from , property , false );
621621 }
622622
623- @ SuppressWarnings ("unchecked" )
624623 static <T > Expression <T > toExpressionRecursively (From <?, ?> from , PropertyPath property , boolean isForSelection ) {
624+ return toExpressionRecursively (from , property , isForSelection , false );
625+ }
626+
627+ /**
628+ * Creates an expression with proper inner and left joins by recursively navigating the path
629+ *
630+ * @param from the {@link From}
631+ * @param property the property path
632+ * @param isForSelection is the property navigated for the selection or ordering part of the query?
633+ * @param hasRequiredOuterJoin has a parent already required an outer join?
634+ * @param <T> the type of the expression
635+ * @return the expression
636+ */
637+ @ SuppressWarnings ("unchecked" ) static <T > Expression <T > toExpressionRecursively (From <?, ?> from ,
638+ PropertyPath property , boolean isForSelection , boolean hasRequiredOuterJoin ) {
625639
626- Bindable <?> propertyPathModel ;
627- Bindable <?> model = from .getModel ();
628640 String segment = property .getSegment ();
629641
630- if ( model instanceof ManagedType ) {
642+ boolean isLeafProperty = ! property . hasNext ();
631643
632- /*
633- * Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
634- * See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
635- */
636- propertyPathModel = (Bindable <?>) ((ManagedType <?>) model ).getAttribute (segment );
637- } else {
638- propertyPathModel = from .get (segment ).getModel ();
644+ boolean requiresOuterJoin = requiresOuterJoin (from , property , isForSelection , hasRequiredOuterJoin );
645+
646+ // if it does not require an outer join and is a leaf, simply get the segment
647+ if (!requiresOuterJoin && isLeafProperty ) {
648+ return from .get (segment );
639649 }
640650
641- if (requiresOuterJoin (propertyPathModel , model instanceof PluralAttribute , !property .hasNext (), isForSelection )
642- && !isAlreadyFetched (from , segment )) {
643- Join <?, ?> join = getOrCreateJoin (from , segment );
644- return (Expression <T >) (property .hasNext () ? toExpressionRecursively (join , property .next (), isForSelection )
645- : join );
646- } else {
647- Path <Object > path = from .get (segment );
648- return (Expression <T >) (property .hasNext () ? toExpressionRecursively (path , property .next ()) : path );
651+ // get or create the join
652+ JoinType joinType = requiresOuterJoin ? JoinType .LEFT : JoinType .INNER ;
653+ Join <?, ?> join = getOrCreateJoin (from , segment , joinType );
654+
655+ // if it's a leaf, return the join
656+ if (isLeafProperty ) {
657+ return (Expression <T >) join ;
649658 }
659+
660+ PropertyPath nextProperty = Objects .requireNonNull (property .next (), "An element of the property path is null!" );
661+
662+ // recurse with the next property
663+ return toExpressionRecursively (join , nextProperty , isForSelection , requiresOuterJoin );
650664 }
651665
652666 /**
653- * Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
654- * optional association.
667+ * Checks if this attribute requires an outer join.
668+ * This is the case eg. if it hadn't already been fetched with an inner join and if it's an a optional association,
669+ * and if previous paths has already required outer joins.
670+ * It also ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999).
655671 *
656- * @param propertyPathModel may be {@literal null}.
657- * @param isPluralAttribute is the attribute of Collection type?
658- * @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
659- * @param isForSelection is the property navigated for the selection part of the query?
672+ * @param from the {@link From} to check for fetches.
673+ * @param property the property path
674+ * @param isForSelection is the property navigated for the selection or ordering part of the query? if true,
675+ * we need to generate an explicit outer join in order to prevent Hibernate to use an
676+ * inner join instead. see https://hibernate.atlassian.net/browse/HHH-12999
677+ * @param hasRequiredOuterJoin has a parent already required an outer join?
660678 * @return whether an outer join is to be used for integrating this attribute in a query.
661679 */
662- private static boolean requiresOuterJoin (@ Nullable Bindable <?> propertyPathModel , boolean isPluralAttribute ,
663- boolean isLeafProperty , boolean isForSelection ) {
680+ private static boolean requiresOuterJoin (From <?, ?> from , PropertyPath property , boolean isForSelection ,
681+ boolean hasRequiredOuterJoin ) {
682+
683+ String segment = property .getSegment ();
684+
685+ // already inner joined so outer join is useless
686+ if (isAlreadyInnerJoined (from , segment ))
687+ return false ;
688+
689+ Bindable <?> propertyPathModel ;
690+ Bindable <?> model = from .getModel ();
691+
692+ // required for EclipseLink: we try to avoid using from.get as EclipseLink produces an inner join
693+ // regardless of which join operation is specified next
694+ // see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
695+ // still occurs as of 2.7
696+ ManagedType <?> managedType = null ;
697+ if (model instanceof ManagedType ) {
698+ managedType = (ManagedType <?>) model ;
699+ } else if (model instanceof SingularAttribute
700+ && ((SingularAttribute <?, ?>) model ).getType () instanceof ManagedType ) {
701+ managedType = (ManagedType <?>) ((SingularAttribute <?, ?>) model ).getType ();
702+ }
703+ if (managedType != null ) {
704+ propertyPathModel = (Bindable <?>) managedType .getAttribute (segment );
705+ } else {
706+ propertyPathModel = from .get (segment ).getModel ();
707+ }
708+
709+ // is the attribute of Collection type?
710+ boolean isPluralAttribute = model instanceof PluralAttribute ;
711+
712+ boolean isLeafProperty = !property .hasNext ();
664713
665714 if (propertyPathModel == null && isPluralAttribute ) {
666715 return true ;
@@ -672,24 +721,23 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
672721
673722 Attribute <?, ?> attribute = (Attribute <?, ?>) propertyPathModel ;
674723
724+ // not a persistent attribute type association (@OneToOne, @ManyToOne)
675725 if (!ASSOCIATION_TYPES .containsKey (attribute .getPersistentAttributeType ())) {
676726 return false ;
677727 }
678728
679- // if this path is an optional one to one attribute navigated from the not owning side we also need an explicit
680- // outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712 and
681- // https://github.com/eclipse-ee4j/jpa-api/issues/170
729+ boolean isCollection = attribute .isCollection ();
730+ // if this path is an optional one to one attribute navigated from the not owning side we also need an
731+ // explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
732+ // and https://github.com/eclipse-ee4j/jpa-api/issues/170
682733 boolean isInverseOptionalOneToOne = PersistentAttributeType .ONE_TO_ONE == attribute .getPersistentAttributeType ()
683734 && StringUtils .hasText (getAnnotationProperty (attribute , "mappedBy" , "" ));
684735
685- // if this path is part of the select list we need to generate an explicit outer join in order to prevent Hibernate
686- // to use an inner join instead.
687- // see https://hibernate.atlassian.net/browse/HHH-12999.
688- if (isLeafProperty && !isForSelection && !attribute .isCollection () && !isInverseOptionalOneToOne ) {
736+ if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin ) {
689737 return false ;
690738 }
691739
692- return getAnnotationProperty (attribute , "optional" , true );
740+ return hasRequiredOuterJoin || getAnnotationProperty (attribute , "optional" , true );
693741 }
694742
695743 private static <T > T getAnnotationProperty (Attribute <?, ?> attribute , String propertyName , T defaultValue ) {
@@ -710,52 +758,37 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
710758 return annotation == null ? defaultValue : (T ) AnnotationUtils .getValue (annotation , propertyName );
711759 }
712760
713- static Expression <Object > toExpressionRecursively (Path <Object > path , PropertyPath property ) {
714-
715- Path <Object > result = path .get (property .getSegment ());
716- return property .hasNext () ? toExpressionRecursively (result , property .next ()) : result ;
717- }
718-
719761 /**
720762 * Returns an existing join for the given attribute if one already exists or creates a new one if not.
721763 *
722- * @param from the {@link From} to get the current joins from.
764+ * @param from the {@link From} to get the current joins from.
723765 * @param attribute the {@link Attribute} to look for in the current joins.
766+ * @param joinType the join type to create if none was found
724767 * @return will never be {@literal null}.
725768 */
726- private static Join <?, ?> getOrCreateJoin (From <?, ?> from , String attribute ) {
727-
728- for (Join <?, ?> join : from .getJoins ()) {
729-
730- boolean sameName = join .getAttribute ().getName ().equals (attribute );
731-
732- if (sameName && join .getJoinType ().equals (JoinType .LEFT )) {
733- return join ;
734- }
735- }
736-
737- return from .join (attribute , JoinType .LEFT );
769+ private static Join <?, ?> getOrCreateJoin (From <?, ?> from , String attribute , JoinType joinType ) {
770+ return from .getJoins ().stream ()
771+ .filter (join -> join .getAttribute ().getName ().equals (attribute ))
772+ .findFirst ()
773+ .orElseGet (() -> from .join (attribute , joinType ));
738774 }
739775
740776 /**
741- * Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
777+ * Return whether the given {@link From} contains an inner join for the attribute with the given name.
742778 *
743- * @param from the {@link From} to check for fetches .
779+ * @param from the {@link From} to check for joins .
744780 * @param attribute the attribute name to check.
745- * @return
781+ * @return true if the attribute has already been inner joined
746782 */
747- private static boolean isAlreadyFetched (From <?, ?> from , String attribute ) {
783+ private static boolean isAlreadyInnerJoined (From <?, ?> from , String attribute ) {
748784
749- for (Fetch <?, ?> fetch : from .getFetches ()) {
785+ boolean isInnerJoinFetched = from .getFetches ().stream ().anyMatch (
786+ fetch -> fetch .getAttribute ().getName ().equals (attribute ) && fetch .getJoinType ().equals (JoinType .INNER ));
750787
751- boolean sameName = fetch .getAttribute ().getName ().equals (attribute );
788+ boolean isSimplyInnerJoined = from .getJoins ().stream ()
789+ .anyMatch (join -> join .getAttribute ().getName ().equals (attribute ) && join .getJoinType ().equals (JoinType .INNER ));
752790
753- if (sameName && fetch .getJoinType ().equals (JoinType .LEFT )) {
754- return true ;
755- }
756- }
757-
758- return false ;
791+ return isInnerJoinFetched || isSimplyInnerJoined ;
759792 }
760793
761794 /**
0 commit comments