From 64762d0f8bbbd4477ec29bd817dd2109c9f78436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9da=20Housni=20Alaoui?= Date: Tue, 2 Oct 2018 17:39:19 +0200 Subject: [PATCH] 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: https://github.com/eclipse-ee4j/jpa-api/issues/189. Original pull request: #294. --- .../jpa/repository/query/JpaQueryCreator.java | 3 +- .../data/jpa/repository/query/QueryUtils.java | 16 +++- .../ProjectionJoinIntegrationTests.java | 95 +++++++++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/springframework/data/jpa/repository/projections/ProjectionJoinIntegrationTests.java 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; + } +}