Skip to content

Commit 5dbf808

Browse files
committed
Do not translate empty collection to null for derived Hibernate query bindings.
Hibernate translates empty collections to disjunctions so we don't have to provide values that would lead to a query yielding no results. Closes #4112
1 parent 32a4286 commit 5dbf808

File tree

4 files changed

+149
-15
lines changed

4 files changed

+149
-15
lines changed

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,24 @@ 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;
232233

233234
public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class<?> parameterType,
234235
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) {
235242

236243
super(identifier, origin);
237244

238245
this.parameterType = parameterType;
239246
this.templates = templates;
240247
this.escape = escape;
241248
this.value = value;
249+
this.persistenceProvider = persistenceProvider;
242250
this.type = value == null
243251
&& (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType()))
244252
? Type.IS_NULL
@@ -285,7 +293,7 @@ public JpqlQueryTemplates getTemplates() {
285293
}
286294

287295
return Collection.class.isAssignableFrom(parameterType) //
288-
? potentiallyIgnoreCase(ignoreCase, toCollection(value)) //
296+
? potentiallyIgnoreCase(ignoreCase, toCollection(value, persistenceProvider)) //
289297
: value;
290298
}
291299

@@ -307,26 +315,27 @@ public JpqlQueryTemplates getTemplates() {
307315

308316
/**
309317
* Returns the given argument as {@link Collection} which means it will return it as is if it's a
310-
* {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element
311-
* {@link Collections}.
318+
* {@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.
312321
*
313322
* @param value the value to be converted to a {@link Collection}.
323+
* @param provider the persistence provider to use.
314324
* @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value.
315325
*/
316-
private static @Nullable Collection<?> toCollection(@Nullable Object value) {
326+
private static @Nullable Collection<?> toCollection(@Nullable Object value, PersistenceProvider provider) {
317327

318328
if (value == null) {
319329
return null;
320330
}
321331

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

326336
if (ObjectUtils.isArray(value)) {
327-
328-
List<Object> collection = Arrays.asList(ObjectUtils.toObjectArray(value));
329-
return collection.isEmpty() ? null : collection;
337+
Object[] array = ObjectUtils.toObjectArray(value);
338+
return provider == PersistenceProvider.HIBERNATE || !ObjectUtils.isEmpty(array) ? Arrays.asList(array) : null;
330339
}
331340

332341
return Collections.singleton(value);

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class ParameterMetadataProvider {
6767
private final EscapeCharacter escape;
6868
private final JpqlQueryTemplates templates;
6969
private final JpaParameters jpaParameters;
70+
private final PersistenceProvider persistenceProvider;
7071
private int position;
7172
private int bindMarker;
7273

@@ -80,19 +81,37 @@ public class ParameterMetadataProvider {
8081
*/
8182
public ParameterMetadataProvider(JpaParametersParameterAccessor accessor, EscapeCharacter escape,
8283
JpqlQueryTemplates templates) {
83-
this(accessor.iterator(), accessor, accessor.getParameters(), escape, templates);
84+
this(accessor.iterator(), accessor, accessor.getParameters(), escape, templates, PersistenceProvider.GENERIC_JPA);
8485
}
8586

8687
/**
87-
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and {@link Parameters} with
88+
* Creates a new {@code ParameterMetadataProvider} from the given {@link CriteriaBuilder},
89+
* {@link ParametersParameterAccessor}, and {@link PersistenceProvider}.
90+
*
91+
* @param accessor must not be {@literal null}.
92+
* @param escape must not be {@literal null}.
93+
* @param templates must not be {@literal null}.
94+
* @param persistenceProvider must not be {@literal null}.
95+
* @since 4.0.1
96+
*/
97+
ParameterMetadataProvider(JpaParametersParameterAccessor accessor, EscapeCharacter escape,
98+
JpqlQueryTemplates templates, PersistenceProvider persistenceProvider) {
99+
this(accessor.iterator(), accessor, accessor.getParameters(), escape, templates, persistenceProvider);
100+
}
101+
102+
/**
103+
* Creates a new {@code ParameterMetadataProvider} from the given {@link CriteriaBuilder} and {@link Parameters} with
88104
* support for parameter value customizations via {@link PersistenceProvider}.
105+
* <p>
106+
* Uses {@link PersistenceProvider#HIBERNATE} as provider since this constructor is used in contexts where no special
107+
* handling is required.
89108
*
90109
* @param parameters must not be {@literal null}.
91110
* @param escape must not be {@literal null}.
92111
* @param templates must not be {@literal null}.
93112
*/
94113
public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escape, JpqlQueryTemplates templates) {
95-
this(null, null, parameters, escape, templates);
114+
this(null, null, parameters, escape, templates, PersistenceProvider.HIBERNATE);
96115
}
97116

98117
/**
@@ -103,10 +122,12 @@ public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escap
103122
* @param parameters must not be {@literal null}.
104123
* @param escape must not be {@literal null}.
105124
* @param templates must not be {@literal null}.
125+
* @param persistenceProvider must not be {@literal null}.
106126
*/
107127
private ParameterMetadataProvider(@Nullable Iterator<Object> bindableParameterValues,
108128
@Nullable JpaParametersParameterAccessor accessor, JpaParameters parameters, EscapeCharacter escape,
109-
JpqlQueryTemplates templates) {
129+
JpqlQueryTemplates templates, PersistenceProvider persistenceProvider) {
130+
110131
Assert.notNull(parameters, "Parameters must not be null");
111132
Assert.notNull(escape, "EscapeCharacter must not be null");
112133
Assert.notNull(templates, "JpqlQueryTemplates must not be null");
@@ -118,6 +139,7 @@ private ParameterMetadataProvider(@Nullable Iterator<Object> bindableParameterVa
118139
this.bindableParameterValues = bindableParameterValues;
119140
this.escape = escape;
120141
this.templates = templates;
142+
this.persistenceProvider = persistenceProvider;
121143
}
122144

123145
JpaParameters getParameters() {
@@ -204,7 +226,7 @@ private <T> PartTreeParameterBinding next(Part part, Class<T> type, Parameter pa
204226
/* identifier refers to bindable parameters, not _all_ parameters index */
205227
MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(origin);
206228
PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier,
207-
methodParameter, reifiedType, part, value, templates, escape);
229+
methodParameter, reifiedType, part, value, templates, escape, persistenceProvider);
208230

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

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.data.domain.OffsetScrollPosition;
3232
import org.springframework.data.domain.ScrollPosition;
3333
import org.springframework.data.domain.Sort;
34+
import org.springframework.data.jpa.provider.PersistenceProvider;
3435
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
3536
import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution;
3637
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution;
@@ -71,6 +72,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
7172
private final EntityManager em;
7273
private final EscapeCharacter escape;
7374
private final Lazy<JpaEntityInformation<?, ?>> entityInformation;
75+
private final PersistenceProvider persistenceProvider;
7476

7577
/**
7678
* Creates a new {@link PartTreeJpaQuery}.
@@ -96,6 +98,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
9698
this.em = em;
9799
this.escape = escape;
98100
this.parameters = method.getParameters();
101+
this.persistenceProvider = PersistenceProvider.fromEntityManager(em);
99102

100103
Class<?> domainClass = method.getEntityInformation().getJavaType();
101104
this.entityInformation = Lazy.of(() -> JpaEntityInformationSupport.getEntityInformation(domainClass, em));
@@ -293,7 +296,8 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess
293296
EntityManager entityManager = getEntityManager();
294297
ResultProcessor processor = getQueryMethod().getResultProcessor();
295298

296-
ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates);
299+
ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates,
300+
persistenceProvider);
297301
ReturnedType returnedType = processor.withDynamicProjection(accessor).getReturnedType();
298302

299303
if (accessor.getScrollPosition() instanceof KeysetScrollPosition keyset) {
@@ -390,7 +394,8 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess
390394
return cached;
391395
}
392396

393-
ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates);
397+
ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates,
398+
persistenceProvider);
394399
JpaCountQueryCreator creator = new JpaCountQueryCreator(tree,
395400
getQueryMethod().getResultProcessor().getReturnedType(), provider, templates, entityInformation.get(),
396401
em.getMetamodel());
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2017-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import java.util.Collection;
21+
import java.util.List;
22+
import java.util.Set;
23+
import java.util.stream.Stream;
24+
25+
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;
29+
30+
import org.springframework.data.jpa.provider.PersistenceProvider;
31+
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
32+
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
33+
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
34+
import org.springframework.data.repository.query.parser.Part;
35+
36+
/**
37+
* Unit tests for {@link ParameterBinding}.
38+
*
39+
* @author Jens Schauder
40+
* @author Diego Krupitza
41+
* @author Mark Paluch
42+
*/
43+
class ParameterBindingUnitTests {
44+
45+
static ParameterBinding hibernateCollectionBinding = new ParameterBinding.PartTreeParameterBinding(
46+
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);
54+
55+
@Test // GH-4112
56+
void shouldPrepareEmptyListForHibernate() {
57+
58+
assertThat(hibernateCollectionBinding.prepare(List.of())).isEqualTo(List.of());
59+
assertThat(hibernateCollectionBinding.prepare(new Integer[0])).isEqualTo(List.of());
60+
}
61+
62+
@Test // GH-4112
63+
void shouldPrepareEmptyListToNullForEclipselink() {
64+
65+
assertThat(eclipselinkCollectionBinding.prepare(List.of())).isNull();
66+
assertThat(eclipselinkCollectionBinding.prepare(new Integer[0])).isNull();
67+
}
68+
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));
75+
}
76+
77+
@ParameterizedTest // GH-4112
78+
@MethodSource("bindings")
79+
void shouldPrepareValueToSingleElementCollection(ParameterBinding binding) {
80+
assertThat(binding.prepare("foo")).isEqualTo(Set.of("foo"));
81+
}
82+
83+
@ParameterizedTest // GH-4112
84+
@MethodSource("bindings")
85+
void shouldPrepareNull(ParameterBinding binding) {
86+
assertThat(binding.prepare(null)).isNull();
87+
}
88+
89+
static Stream<Arguments.ArgumentSet> bindings() {
90+
return Stream.of(Arguments.argumentSet("Hibernate", hibernateCollectionBinding),
91+
Arguments.argumentSet("EclipseLink", eclipselinkCollectionBinding));
92+
}
93+
94+
private record TestEntity(String name) {
95+
96+
}
97+
98+
}

0 commit comments

Comments
 (0)