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.
This commit is contained in:
Réda Housni Alaoui
2018-10-02 17:39:19 +02:00
committed by Jens Schauder
parent 033c46fc19
commit 64762d0f8b
3 changed files with 109 additions and 5 deletions

View File

@@ -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<CriteriaQuery<? extends Object>, Predicate> {
@@ -168,7 +169,7 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
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);

View File

@@ -75,6 +75,7 @@ import org.springframework.util.StringUtils;
* @author Sébastien Péralta
* @author Jens Schauder
* @author Nils Borrmann
* @author Reda.Housni-Alaoui
*/
public abstract class QueryUtils {
@@ -573,6 +574,11 @@ public abstract class QueryUtils {
@SuppressWarnings("unchecked")
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
return toExpressionRecursively(from, property, false);
}
@SuppressWarnings("unchecked")
static <T> Expression<T> 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<T>) (property.hasNext() ? toExpressionRecursively(join, property.next()) : join);
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection)
: join);
} else {
Path<Object> path = from.get(segment);
return (Expression<T>) (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;
}

View File

@@ -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<User, Integer> {
<T> T findById(int id, Class<T> 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;
}
}