Skip to content

Commit 9df9238

Browse files
reda-alaouischauder
authored andcommitted
DATAJPA-1418 - Interface-based Projections - Generate inner join instead of left join.
When a projection contains a reference to an entity we generate an outer join again. This is necessary since Hibernate insist on creating an inner join instead, which will filter out null values of the reference. See also: https://hibernate.atlassian.net/browse/HHH-12999. See also: jakartaee/persistence#189. Original pull request: #294.
1 parent 7f51d65 commit 9df9238

File tree

3 files changed

+109
-5
lines changed

3 files changed

+109
-5
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
* @author Mark Paluch
5353
* @author Michael Cramer
5454
* @author Mark Paluch
55+
* @author Reda.Housni-Alaoui
5556
*/
5657
public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extends Object>, Predicate> {
5758

@@ -168,7 +169,7 @@ protected CriteriaQuery<? extends Object> complete(@Nullable Predicate predicate
168169
for (String property : returnedType.getInputProperties()) {
169170

170171
PropertyPath path = PropertyPath.from(property, returnedType.getDomainType());
171-
selections.add(toExpressionRecursively(root, path).alias(property));
172+
selections.add(toExpressionRecursively(root, path, true).alias(property));
172173
}
173174

174175
query = query.multiselect(selections);

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
* @author Sébastien Péralta
7676
* @author Jens Schauder
7777
* @author Nils Borrmann
78+
* @author Reda.Housni-Alaoui
7879
*/
7980
public abstract class QueryUtils {
8081

@@ -573,6 +574,11 @@ private static javax.persistence.criteria.Order toJpaOrder(Order order, From<?,
573574

574575
@SuppressWarnings("unchecked")
575576
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
577+
return toExpressionRecursively(from, property, false);
578+
}
579+
580+
@SuppressWarnings("unchecked")
581+
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
576582

577583
Bindable<?> propertyPathModel;
578584
Bindable<?> model = from.getModel();
@@ -589,10 +595,11 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
589595
propertyPathModel = from.get(segment).getModel();
590596
}
591597

592-
if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext())
598+
if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(), isForSelection)
593599
&& !isAlreadyFetched(from, segment)) {
594600
Join<?, ?> join = getOrCreateJoin(from, segment);
595-
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next()) : join);
601+
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection)
602+
: join);
596603
} else {
597604
Path<Object> path = from.get(segment);
598605
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path);
@@ -606,10 +613,11 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
606613
* @param propertyPathModel may be {@literal null}.
607614
* @param isPluralAttribute is the attribute of Collection type?
608615
* @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
616+
* @param isForSelection is the property navigated for the selection part of the query?
609617
* @return wether an outer join is to be used for integrating this attribute in a query.
610618
*/
611619
private static boolean requiresJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
612-
boolean isLeafProperty) {
620+
boolean isLeafProperty, boolean isForSelection) {
613621

614622
if (propertyPathModel == null && isPluralAttribute) {
615623
return true;
@@ -625,7 +633,7 @@ private static boolean requiresJoin(@Nullable Bindable<?> propertyPathModel, boo
625633
return false;
626634
}
627635

628-
if (isLeafProperty && !attribute.isCollection()) {
636+
if (isLeafProperty && !isForSelection && !attribute.isCollection()) {
629637
return false;
630638
}
631639

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.repository.projections;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import lombok.Data;
21+
22+
import javax.persistence.Access;
23+
import javax.persistence.AccessType;
24+
import javax.persistence.CascadeType;
25+
import javax.persistence.Entity;
26+
import javax.persistence.GeneratedValue;
27+
import javax.persistence.GenerationType;
28+
import javax.persistence.Id;
29+
import javax.persistence.OneToOne;
30+
import javax.persistence.Table;
31+
32+
import org.junit.Test;
33+
import org.junit.runner.RunWith;
34+
import org.springframework.beans.factory.annotation.Autowired;
35+
import org.springframework.data.repository.CrudRepository;
36+
import org.springframework.test.context.ContextConfiguration;
37+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
38+
import org.springframework.transaction.annotation.Transactional;
39+
40+
/**
41+
* @author Reda.Housni-Alaoui
42+
*/
43+
@Transactional
44+
@RunWith(SpringJUnit4ClassRunner.class)
45+
@ContextConfiguration(classes = ProjectionsIntegrationTests.Config.class)
46+
public class ProjectionJoinIntegrationTests {
47+
48+
@Autowired private UserRepository userRepository;
49+
50+
@Test
51+
public void findByIdPerformsAnOuterJoin() {
52+
User user = userRepository.save(new User());
53+
54+
UserProjection projection = userRepository.findById(user.getId(), UserProjection.class);
55+
56+
assertThat(projection).isNotNull();
57+
assertThat(projection.getId()).isEqualTo(user.getId());
58+
assertThat(projection.getAddress()).isNull();
59+
}
60+
61+
@Data
62+
private static class UserProjection {
63+
64+
private final int id;
65+
private final Address address;
66+
67+
public UserProjection(int id, Address address) {
68+
this.id = id;
69+
this.address = address;
70+
}
71+
}
72+
73+
public interface UserRepository extends CrudRepository<User, Integer> {
74+
75+
<T> T findById(int id, Class<T> projectionClass);
76+
}
77+
78+
@Data
79+
@Table(name = "ProjectionJoinIntegrationTests_User")
80+
@Entity
81+
static class User {
82+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Access(value = AccessType.PROPERTY) int id;
83+
84+
@OneToOne(cascade = CascadeType.ALL) Address address;
85+
}
86+
87+
@Data
88+
@Table(name = "ProjectionJoinIntegrationTests_Address")
89+
@Entity
90+
static class Address {
91+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Access(value = AccessType.PROPERTY) int id;
92+
93+
String streetName;
94+
}
95+
}

0 commit comments

Comments
 (0)