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 ;
@@ -619,47 +619,96 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
619
619
return toExpressionRecursively (from , property , false );
620
620
}
621
621
622
- @ SuppressWarnings ("unchecked" )
623
622
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 ) {
624
638
625
- Bindable <?> propertyPathModel ;
626
- Bindable <?> model = from .getModel ();
627
639
String segment = property .getSegment ();
628
640
629
- if ( model instanceof ManagedType ) {
641
+ boolean isLeafProperty = ! property . hasNext ();
630
642
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 );
638
648
}
639
649
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 ;
648
657
}
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 );
649
663
}
650
664
651
665
/**
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).
654
670
*
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?
659
677
* @return whether an outer join is to be used for integrating this attribute in a query.
660
678
*/
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 ();
663
712
664
713
if (propertyPathModel == null && isPluralAttribute ) {
665
714
return true ;
@@ -671,24 +720,23 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
671
720
672
721
Attribute <?, ?> attribute = (Attribute <?, ?>) propertyPathModel ;
673
722
723
+ // not a persistent attribute type association (@OneToOne, @ManyToOne)
674
724
if (!ASSOCIATION_TYPES .containsKey (attribute .getPersistentAttributeType ())) {
675
725
return false ;
676
726
}
677
727
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
681
732
boolean isInverseOptionalOneToOne = PersistentAttributeType .ONE_TO_ONE == attribute .getPersistentAttributeType ()
682
733
&& StringUtils .hasText (getAnnotationProperty (attribute , "mappedBy" , "" ));
683
734
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 ) {
688
736
return false ;
689
737
}
690
738
691
- return getAnnotationProperty (attribute , "optional" , true );
739
+ return hasRequiredOuterJoin || getAnnotationProperty (attribute , "optional" , true );
692
740
}
693
741
694
742
private static <T > T getAnnotationProperty (Attribute <?, ?> attribute , String propertyName , T defaultValue ) {
@@ -709,52 +757,37 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
709
757
return annotation == null ? defaultValue : (T ) AnnotationUtils .getValue (annotation , propertyName );
710
758
}
711
759
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
-
718
760
/**
719
761
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
720
762
*
721
- * @param from the {@link From} to get the current joins from.
763
+ * @param from the {@link From} to get the current joins from.
722
764
* @param attribute the {@link Attribute} to look for in the current joins.
765
+ * @param joinType the join type to create if none was found
723
766
* @return will never be {@literal null}.
724
767
*/
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 ));
737
773
}
738
774
739
775
/**
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.
741
777
*
742
- * @param from the {@link From} to check for fetches .
778
+ * @param from the {@link From} to check for joins .
743
779
* @param attribute the attribute name to check.
744
- * @return
780
+ * @return true if the attribute has already been inner joined
745
781
*/
746
- private static boolean isAlreadyFetched (From <?, ?> from , String attribute ) {
782
+ private static boolean isAlreadyInnerJoined (From <?, ?> from , String attribute ) {
747
783
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 ));
749
786
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 ));
751
789
752
- if (sameName && fetch .getJoinType ().equals (JoinType .LEFT )) {
753
- return true ;
754
- }
755
- }
756
-
757
- return false ;
790
+ return isInnerJoinFetched || isSimplyInnerJoined ;
758
791
}
759
792
760
793
/**
0 commit comments