Skip to content

Commit 2deabac

Browse files
committed
1 parent e2db961 commit 2deabac

File tree

6 files changed

+148
-23
lines changed

6 files changed

+148
-23
lines changed

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

+24-14
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ public static String getProjection(String query) {
605605
private static javax.persistence.criteria.Order toJpaOrder(Order order, From<?, ?> from, CriteriaBuilder cb) {
606606

607607
PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType());
608-
Expression<?> expression = toExpressionRecursively(from, property);
608+
Expression<?> expression = toExpressionRecursively(from, property, true);
609609

610610
if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) {
611611
Expression<String> lower = cb.lower((Expression<String>) expression);
@@ -619,8 +619,12 @@ 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+
@SuppressWarnings("unchecked")
627+
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
624628

625629
Bindable<?> propertyPathModel;
626630
Bindable<?> model = from.getModel();
@@ -637,10 +641,14 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
637641
propertyPathModel = from.get(segment).getModel();
638642
}
639643

640-
if (requiresOuterJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(), isForSelection)
644+
if (requiresOuterJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(), isForSelection, hasRequiredOuterJoin)
641645
&& !isAlreadyFetched(from, segment)) {
642-
Join<?, ?> join = getOrCreateJoin(from, segment);
643-
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection)
646+
Join<?, ?> join = getOrCreateJoin(from, segment, JoinType.LEFT);
647+
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection, true)
648+
: join);
649+
} else if(property.hasNext()) {
650+
Join<?, ?> join = getOrCreateJoin(from, segment, JoinType.INNER);
651+
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection, false)
644652
: join);
645653
} else {
646654
Path<Object> path = from.get(segment);
@@ -655,11 +663,12 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
655663
* @param propertyPathModel may be {@literal null}.
656664
* @param isPluralAttribute is the attribute of Collection type?
657665
* @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?
666+
* @param isForSelection is the property navigated for the selection or ordering part of the query?
667+
* @param hasRequiredOuterJoin has a parent already required an outer join?
659668
* @return whether an outer join is to be used for integrating this attribute in a query.
660669
*/
661670
private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
662-
boolean isLeafProperty, boolean isForSelection) {
671+
boolean isLeafProperty, boolean isForSelection, boolean hasRequiredOuterJoin) {
663672

664673
if (propertyPathModel == null && isPluralAttribute) {
665674
return true;
@@ -681,14 +690,14 @@ private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel
681690
boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
682691
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
683692

684-
// if this path is part of the select list we need to generate an explicit outer join in order to prevent Hibernate
693+
// if this path is part of the select or order list we need to generate an explicit outer join in order to prevent Hibernate
685694
// to use an inner join instead.
686695
// see https://hibernate.atlassian.net/browse/HHH-12999.
687-
if (isLeafProperty && !isForSelection && !attribute.isCollection() && !isInverseOptionalOneToOne) {
696+
if (isLeafProperty && !isForSelection && !attribute.isCollection() && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) {
688697
return false;
689698
}
690699

691-
return getAnnotationProperty(attribute, "optional", true);
700+
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
692701
}
693702

694703
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
@@ -716,24 +725,25 @@ static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPat
716725
}
717726

718727
/**
719-
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
728+
* Returns an existing join for the given attribute and join type if one already exists or creates a new one if not.
720729
*
721730
* @param from the {@link From} to get the current joins from.
722731
* @param attribute the {@link Attribute} to look for in the current joins.
732+
* @param joinType the join type
723733
* @return will never be {@literal null}.
724734
*/
725-
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
735+
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute, JoinType joinType) {
726736

727737
for (Join<?, ?> join : from.getJoins()) {
728738

729739
boolean sameName = join.getAttribute().getName().equals(attribute);
730740

731-
if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
741+
if (sameName && join.getJoinType().equals(joinType)) {
732742
return join;
733743
}
734744
}
735745

736-
return from.join(attribute, JoinType.LEFT);
746+
return from.join(attribute, joinType);
737747
}
738748

739749
/**

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020

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

27-
@Id Long id;
28+
@Id
29+
Long id;
30+
31+
String name;
2832
}
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+
}

src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java

+49-8
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@
3333
import javax.persistence.OneToOne;
3434
import javax.persistence.Persistence;
3535
import javax.persistence.PersistenceContext;
36-
import javax.persistence.criteria.CriteriaBuilder;
37-
import javax.persistence.criteria.CriteriaQuery;
38-
import javax.persistence.criteria.Join;
39-
import javax.persistence.criteria.JoinType;
40-
import javax.persistence.criteria.Root;
36+
import javax.persistence.criteria.*;
4137
import javax.persistence.spi.PersistenceProvider;
4238
import javax.persistence.spi.PersistenceProviderResolver;
4339
import javax.persistence.spi.PersistenceProviderResolverHolder;
@@ -49,6 +45,8 @@
4945
import org.springframework.data.domain.Sort;
5046
import org.springframework.data.domain.Sort.Direction;
5147
import org.springframework.data.jpa.domain.sample.Category;
48+
import org.springframework.data.jpa.domain.sample.Invoice;
49+
import org.springframework.data.jpa.domain.sample.InvoiceItem;
5250
import org.springframework.data.jpa.domain.sample.Order;
5351
import org.springframework.data.jpa.domain.sample.User;
5452
import org.springframework.data.jpa.infrastructure.HibernateTestUtils;
@@ -61,6 +59,7 @@
6159
*
6260
* @author Oliver Gierke
6361
* @author Sébastien Péralta
62+
* @author Patrice Blanchardie
6463
*/
6564
@ExtendWith(SpringExtension.class)
6665
@ContextConfiguration("classpath:infrastructure.xml")
@@ -111,6 +110,35 @@ void createsJoinForOptionalOneToOneInReverseDirection() {
111110
});
112111
}
113112

113+
@Test // DATAJPA-1822
114+
void createsLeftJoinForOptionalToOneWithNestedNonOptional() {
115+
116+
CriteriaBuilder builder = em.getCriteriaBuilder();
117+
CriteriaQuery<Invoice> query = builder.createQuery(Invoice.class);
118+
Root<Invoice> root = query.from(Invoice.class);
119+
120+
QueryUtils.toExpressionRecursively(root, PropertyPath.from("order.customer.name", Invoice.class), false);
121+
122+
assertThat(getNonInnerJoins(root)).hasSize(1); // left join order
123+
assertThat(getNonInnerJoins(root.getJoins().iterator().next())).hasSize(1); // left join customer
124+
}
125+
126+
@Test // DATAJPA-1822
127+
void createsLeftJoinForNonOptionalToOneWithNestedOptional() {
128+
129+
CriteriaBuilder builder = em.getCriteriaBuilder();
130+
CriteriaQuery<InvoiceItem> query = builder.createQuery(InvoiceItem.class);
131+
Root<InvoiceItem> root = query.from(InvoiceItem.class);
132+
133+
QueryUtils.toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class), false);
134+
135+
assertThat(getInnerJoins(root)).hasSize(1); // join invoice
136+
Join<?, ?> rootInnerJoin = getInnerJoins(root).iterator().next();
137+
assertThat(getNonInnerJoins(rootInnerJoin)).hasSize(1); // left join order
138+
Join<?, ?> leftJoin = getNonInnerJoins(rootInnerJoin).iterator().next();
139+
assertThat(getNonInnerJoins(leftJoin)).hasSize(1); // left join customer
140+
}
141+
114142
@Test // DATAJPA-1404
115143
void createsNoJoinForOptionalOneToOneInNormalDirection() {
116144

@@ -248,9 +276,22 @@ int getNumberOfJoinsAfterCreatingAPath() {
248276

249277
private Set<Join<?, ?>> getNonInnerJoins(Root<?> root) {
250278

251-
return root.getJoins() //
252-
.stream() //
253-
.filter(j -> j.getJoinType() != JoinType.INNER) //
279+
return getNonInnerJoins((From<?,?>) root);
280+
}
281+
282+
private Set<Join<?, ?>> getNonInnerJoins(From<?,?> root) {
283+
284+
return root.getJoins()
285+
.stream()
286+
.filter(j -> j.getJoinType() != JoinType.INNER)
287+
.collect(Collectors.toSet());
288+
}
289+
290+
private Set<Join<?, ?>> getInnerJoins(From<?,?> root) {
291+
292+
return root.getJoins()
293+
.stream()
294+
.filter(j -> j.getJoinType() == JoinType.INNER)
254295
.collect(Collectors.toSet());
255296
}
256297

src/test/resources/META-INF/persistence.xml

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
<class>org.springframework.data.jpa.domain.sample.EmbeddedIdExampleDepartment</class>
2323
<class>org.springframework.data.jpa.domain.sample.IdClassExampleEmployee</class>
2424
<class>org.springframework.data.jpa.domain.sample.IdClassExampleDepartment</class>
25+
<class>org.springframework.data.jpa.domain.sample.Invoice</class>
26+
<class>org.springframework.data.jpa.domain.sample.InvoiceItem</class>
2527
<class>org.springframework.data.jpa.domain.sample.Item</class>
2628
<class>org.springframework.data.jpa.domain.sample.ItemSite</class>
2729
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>

0 commit comments

Comments
 (0)