Skip to content

Commit 57ff955

Browse files
committed
Refine JPQL rendering for IN expressions using parameter binding.
We now no longer wrap bound parameters with parenthesis as leaving parenthesis away is the spec-compliant approach that also works with EclipseLink and allows to remove the nasty empty-collection to null conversion. See #4112
1 parent 113b7cf commit 57ff955

File tree

6 files changed

+24
-80
lines changed

6 files changed

+24
-80
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,7 +1476,8 @@ public String render(RenderContext context) {
14761476
Expression predicate = this.predicate;
14771477
String rendered = predicate.render(context);
14781478

1479-
return (hasParenthesis(rendered) ? "%s %s %s" : "%s %s (%s)").formatted(path.render(context), operator, rendered);
1479+
return ((hasParenthesis(rendered) || predicate instanceof ParameterExpression) ? "%s %s %s" : "%s %s (%s)")
1480+
.formatted(path.render(context), operator, rendered);
14801481
}
14811482

14821483
@Override

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

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -229,24 +229,16 @@ public static class PartTreeParameterBinding extends ParameterBinding {
229229
private final boolean ignoreCase;
230230
private final boolean noWildcards;
231231
private final @Nullable Object value;
232-
private final PersistenceProvider persistenceProvider;
233232

234233
public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class<?> parameterType,
235234
Part part, @Nullable Object value, JpqlQueryTemplates templates, EscapeCharacter escape) {
236-
this(identifier, origin, parameterType, part, value, templates, escape, PersistenceProvider.GENERIC_JPA);
237-
}
238-
239-
PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class<?> parameterType, Part part,
240-
@Nullable Object value, JpqlQueryTemplates templates, EscapeCharacter escape,
241-
PersistenceProvider persistenceProvider) {
242235

243236
super(identifier, origin);
244237

245238
this.parameterType = parameterType;
246239
this.templates = templates;
247240
this.escape = escape;
248241
this.value = value;
249-
this.persistenceProvider = persistenceProvider;
250242
this.type = value == null
251243
&& (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType()))
252244
? Type.IS_NULL
@@ -293,7 +285,7 @@ public JpqlQueryTemplates getTemplates() {
293285
}
294286

295287
return Collection.class.isAssignableFrom(parameterType) //
296-
? potentiallyIgnoreCase(ignoreCase, toCollection(value, persistenceProvider)) //
288+
? potentiallyIgnoreCase(ignoreCase, toCollection(value)) //
297289
: value;
298290
}
299291

@@ -316,26 +308,23 @@ public JpqlQueryTemplates getTemplates() {
316308
/**
317309
* Returns the given argument as {@link Collection} which means it will return it as is if it's a
318310
* {@link Collection}, turn an array into an {@link ArrayList} or simply wrap any other value into a single-element
319-
* {@link Collection} by default. {@link PersistenceProvider#HIBERNATE} translates empty collections to disjunctions
320-
* to avoid syntax errors.
311+
* {@link Collection} by default.
321312
*
322313
* @param value the value to be converted to a {@link Collection}.
323-
* @param provider the persistence provider to use.
324314
* @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value.
325315
*/
326-
private static @Nullable Collection<?> toCollection(@Nullable Object value, PersistenceProvider provider) {
316+
private static @Nullable Collection<?> toCollection(@Nullable Object value) {
327317

328318
if (value == null) {
329319
return null;
330320
}
331321

332322
if (value instanceof Collection<?> collection) {
333-
return provider == PersistenceProvider.HIBERNATE || !collection.isEmpty() ? collection : null;
323+
return collection;
334324
}
335325

336326
if (ObjectUtils.isArray(value)) {
337-
Object[] array = ObjectUtils.toObjectArray(value);
338-
return provider == PersistenceProvider.HIBERNATE || !ObjectUtils.isEmpty(array) ? Arrays.asList(array) : null;
327+
return Arrays.asList(ObjectUtils.toObjectArray(value));
339328
}
340329

341330
return Collections.singleton(value);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ private <T> PartTreeParameterBinding next(Part part, Class<T> type, Parameter pa
226226
/* identifier refers to bindable parameters, not _all_ parameters index */
227227
MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(origin);
228228
PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier,
229-
methodParameter, reifiedType, part, value, templates, escape, persistenceProvider);
229+
methodParameter, reifiedType, part, value, templates, escape);
230230

231231
// PartTreeParameterBinding is more expressive than a potential ParameterBinding for Vector.
232232
bindings.add(binding);

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ void sortByAssociationPropertyShouldUseLeftOuterJoin() {}
5151
@Override
5252
void sortByAssociationPropertyInPageableShouldUseLeftOuterJoin() {}
5353

54-
/**
55-
* Ignored until https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 is resolved.
56-
*/
57-
@Override
58-
void findByElementCollectionAttribute() {}
59-
6054
/**
6155
* This test will fail once https://bugs.eclipse.org/bugs/show_bug.cgi?id=521915 is fixed.
6256
*/
@@ -142,11 +136,6 @@ void findByElementVarargInAttributeIgnoreCase() {}
142136
@Test // DATAJPA-1303
143137
void findByElementCollectionInAttributeIgnoreCaseWithNulls() {}
144138

145-
@Disabled("Binding collections to IN predicates not supported")
146-
@Override
147-
@Test
148-
void invokesQueryWithVarargsParametersCorrectly() {}
149-
150139
@Disabled("Named parameters in native SQL queries are not supported in EclipseLink")
151140
@Override
152141
@Test
@@ -237,16 +226,6 @@ public void findByFluentPredicateWithProjectionAndPageRequest() {}
237226
@Test
238227
public void findByFluentPredicateWithProjectionAndAll() {}
239228

240-
@Disabled("Binding collections to IN predicates not supported")
241-
@Override
242-
@Test
243-
public void findByCollectionWithPageRequest() {}
244-
245-
@Disabled("Binding collections to IN predicates not supported")
246-
@Override
247-
@Test
248-
public void findByCollectionWithPageable() {}
249-
250229
@Disabled("EclipseLink does not support records")
251230
@Override
252231
@Test

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ void in() {
385385
.forTree(Product.class, "findProductByNameIn") //
386386
.withParameters(List.of("spring", "data")) //
387387
.as(QueryCreatorTester::create) //
388-
.expectJpql("SELECT p FROM %s p WHERE p.name IN (?1)", DefaultJpaEntityMetadata.unqualify(Product.class)) //
388+
.expectJpql("SELECT p FROM %s p WHERE p.name IN ?1", DefaultJpaEntityMetadata.unqualify(Product.class)) //
389389
.expectPlaceholderValue("?1", List.of("spring", "data")) //
390390
.validateQuery();
391391
}
@@ -397,7 +397,7 @@ void notIn() {
397397
.forTree(Product.class, "findProductByNameNotIn") //
398398
.withParameters(List.of("spring", "data")) //
399399
.as(QueryCreatorTester::create) //
400-
.expectJpql("SELECT p FROM %s p WHERE p.name NOT IN (?1)", DefaultJpaEntityMetadata.unqualify(Product.class)) //
400+
.expectJpql("SELECT p FROM %s p WHERE p.name NOT IN ?1", DefaultJpaEntityMetadata.unqualify(Product.class)) //
401401
.expectPlaceholderValue("?1", List.of("spring", "data")) //
402402
.validateQuery();
403403
}

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

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,9 @@
2020
import java.util.Collection;
2121
import java.util.List;
2222
import java.util.Set;
23-
import java.util.stream.Stream;
2423

2524
import org.junit.jupiter.api.Test;
26-
import org.junit.jupiter.params.ParameterizedTest;
27-
import org.junit.jupiter.params.provider.Arguments;
28-
import org.junit.jupiter.params.provider.MethodSource;
2925

30-
import org.springframework.data.jpa.provider.PersistenceProvider;
3126
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
3227
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
3328
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
@@ -42,54 +37,34 @@
4237
*/
4338
class ParameterBindingUnitTests {
4439

45-
static ParameterBinding hibernateCollectionBinding = new ParameterBinding.PartTreeParameterBinding(
40+
static ParameterBinding collectionBinding = new ParameterBinding.PartTreeParameterBinding(
4641
BindingIdentifier.of("foo"), ParameterOrigin.ofParameter("foo"), Collection.class,
47-
new Part("name", TestEntity.class), null, JpqlQueryTemplates.UPPER, EscapeCharacter.DEFAULT,
48-
PersistenceProvider.HIBERNATE);
49-
50-
static ParameterBinding eclipselinkCollectionBinding = new ParameterBinding.PartTreeParameterBinding(
51-
BindingIdentifier.of("foo"), ParameterOrigin.ofParameter("foo"), Collection.class,
52-
new Part("name", TestEntity.class), null, JpqlQueryTemplates.UPPER, EscapeCharacter.DEFAULT,
53-
PersistenceProvider.ECLIPSELINK);
42+
new Part("name", TestEntity.class), null, JpqlQueryTemplates.UPPER, EscapeCharacter.DEFAULT);
5443

5544
@Test // GH-4112
56-
void shouldPrepareEmptyListForHibernate() {
45+
void shouldPrepareEmptyList() {
5746

58-
assertThat(hibernateCollectionBinding.prepare(List.of())).isEqualTo(List.of());
59-
assertThat(hibernateCollectionBinding.prepare(new Integer[0])).isEqualTo(List.of());
47+
assertThat(collectionBinding.prepare(List.of())).isEqualTo(List.of());
48+
assertThat(collectionBinding.prepare(new Integer[0])).isEqualTo(List.of());
6049
}
6150

6251
@Test // GH-4112
63-
void shouldPrepareEmptyListToNullForEclipselink() {
64-
65-
assertThat(eclipselinkCollectionBinding.prepare(List.of())).isNull();
66-
assertThat(eclipselinkCollectionBinding.prepare(new Integer[0])).isNull();
67-
}
52+
void shouldPrepareListToCollection() {
6853

69-
@ParameterizedTest // GH-4112
70-
@MethodSource("bindings")
71-
void shouldPrepareListToCollection(ParameterBinding binding) {
72-
73-
assertThat(binding.prepare(List.of("foo"))).isEqualTo(List.of("foo"));
74-
assertThat(binding.prepare(new Integer[] { 1, 2, 3 })).isEqualTo(List.of(1, 2, 3));
54+
assertThat(collectionBinding.prepare(List.of("foo"))).isEqualTo(List.of("foo"));
55+
assertThat(collectionBinding.prepare(new Integer[] { 1, 2, 3 })).isEqualTo(List.of(1, 2, 3));
7556
}
7657

77-
@ParameterizedTest // GH-4112
78-
@MethodSource("bindings")
79-
void shouldPrepareValueToSingleElementCollection(ParameterBinding binding) {
80-
assertThat(binding.prepare("foo")).isEqualTo(Set.of("foo"));
58+
@Test // GH-4112
59+
void shouldPrepareValueToSingleElementCollection() {
60+
assertThat(collectionBinding.prepare("foo")).isEqualTo(Set.of("foo"));
8161
}
8262

83-
@ParameterizedTest // GH-4112
84-
@MethodSource("bindings")
85-
void shouldPrepareNull(ParameterBinding binding) {
86-
assertThat(binding.prepare(null)).isNull();
63+
@Test // GH-4112
64+
void shouldPrepareNull() {
65+
assertThat(collectionBinding.prepare(null)).isNull();
8766
}
8867

89-
static Stream<Arguments.ArgumentSet> bindings() {
90-
return Stream.of(Arguments.argumentSet("Hibernate", hibernateCollectionBinding),
91-
Arguments.argumentSet("EclipseLink", eclipselinkCollectionBinding));
92-
}
9368

9469
private record TestEntity(String name) {
9570

0 commit comments

Comments
 (0)