29
29
import java .util .List ;
30
30
import java .util .Locale ;
31
31
import java .util .Map ;
32
+ import java .util .Objects ;
32
33
import java .util .Set ;
33
34
import java .util .regex .Matcher ;
34
35
import java .util .regex .Pattern ;
41
42
import javax .persistence .Query ;
42
43
import javax .persistence .criteria .CriteriaBuilder ;
43
44
import javax .persistence .criteria .Expression ;
44
- import javax .persistence .criteria .Fetch ;
45
45
import javax .persistence .criteria .From ;
46
46
import javax .persistence .criteria .Join ;
47
47
import javax .persistence .criteria .JoinType ;
48
- import javax .persistence .criteria .Path ;
49
48
import javax .persistence .metamodel .Attribute ;
50
49
import javax .persistence .metamodel .Attribute .PersistentAttributeType ;
51
50
import javax .persistence .metamodel .Bindable ;
52
51
import javax .persistence .metamodel .ManagedType ;
53
52
import javax .persistence .metamodel .PluralAttribute ;
53
+ import javax .persistence .metamodel .SingularAttribute ;
54
54
55
55
import org .springframework .core .annotation .AnnotationUtils ;
56
56
import org .springframework .dao .InvalidDataAccessApiUsageException ;
@@ -620,47 +620,96 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
620
620
return toExpressionRecursively (from , property , false );
621
621
}
622
622
623
- @ SuppressWarnings ("unchecked" )
624
623
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 ) {
625
639
626
- Bindable <?> propertyPathModel ;
627
- Bindable <?> model = from .getModel ();
628
640
String segment = property .getSegment ();
629
641
630
- if ( model instanceof ManagedType ) {
642
+ boolean isLeafProperty = ! property . hasNext ();
631
643
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 );
639
649
}
640
650
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 ;
649
658
}
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 );
650
664
}
651
665
652
666
/**
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).
655
671
*
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?
660
678
* @return whether an outer join is to be used for integrating this attribute in a query.
661
679
*/
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 ();
664
713
665
714
if (propertyPathModel == null && isPluralAttribute ) {
666
715
return true ;
@@ -672,24 +721,23 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
672
721
673
722
Attribute <?, ?> attribute = (Attribute <?, ?>) propertyPathModel ;
674
723
724
+ // not a persistent attribute type association (@OneToOne, @ManyToOne)
675
725
if (!ASSOCIATION_TYPES .containsKey (attribute .getPersistentAttributeType ())) {
676
726
return false ;
677
727
}
678
728
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
682
733
boolean isInverseOptionalOneToOne = PersistentAttributeType .ONE_TO_ONE == attribute .getPersistentAttributeType ()
683
734
&& StringUtils .hasText (getAnnotationProperty (attribute , "mappedBy" , "" ));
684
735
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 ) {
689
737
return false ;
690
738
}
691
739
692
- return getAnnotationProperty (attribute , "optional" , true );
740
+ return hasRequiredOuterJoin || getAnnotationProperty (attribute , "optional" , true );
693
741
}
694
742
695
743
private static <T > T getAnnotationProperty (Attribute <?, ?> attribute , String propertyName , T defaultValue ) {
@@ -710,52 +758,37 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
710
758
return annotation == null ? defaultValue : (T ) AnnotationUtils .getValue (annotation , propertyName );
711
759
}
712
760
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
-
719
761
/**
720
762
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
721
763
*
722
- * @param from the {@link From} to get the current joins from.
764
+ * @param from the {@link From} to get the current joins from.
723
765
* @param attribute the {@link Attribute} to look for in the current joins.
766
+ * @param joinType the join type to create if none was found
724
767
* @return will never be {@literal null}.
725
768
*/
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 ));
738
774
}
739
775
740
776
/**
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.
742
778
*
743
- * @param from the {@link From} to check for fetches .
779
+ * @param from the {@link From} to check for joins .
744
780
* @param attribute the attribute name to check.
745
- * @return
781
+ * @return true if the attribute has already been inner joined
746
782
*/
747
- private static boolean isAlreadyFetched (From <?, ?> from , String attribute ) {
783
+ private static boolean isAlreadyInnerJoined (From <?, ?> from , String attribute ) {
748
784
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 ));
750
787
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 ));
752
790
753
- if (sameName && fetch .getJoinType ().equals (JoinType .LEFT )) {
754
- return true ;
755
- }
756
- }
757
-
758
- return false ;
791
+ return isInnerJoinFetched || isSimplyInnerJoined ;
759
792
}
760
793
761
794
/**
0 commit comments