Skip to content

Commit d34899a

Browse files
committed
Reuse existing FETCH JOINs when creating Expressions from PropertyPath.
We now consider existing fetch joins for attributes when CriteriaQuery has used a fetch join already. This helps Hibernate to e.g. include mandatory fields in the select list when using DISTINCT using fetch joins. Closes #2756
1 parent 7f170ea commit d34899a

File tree

3 files changed

+82
-1
lines changed

3 files changed

+82
-1
lines changed

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,7 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
889889
}
890890

891891
/**
892-
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
892+
* Returns an existing (fetch) join for the given attribute if one already exists or creates a new one if not.
893893
*
894894
* @param from the {@link From} to get the current joins from.
895895
* @param attribute the {@link Attribute} to look for in the current joins.
@@ -898,6 +898,13 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
898898
*/
899899
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute, JoinType joinType) {
900900

901+
for (Fetch<?, ?> fetch : from.getFetches()) {
902+
903+
if (fetch instanceof Join<?, ?> join && join.getAttribute().getName().equals(attribute)) {
904+
return join;
905+
}
906+
}
907+
901908
for (Join<?, ?> join : from.getJoins()) {
902909

903910
if (join.getAttribute().getName().equals(attribute)) {

Diff for: spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java

+35
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,25 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import jakarta.persistence.criteria.CriteriaBuilder;
21+
import jakarta.persistence.criteria.CriteriaQuery;
22+
import jakarta.persistence.criteria.Path;
23+
import jakarta.persistence.criteria.Root;
24+
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.data.jpa.domain.sample.User;
28+
import org.springframework.data.mapping.PropertyPath;
1829
import org.springframework.test.context.ContextConfiguration;
1930

2031
/**
32+
* EclipseLink variant of {@link QueryUtilsIntegrationTests}.
33+
*
2134
* @author Oliver Gierke
2235
* @author Jens Schauder
36+
* @author Mark Paluch
2337
*/
2438
@ContextConfiguration("classpath:eclipselink.xml")
2539
class EclipseLinkQueryUtilsIntegrationTests extends QueryUtilsIntegrationTests {
@@ -28,4 +42,25 @@ int getNumberOfJoinsAfterCreatingAPath() {
2842
return 1;
2943
}
3044

45+
@Test // GH-2756
46+
@Override
47+
void prefersFetchOverJoin() {
48+
49+
CriteriaBuilder builder = em.getCriteriaBuilder();
50+
CriteriaQuery<User> query = builder.createQuery(User.class);
51+
Root<User> from = query.from(User.class);
52+
from.fetch("manager");
53+
from.join("manager");
54+
55+
PropertyPath managerFirstname = PropertyPath.from("manager.firstname", User.class);
56+
PropertyPath managerLastname = PropertyPath.from("manager.lastname", User.class);
57+
58+
QueryUtils.toExpressionRecursively(from, managerLastname);
59+
Path<Object> expr = (Path) QueryUtils.toExpressionRecursively(from, managerFirstname);
60+
61+
assertThat(expr.getParentPath()).hasFieldOrPropertyWithValue("isFetch", true);
62+
assertThat(from.getFetches()).hasSize(1);
63+
assertThat(from.getJoins()).hasSize(1);
64+
}
65+
3166
}

Diff for: spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java

+39
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import jakarta.persistence.criteria.From;
3333
import jakarta.persistence.criteria.Join;
3434
import jakarta.persistence.criteria.JoinType;
35+
import jakarta.persistence.criteria.Path;
3536
import jakarta.persistence.criteria.Root;
3637
import jakarta.persistence.spi.PersistenceProvider;
3738
import jakarta.persistence.spi.PersistenceProviderResolver;
@@ -91,6 +92,44 @@ void reusesExistingJoinForExpression() {
9192
assertThat(from.getJoins()).hasSize(1);
9293
}
9394

95+
@Test // GH-2756
96+
void reusesExistingFetchJoinForExpression() {
97+
98+
CriteriaBuilder builder = em.getCriteriaBuilder();
99+
CriteriaQuery<User> query = builder.createQuery(User.class);
100+
Root<User> from = query.from(User.class);
101+
from.fetch("manager");
102+
103+
PropertyPath managerFirstname = PropertyPath.from("manager.firstname", User.class);
104+
PropertyPath managerLastname = PropertyPath.from("manager.lastname", User.class);
105+
106+
QueryUtils.toExpressionRecursively(from, managerLastname);
107+
QueryUtils.toExpressionRecursively(from, managerFirstname);
108+
109+
assertThat(from.getFetches()).hasSize(1);
110+
assertThat(from.getJoins()).isEmpty();
111+
}
112+
113+
@Test // GH-2756
114+
void prefersFetchOverJoin() {
115+
116+
CriteriaBuilder builder = em.getCriteriaBuilder();
117+
CriteriaQuery<User> query = builder.createQuery(User.class);
118+
Root<User> from = query.from(User.class);
119+
from.fetch("manager");
120+
from.join("manager");
121+
122+
PropertyPath managerFirstname = PropertyPath.from("manager.firstname", User.class);
123+
PropertyPath managerLastname = PropertyPath.from("manager.lastname", User.class);
124+
125+
QueryUtils.toExpressionRecursively(from, managerLastname);
126+
Path<Object> expr = (Path<Object>) QueryUtils.toExpressionRecursively(from, managerFirstname);
127+
128+
assertThat(expr.getParentPath()).hasFieldOrPropertyWithValue("fetched", true);
129+
assertThat(from.getFetches()).hasSize(1);
130+
assertThat(from.getJoins()).hasSize(1);
131+
}
132+
94133
@Test // DATAJPA-401, DATAJPA-1238
95134
void createsJoinForNavigationAcrossOptionalAssociation() {
96135

0 commit comments

Comments
 (0)