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 e02dfe7c9..2fd1193f4 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 @@ import org.springframework.util.Assert; * @author Mark Paluch * @author Michael Cramer * @author Mark Paluch + * @author Reda.Housni-Alaoui */ public class JpaQueryCreator extends AbstractQueryCreator, Predicate> { @@ -168,7 +169,7 @@ public class JpaQueryCreator extends AbstractQueryCreator 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 @@ public abstract class QueryUtils { 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 @@ public abstract class QueryUtils { * @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 @@ public abstract class QueryUtils { 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 000000000..2146cbd5a --- /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; + } +}