Skip to content

Commit 89797a4

Browse files
committed
Consider full origin path for joins in JpqlQueryBuilder.
We now consider the full join path in JpqlQueryBuilder to fix the cache key used to capture joins. Previously, we only considered the property name (a single property path segment) causing omission of joins using the same property name. Closes #3989
1 parent 697dbe3 commit 89797a4

File tree

2 files changed

+87
-2
lines changed

2 files changed

+87
-2
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
*
4545
* @author Mark Paluch
4646
* @author Choi Wang Gyu
47+
* @since 4.0
4748
*/
4849
@SuppressWarnings("JavadocDeclaration")
4950
public final class JpqlQueryBuilder {
@@ -804,7 +805,7 @@ public Select join(Join join) {
804805
join(parent);
805806
}
806807

807-
this.joins.put(join.joinType() + "_" + join.getName() + "_" + join.path(), join);
808+
this.joins.put(join.joinType() + "_" + join.getPath(), join);
808809
return this;
809810
}
810811

@@ -1001,6 +1002,14 @@ public interface Origin {
10011002
*/
10021003
String getName();
10031004

1005+
/**
1006+
* Returns the path in dot-path notation to identify the origin uniquely. Entities typically return their entity
1007+
* name while joins return a dot-path.
1008+
*
1009+
* @return the dot-path of this origin.
1010+
*/
1011+
String getPath();
1012+
10041013
}
10051014

10061015
/**
@@ -1028,6 +1037,11 @@ public String getName() {
10281037
return entity;
10291038
}
10301039

1040+
@Override
1041+
public String getPath() {
1042+
return getName();
1043+
}
1044+
10311045
public String getAlias() {
10321046
return alias;
10331047
}
@@ -1083,6 +1097,11 @@ public String getName() {
10831097
return path;
10841098
}
10851099

1100+
@Override
1101+
public String getPath() {
1102+
return source.getPath() + "." + getName();
1103+
}
1104+
10861105
@Override
10871106
public String render(RenderContext context) {
10881107
return "%s %s %s".formatted(joinType, context.getAlias(source), path);

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,27 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.springframework.data.jpa.repository.query.JpqlQueryBuilder.*;
2020

21+
import jakarta.persistence.Embeddable;
22+
import jakarta.persistence.EmbeddedId;
2123
import jakarta.persistence.Id;
2224
import jakarta.persistence.ManyToOne;
2325
import jakarta.persistence.OneToMany;
26+
import jakarta.persistence.metamodel.EntityType;
2427

2528
import java.util.Date;
29+
import java.util.HashSet;
2630
import java.util.LinkedHashMap;
2731
import java.util.List;
2832
import java.util.Map;
33+
import java.util.Set;
2934

3035
import org.assertj.core.api.AbstractStringAssert;
3136
import org.assertj.core.api.Assertions;
3237
import org.junit.jupiter.api.Test;
3338

39+
import org.springframework.data.core.PropertyPath;
40+
import org.springframework.data.jpa.util.TestMetaModel;
41+
3442
/**
3543
* Unit tests for {@link JpqlQueryBuilder}.
3644
*
@@ -200,7 +208,7 @@ void joins() {
200208
String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30"))
201209
.and(JpqlQueryBuilder.where(personName).eq(literal("ex40"))).render(ctx(entity));
202210

203-
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'");
211+
assertThat(fragment).isEqualTo("p.name = 'ex30' AND p_0.name = 'ex40'");
204212
}
205213

206214
@Test // GH-3588
@@ -219,6 +227,27 @@ void joinOnPaths() {
219227
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
220228
}
221229

230+
@Test // GH-3989
231+
void shouldRenderJoinsWithSamePathSegmentCorrectly() {
232+
233+
TestMetaModel model = TestMetaModel.hibernateModel(WithCompositeId.class, CompositeId.class, Groups.class,
234+
Group.class, Person.class);
235+
Entity entity = entity(WithCompositeId.class);
236+
237+
EntityType<WithCompositeId> entityType = model.entity(WithCompositeId.class);
238+
JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(model, entity, entityType,
239+
PropertyPath.from("id.groups.groups.id.person.id", WithCompositeId.class));
240+
String jpql = JpqlQueryBuilder.selectFrom(entity).entity().where(JpqlQueryBuilder.where(pas).isNotNull()).render();
241+
242+
assertThat(jpql).isEqualTo("SELECT w FROM JpqlQueryBuilderUnitTests$WithCompositeId w " //
243+
+ "INNER JOIN w.id i_0 " //
244+
+ "LEFT JOIN i_0.groups g " //
245+
+ "LEFT JOIN g.groups g_0 " //
246+
+ "INNER JOIN g_0.id i " //
247+
+ "WHERE i.person.id IS NOT NULL");
248+
249+
}
250+
222251
static ContextualAssert contextual(RenderContext context) {
223252
return new ContextualAssert(context);
224253
}
@@ -294,4 +323,41 @@ static class Product {
294323
String productType;
295324

296325
}
326+
327+
@jakarta.persistence.Entity
328+
static class WithCompositeId {
329+
330+
@EmbeddedId CompositeId id;
331+
332+
}
333+
334+
@Embeddable
335+
static class CompositeId {
336+
337+
@ManyToOne Groups groups;
338+
339+
}
340+
341+
@jakarta.persistence.Entity
342+
static class Groups {
343+
344+
@Id long id;
345+
@OneToMany Set<Group> groups = new HashSet<>();
346+
347+
}
348+
349+
@jakarta.persistence.Entity
350+
static class Group {
351+
352+
@EmbeddedId GroupId id;
353+
354+
}
355+
356+
@Embeddable
357+
static class GroupId {
358+
359+
@ManyToOne Person person;
360+
361+
}
362+
297363
}

0 commit comments

Comments
 (0)