Skip to content

Commit 66ab882

Browse files
pblanchardieschauder
authored andcommitted
Use left outer joins for nested optionals.
Original pull request #436
1 parent 14c1de3 commit 66ab882

File tree

6 files changed

+272
-73
lines changed

6 files changed

+272
-73
lines changed

src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

+101-68
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.List;
3030
import java.util.Locale;
3131
import java.util.Map;
32+
import java.util.Objects;
3233
import java.util.Set;
3334
import java.util.regex.Matcher;
3435
import java.util.regex.Pattern;
@@ -41,16 +42,15 @@
4142
import javax.persistence.Query;
4243
import javax.persistence.criteria.CriteriaBuilder;
4344
import javax.persistence.criteria.Expression;
44-
import javax.persistence.criteria.Fetch;
4545
import javax.persistence.criteria.From;
4646
import javax.persistence.criteria.Join;
4747
import javax.persistence.criteria.JoinType;
48-
import javax.persistence.criteria.Path;
4948
import javax.persistence.metamodel.Attribute;
5049
import javax.persistence.metamodel.Attribute.PersistentAttributeType;
5150
import javax.persistence.metamodel.Bindable;
5251
import javax.persistence.metamodel.ManagedType;
5352
import javax.persistence.metamodel.PluralAttribute;
53+
import javax.persistence.metamodel.SingularAttribute;
5454

5555
import org.springframework.core.annotation.AnnotationUtils;
5656
import 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
/**

src/test/java/org/springframework/data/jpa/domain/sample/Customer.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020

2121
/**
2222
* @author Oliver Gierke
23+
* @author Patrice Blanchardie
2324
*/
2425
@Entity
2526
public class Customer {
2627

27-
@Id Long id;
28+
@Id Long id;
29+
30+
String name;
2831
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain.sample;
17+
18+
import javax.persistence.Entity;
19+
import javax.persistence.Id;
20+
import javax.persistence.ManyToOne;
21+
import javax.persistence.Table;
22+
23+
/**
24+
* @author Patrice Blanchardie
25+
*/
26+
@Entity
27+
@Table(name = "INVOICES")
28+
public class Invoice {
29+
30+
@Id Long id;
31+
32+
@ManyToOne(optional = false) Customer customer;
33+
34+
@ManyToOne Order order;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain.sample;
17+
18+
import javax.persistence.Entity;
19+
import javax.persistence.Id;
20+
import javax.persistence.ManyToOne;
21+
import javax.persistence.Table;
22+
23+
/**
24+
* @author Patrice Blanchardie
25+
*/
26+
@Entity
27+
@Table(name = "INVOICE_ITEMS")
28+
public class InvoiceItem {
29+
30+
@Id Long id;
31+
32+
@ManyToOne(optional = false) Invoice invoice;
33+
}

0 commit comments

Comments
 (0)