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 ;
@@ -619,47 +619,96 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
619619 return toExpressionRecursively (from , property , false );
620620 }
621621
622- @ SuppressWarnings ("unchecked" )
623622 static <T > Expression <T > toExpressionRecursively (From <?, ?> from , PropertyPath property , boolean isForSelection ) {
623+ return toExpressionRecursively (from , property , isForSelection , false );
624+ }
625+
626+ /**
627+ * Creates an expression with proper inner and left joins by recursively navigating the path
628+ *
629+ * @param from the {@link From}
630+ * @param property the property path
631+ * @param isForSelection is the property navigated for the selection or ordering part of the query?
632+ * @param hasRequiredOuterJoin has a parent already required an outer join?
633+ * @param <T> the type of the expression
634+ * @return the expression
635+ */
636+ @ SuppressWarnings ("unchecked" ) static <T > Expression <T > toExpressionRecursively (From <?, ?> from ,
637+ PropertyPath property , boolean isForSelection , boolean hasRequiredOuterJoin ) {
624638
625- Bindable <?> propertyPathModel ;
626- Bindable <?> model = from .getModel ();
627639 String segment = property .getSegment ();
628640
629- if ( model instanceof ManagedType ) {
641+ boolean isLeafProperty = ! property . hasNext ();
630642
631- /*
632- * Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
633- * See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
634- */
635- propertyPathModel = (Bindable <?>) ((ManagedType <?>) model ).getAttribute (segment );
636- } else {
637- propertyPathModel = from .get (segment ).getModel ();
643+ boolean requiresOuterJoin = requiresOuterJoin (from , property , isForSelection , hasRequiredOuterJoin );
644+
645+ // if it does not require an outer join and is a leaf, simply get the segment
646+ if (!requiresOuterJoin && isLeafProperty ) {
647+ return from .get (segment );
638648 }
639649
640- if (requiresOuterJoin (propertyPathModel , model instanceof PluralAttribute , !property .hasNext (), isForSelection )
641- && !isAlreadyFetched (from , segment )) {
642- Join <?, ?> join = getOrCreateJoin (from , segment );
643- return (Expression <T >) (property .hasNext () ? toExpressionRecursively (join , property .next (), isForSelection )
644- : join );
645- } else {
646- Path <Object > path = from .get (segment );
647- return (Expression <T >) (property .hasNext () ? toExpressionRecursively (path , property .next ()) : path );
650+ // get or create the join
651+ JoinType joinType = requiresOuterJoin ? JoinType .LEFT : JoinType .INNER ;
652+ Join <?, ?> join = getOrCreateJoin (from , segment , joinType );
653+
654+ // if it's a leaf, return the join
655+ if (isLeafProperty ) {
656+ return (Expression <T >) join ;
648657 }
658+
659+ PropertyPath nextProperty = Objects .requireNonNull (property .next (), "An element of the property path is null!" );
660+
661+ // recurse with the next property
662+ return toExpressionRecursively (join , nextProperty , isForSelection , requiresOuterJoin );
649663 }
650664
651665 /**
652- * Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
653- * optional association.
666+ * Checks if this attribute requires an outer join.
667+ * This is the case eg. if it hadn't already been fetched with an inner join and if it's an a optional association,
668+ * and if previous paths has already required outer joins.
669+ * It also ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999).
654670 *
655- * @param propertyPathModel may be {@literal null}.
656- * @param isPluralAttribute is the attribute of Collection type?
657- * @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
658- * @param isForSelection is the property navigated for the selection part of the query?
671+ * @param from the {@link From} to check for fetches.
672+ * @param property the property path
673+ * @param isForSelection is the property navigated for the selection or ordering part of the query? if true,
674+ * we need to generate an explicit outer join in order to prevent Hibernate to use an
675+ * inner join instead. see https://hibernate.atlassian.net/browse/HHH-12999
676+ * @param hasRequiredOuterJoin has a parent already required an outer join?
659677 * @return whether an outer join is to be used for integrating this attribute in a query.
660678 */
661- private static boolean requiresOuterJoin (@ Nullable Bindable <?> propertyPathModel , boolean isPluralAttribute ,
662- boolean isLeafProperty , boolean isForSelection ) {
679+ private static boolean requiresOuterJoin (From <?, ?> from , PropertyPath property , boolean isForSelection ,
680+ boolean hasRequiredOuterJoin ) {
681+
682+ String segment = property .getSegment ();
683+
684+ // already inner joined so outer join is useless
685+ if (isAlreadyInnerJoined (from , segment ))
686+ return false ;
687+
688+ Bindable <?> propertyPathModel ;
689+ Bindable <?> model = from .getModel ();
690+
691+ // required for EclipseLink: we try to avoid using from.get as EclipseLink produces an inner join
692+ // regardless of which join operation is specified next
693+ // see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
694+ // still occurs as of 2.7
695+ ManagedType <?> managedType = null ;
696+ if (model instanceof ManagedType ) {
697+ managedType = (ManagedType <?>) model ;
698+ } else if (model instanceof SingularAttribute
699+ && ((SingularAttribute <?, ?>) model ).getType () instanceof ManagedType ) {
700+ managedType = (ManagedType <?>) ((SingularAttribute <?, ?>) model ).getType ();
701+ }
702+ if (managedType != null ) {
703+ propertyPathModel = (Bindable <?>) managedType .getAttribute (segment );
704+ } else {
705+ propertyPathModel = from .get (segment ).getModel ();
706+ }
707+
708+ // is the attribute of Collection type?
709+ boolean isPluralAttribute = model instanceof PluralAttribute ;
710+
711+ boolean isLeafProperty = !property .hasNext ();
663712
664713 if (propertyPathModel == null && isPluralAttribute ) {
665714 return true ;
@@ -671,24 +720,23 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
671720
672721 Attribute <?, ?> attribute = (Attribute <?, ?>) propertyPathModel ;
673722
723+ // not a persistent attribute type association (@OneToOne, @ManyToOne)
674724 if (!ASSOCIATION_TYPES .containsKey (attribute .getPersistentAttributeType ())) {
675725 return false ;
676726 }
677727
678- // if this path is an optional one to one attribute navigated from the not owning side we also need an explicit
679- // outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712 and
680- // https://github.com/eclipse-ee4j/jpa-api/issues/170
728+ boolean isCollection = attribute .isCollection ();
729+ // if this path is an optional one to one attribute navigated from the not owning side we also need an
730+ // explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
731+ // and https://github.com/eclipse-ee4j/jpa-api/issues/170
681732 boolean isInverseOptionalOneToOne = PersistentAttributeType .ONE_TO_ONE == attribute .getPersistentAttributeType ()
682733 && StringUtils .hasText (getAnnotationProperty (attribute , "mappedBy" , "" ));
683734
684- // if this path is part of the select list we need to generate an explicit outer join in order to prevent Hibernate
685- // to use an inner join instead.
686- // see https://hibernate.atlassian.net/browse/HHH-12999.
687- if (isLeafProperty && !isForSelection && !attribute .isCollection () && !isInverseOptionalOneToOne ) {
735+ if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin ) {
688736 return false ;
689737 }
690738
691- return getAnnotationProperty (attribute , "optional" , true );
739+ return hasRequiredOuterJoin || getAnnotationProperty (attribute , "optional" , true );
692740 }
693741
694742 private static <T > T getAnnotationProperty (Attribute <?, ?> attribute , String propertyName , T defaultValue ) {
@@ -709,52 +757,37 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
709757 return annotation == null ? defaultValue : (T ) AnnotationUtils .getValue (annotation , propertyName );
710758 }
711759
712- static Expression <Object > toExpressionRecursively (Path <Object > path , PropertyPath property ) {
713-
714- Path <Object > result = path .get (property .getSegment ());
715- return property .hasNext () ? toExpressionRecursively (result , property .next ()) : result ;
716- }
717-
718760 /**
719761 * Returns an existing join for the given attribute if one already exists or creates a new one if not.
720762 *
721- * @param from the {@link From} to get the current joins from.
763+ * @param from the {@link From} to get the current joins from.
722764 * @param attribute the {@link Attribute} to look for in the current joins.
765+ * @param joinType the join type to create if none was found
723766 * @return will never be {@literal null}.
724767 */
725- private static Join <?, ?> getOrCreateJoin (From <?, ?> from , String attribute ) {
726-
727- for (Join <?, ?> join : from .getJoins ()) {
728-
729- boolean sameName = join .getAttribute ().getName ().equals (attribute );
730-
731- if (sameName && join .getJoinType ().equals (JoinType .LEFT )) {
732- return join ;
733- }
734- }
735-
736- return from .join (attribute , JoinType .LEFT );
768+ private static Join <?, ?> getOrCreateJoin (From <?, ?> from , String attribute , JoinType joinType ) {
769+ return from .getJoins ().stream ()
770+ .filter (join -> join .getAttribute ().getName ().equals (attribute ))
771+ .findFirst ()
772+ .orElseGet (() -> from .join (attribute , joinType ));
737773 }
738774
739775 /**
740- * Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
776+ * Return whether the given {@link From} contains an inner join for the attribute with the given name.
741777 *
742- * @param from the {@link From} to check for fetches .
778+ * @param from the {@link From} to check for joins .
743779 * @param attribute the attribute name to check.
744- * @return
780+ * @return true if the attribute has already been inner joined
745781 */
746- private static boolean isAlreadyFetched (From <?, ?> from , String attribute ) {
782+ private static boolean isAlreadyInnerJoined (From <?, ?> from , String attribute ) {
747783
748- for (Fetch <?, ?> fetch : from .getFetches ()) {
784+ boolean isInnerJoinFetched = from .getFetches ().stream ().anyMatch (
785+ fetch -> fetch .getAttribute ().getName ().equals (attribute ) && fetch .getJoinType ().equals (JoinType .INNER ));
749786
750- boolean sameName = fetch .getAttribute ().getName ().equals (attribute );
787+ boolean isSimplyInnerJoined = from .getJoins ().stream ()
788+ .anyMatch (join -> join .getAttribute ().getName ().equals (attribute ) && join .getJoinType ().equals (JoinType .INNER ));
751789
752- if (sameName && fetch .getJoinType ().equals (JoinType .LEFT )) {
753- return true ;
754- }
755- }
756-
757- return false ;
790+ return isInnerJoinFetched || isSimplyInnerJoined ;
758791 }
759792
760793 /**
0 commit comments