Skip to content

Commit 32b6163

Browse files
pblanchardieschauder
authored andcommitted
Use left outer joins for nested optionals.
Closes #2111 Original pull request #436
1 parent eb023fc commit 32b6163

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;
@@ -620,47 +620,96 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
620620
return toExpressionRecursively(from, property, false);
621621
}
622622

623-
@SuppressWarnings("unchecked")
624623
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) {
625639

626-
Bindable<?> propertyPathModel;
627-
Bindable<?> model = from.getModel();
628640
String segment = property.getSegment();
629641

630-
if (model instanceof ManagedType) {
642+
boolean isLeafProperty = !property.hasNext();
631643

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);
639649
}
640650

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;
649658
}
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);
650664
}
651665

652666
/**
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).
655671
*
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?
660678
* @return whether an outer join is to be used for integrating this attribute in a query.
661679
*/
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();
664713

665714
if (propertyPathModel == null && isPluralAttribute) {
666715
return true;
@@ -672,24 +721,23 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
672721

673722
Attribute<?, ?> attribute = (Attribute<?, ?>) propertyPathModel;
674723

724+
// not a persistent attribute type association (@OneToOne, @ManyToOne)
675725
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
676726
return false;
677727
}
678728

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
682733
boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
683734
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
684735

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) {
689737
return false;
690738
}
691739

692-
return getAnnotationProperty(attribute, "optional", true);
740+
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
693741
}
694742

695743
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
@@ -710,52 +758,37 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
710758
return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName);
711759
}
712760

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-
719761
/**
720762
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
721763
*
722-
* @param from the {@link From} to get the current joins from.
764+
* @param from the {@link From} to get the current joins from.
723765
* @param attribute the {@link Attribute} to look for in the current joins.
766+
* @param joinType the join type to create if none was found
724767
* @return will never be {@literal null}.
725768
*/
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));
738774
}
739775

740776
/**
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.
742778
*
743-
* @param from the {@link From} to check for fetches.
779+
* @param from the {@link From} to check for joins.
744780
* @param attribute the attribute name to check.
745-
* @return
781+
* @return true if the attribute has already been inner joined
746782
*/
747-
private static boolean isAlreadyFetched(From<?, ?> from, String attribute) {
783+
private static boolean isAlreadyInnerJoined(From<?, ?> from, String attribute) {
748784

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));
750787

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));
752790

753-
if (sameName && fetch.getJoinType().equals(JoinType.LEFT)) {
754-
return true;
755-
}
756-
}
757-
758-
return false;
791+
return isInnerJoinFetched || isSimplyInnerJoined;
759792
}
760793

761794
/**

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)