diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index e02dfe7c90..2fd1193f46 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -52,6 +52,7 @@ * @author Mark Paluch * @author Michael Cramer * @author Mark Paluch + * @author Reda.Housni-Alaoui */ public class JpaQueryCreator extends AbstractQueryCreator, Predicate> { @@ -168,7 +169,7 @@ protected CriteriaQuery complete(@Nullable Predicate predicate for (String property : returnedType.getInputProperties()) { PropertyPath path = PropertyPath.from(property, returnedType.getDomainType()); - selections.add(toExpressionRecursively(root, path).alias(property)); + selections.add(toExpressionRecursively(root, path, true).alias(property)); } query = query.multiselect(selections); diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index c447e67e5c..503810fbad 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -75,6 +75,7 @@ * @author Sébastien Péralta * @author Jens Schauder * @author Nils Borrmann + * @author Reda.Housni-Alaoui */ public abstract class QueryUtils { @@ -573,6 +574,11 @@ private static javax.persistence.criteria.Order toJpaOrder(Order order, From Expression toExpressionRecursively(From from, PropertyPath property) { + return toExpressionRecursively(from, property, false); + } + + @SuppressWarnings("unchecked") + static Expression toExpressionRecursively(From from, PropertyPath property, boolean isForSelection) { Bindable propertyPathModel; Bindable model = from.getModel(); @@ -589,10 +595,11 @@ static Expression toExpressionRecursively(From from, PropertyPath p propertyPathModel = from.get(segment).getModel(); } - if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext()) + if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(), isForSelection) && !isAlreadyFetched(from, segment)) { Join join = getOrCreateJoin(from, segment); - return (Expression) (property.hasNext() ? toExpressionRecursively(join, property.next()) : join); + return (Expression) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection) + : join); } else { Path path = from.get(segment); return (Expression) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path); @@ -606,10 +613,11 @@ static Expression toExpressionRecursively(From from, PropertyPath p * @param propertyPathModel may be {@literal null}. * @param isPluralAttribute is the attribute of Collection type? * @param isLeafProperty is this the final property navigated by a {@link PropertyPath}? + * @param isForSelection is the property navigated for the selection part of the query? * @return wether an outer join is to be used for integrating this attribute in a query. */ private static boolean requiresJoin(@Nullable Bindable propertyPathModel, boolean isPluralAttribute, - boolean isLeafProperty) { + boolean isLeafProperty, boolean isForSelection) { if (propertyPathModel == null && isPluralAttribute) { return true; @@ -625,7 +633,7 @@ private static boolean requiresJoin(@Nullable Bindable propertyPathModel, boo return false; } - if (isLeafProperty && !attribute.isCollection()) { + if (isLeafProperty && !isForSelection && !attribute.isCollection()) { return false; } diff --git a/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionJoinIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionJoinIntegrationTests.java new file mode 100644 index 0000000000..2146cbd5af --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionJoinIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.projections; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.Table; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Reda.Housni-Alaoui + */ +@Transactional +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = ProjectionsIntegrationTests.Config.class) +public class ProjectionJoinIntegrationTests { + + @Autowired private UserRepository userRepository; + + @Test + public void findByIdPerformsAnOuterJoin() { + User user = userRepository.save(new User()); + + UserProjection projection = userRepository.findById(user.getId(), UserProjection.class); + + assertThat(projection).isNotNull(); + assertThat(projection.getId()).isEqualTo(user.getId()); + assertThat(projection.getAddress()).isNull(); + } + + @Data + private static class UserProjection { + + private final int id; + private final Address address; + + public UserProjection(int id, Address address) { + this.id = id; + this.address = address; + } + } + + public interface UserRepository extends CrudRepository { + + T findById(int id, Class projectionClass); + } + + @Data + @Table(name = "ProjectionJoinIntegrationTests_User") + @Entity + static class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Access(value = AccessType.PROPERTY) int id; + + @OneToOne(cascade = CascadeType.ALL) Address address; + } + + @Data + @Table(name = "ProjectionJoinIntegrationTests_Address") + @Entity + static class Address { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Access(value = AccessType.PROPERTY) int id; + + String streetName; + } +}