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 ;
@@ -620,12 +619,74 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
620
619
return toExpressionRecursively (from , property , false );
621
620
}
622
621
623
- @ SuppressWarnings ("unchecked" )
624
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 ) {
638
+
639
+ String segment = property .getSegment ();
640
+
641
+ boolean isLeafProperty = !property .hasNext ();
642
+
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 );
648
+ }
649
+
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 ;
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 );
663
+ }
664
+
665
+ /**
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).
670
+ *
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?
677
+ * @return whether an outer join is to be used for integrating this attribute in a query.
678
+ */
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 ;
625
687
626
688
Bindable <?> propertyPathModel ;
627
689
Bindable <?> model = from .getModel ();
628
- String segment = property .getSegment ();
629
690
630
691
if (model instanceof ManagedType ) {
631
692
@@ -638,29 +699,10 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
638
699
propertyPathModel = from .get (segment ).getModel ();
639
700
}
640
701
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 );
649
- }
650
- }
702
+ // is the attribute of Collection type?
703
+ boolean isPluralAttribute = model instanceof PluralAttribute ;
651
704
652
- /**
653
- * Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
654
- * optional association.
655
- *
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?
660
- * @return whether an outer join is to be used for integrating this attribute in a query.
661
- */
662
- private static boolean requiresOuterJoin (@ Nullable Bindable <?> propertyPathModel , boolean isPluralAttribute ,
663
- boolean isLeafProperty , boolean isForSelection ) {
705
+ boolean isLeafProperty = !property .hasNext ();
664
706
665
707
if (propertyPathModel == null && isPluralAttribute ) {
666
708
return true ;
@@ -672,24 +714,23 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
672
714
673
715
Attribute <?, ?> attribute = (Attribute <?, ?>) propertyPathModel ;
674
716
717
+ // not a persistent attribute type association (@OneToOne, @ManyToOne)
675
718
if (!ASSOCIATION_TYPES .containsKey (attribute .getPersistentAttributeType ())) {
676
719
return false ;
677
720
}
678
721
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
722
+ boolean isCollection = attribute .isCollection ();
723
+ // if this path is an optional one to one attribute navigated from the not owning side we also need an
724
+ // explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
725
+ // and https://github.com/eclipse-ee4j/jpa-api/issues/170
682
726
boolean isInverseOptionalOneToOne = PersistentAttributeType .ONE_TO_ONE == attribute .getPersistentAttributeType ()
683
727
&& StringUtils .hasText (getAnnotationProperty (attribute , "mappedBy" , "" ));
684
728
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 ) {
729
+ if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin ) {
689
730
return false ;
690
731
}
691
732
692
- return getAnnotationProperty (attribute , "optional" , true );
733
+ return hasRequiredOuterJoin || getAnnotationProperty (attribute , "optional" , true );
693
734
}
694
735
695
736
private static <T > T getAnnotationProperty (Attribute <?, ?> attribute , String propertyName , T defaultValue ) {
@@ -710,52 +751,37 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
710
751
return annotation == null ? defaultValue : (T ) AnnotationUtils .getValue (annotation , propertyName );
711
752
}
712
753
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
754
/**
720
- * Returns an existing join for the given attribute if one already exists or creates a new one if not.
755
+ * Returns an existing join for the given attribute and join type if one already exists or creates a new one if not.
721
756
*
722
- * @param from the {@link From} to get the current joins from.
757
+ * @param from the {@link From} to get the current joins from.
723
758
* @param attribute the {@link Attribute} to look for in the current joins.
759
+ * @param joinType the join type
724
760
* @return will never be {@literal null}.
725
761
*/
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 );
762
+ private static Join <?, ?> getOrCreateJoin (From <?, ?> from , String attribute , JoinType joinType ) {
763
+ return from .getJoins ().stream ()
764
+ .filter (join -> join .getAttribute ().getName ().equals (attribute ) && join .getJoinType ().equals (joinType ))
765
+ .findFirst ()
766
+ .orElseGet (() -> from .join (attribute , joinType ));
738
767
}
739
768
740
769
/**
741
- * Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
770
+ * Return whether the given {@link From} contains an inner join for the attribute with the given name.
742
771
*
743
- * @param from the {@link From} to check for fetches .
772
+ * @param from the {@link From} to check for joins .
744
773
* @param attribute the attribute name to check.
745
- * @return
774
+ * @return true if the attribute has already been inner joined
746
775
*/
747
- private static boolean isAlreadyFetched (From <?, ?> from , String attribute ) {
748
-
749
- for (Fetch <?, ?> fetch : from .getFetches ()) {
776
+ private static boolean isAlreadyInnerJoined (From <?, ?> from , String attribute ) {
750
777
751
- boolean sameName = fetch .getAttribute ().getName ().equals (attribute );
778
+ boolean isInnerJoinFetched = from .getFetches ().stream ().anyMatch (
779
+ fetch -> fetch .getAttribute ().getName ().equals (attribute ) && fetch .getJoinType ().equals (JoinType .INNER ));
752
780
753
- if (sameName && fetch .getJoinType ().equals (JoinType .LEFT )) {
754
- return true ;
755
- }
756
- }
781
+ boolean isSimplyInnerJoined = from .getJoins ().stream ()
782
+ .anyMatch (join -> join .getAttribute ().getName ().equals (attribute ) && join .getJoinType ().equals (JoinType .INNER ));
757
783
758
- return false ;
784
+ return isInnerJoinFetched || isSimplyInnerJoined ;
759
785
}
760
786
761
787
/**
0 commit comments