Skip to content

Commit 23b97b4

Browse files
committed
Change how a Like comparator parameter is handled in a StringQuery. Replacing the provided parameter in a StringQuery with a new name or index when the named or indexed parameter is used more then once in the query.
Closes spring-projects#1929
1 parent 752fe46 commit 23b97b4

File tree

6 files changed

+390
-17
lines changed

6 files changed

+390
-17
lines changed

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
* @author Jens Schauder
4343
* @author Oliver Gierke
4444
* @author Mark Paluch
45+
* @author Klajdi Paja
4546
* @since 2.0
4647
*/
4748
abstract class QueryParameterSetterFactory {
@@ -329,7 +330,7 @@ static jakarta.persistence.Parameter<?> of(@Nullable JpaParameter parameter, Par
329330

330331
Class<?> type = parameter == null ? Object.class : parameter.getType();
331332

332-
return new ParameterImpl<>(type, getName(parameter, binding), binding.getPosition());
333+
return new ParameterImpl<>(type, getName(parameter, binding), getPosition(binding));
333334
}
334335

335336
/**
@@ -370,9 +371,22 @@ private static String getName(@Nullable JpaParameter parameter, ParameterBinding
370371
return binding.getName();
371372
}
372373

374+
if (parameter.isNamedParameter() && StringQuery.LikeParameterBinding.class.equals(binding.getClass())) {
375+
return ((StringQuery.LikeParameterBinding) binding).getUpdatedName();
376+
}
377+
373378
return parameter.isNamedParameter() //
374379
? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name")) //
375380
: null;
376381
}
382+
383+
@Nullable
384+
private static Integer getPosition(ParameterBinding binding) {
385+
if (StringQuery.LikeParameterBinding.class.equals(binding.getClass()) ) {
386+
return ((StringQuery.LikeParameterBinding) binding).getUpdatedPosition();
387+
}
388+
return binding.getPosition();
389+
390+
}
377391
}
378392
}

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

+144-10
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
import static org.springframework.util.ObjectUtils.nullSafeHashCode;
2121

2222
import java.lang.reflect.Array;
23-
import java.util.ArrayList;
24-
import java.util.Arrays;
25-
import java.util.Collection;
26-
import java.util.List;
23+
import java.util.*;
2724
import java.util.function.BiFunction;
2825
import java.util.regex.Matcher;
2926
import java.util.regex.Pattern;
@@ -51,9 +48,11 @@
5148
* @author Diego Krupitza
5249
* @author Greg Turnquist
5350
* @author Yuriy Tsarkov
51+
* @author Klajdi Paja
5452
*/
5553
class StringQuery implements DeclaredQuery {
5654

55+
public static final String LIKE_BINDING_RENAMING_PREFIX = "_like_binding_prefix";
5756
private final String query;
5857
private final List<ParameterBinding> bindings;
5958
private final @Nullable String alias;
@@ -235,6 +234,8 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St
235234
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
236235

237236
boolean usesJpaStyleParameters = false;
237+
HashMap<Object,Integer> occurrencesMap=buildOccurrencesMap(matcher,resultingQuery);
238+
238239
while (matcher.find()) {
239240

240241
if (spelExtractor.isQuoted(matcher.start())) {
@@ -269,14 +270,23 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St
269270
case LIKE:
270271

271272
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
272-
replacement = matcher.group(3);
273+
274+
// Convert the query defined by the user by replacing repeated parameters
275+
// in a Like compare operator with a dynamic unique name.
276+
// example: Select u from user u where u.name=:q or name like %:q%
277+
// should be converted to: Select u from user u where u.name=:q or name like :q_prefix
278+
// This would make sure that the value passed to q parameter is set correctly by the JPA Provider
273279

274280
if (parameterIndex != null) {
275-
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
281+
int updatedPosition = generateNewPosition(bindings, resultingQuery, occurrencesMap, parameterIndex, likeType);
282+
LikeParameterBinding binding = new LikeParameterBinding(parameterIndex, updatedPosition, likeType, expression);
283+
checkAndRegister(binding, bindings);
284+
replacement = "?" + updatedPosition;
276285
} else {
277-
checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
286+
String updatedName = generateUpdatedName(bindings, occurrencesMap, parameterName, likeType);
287+
checkAndRegister(new LikeParameterBinding(parameterName, updatedName, likeType, expression), bindings);
278288

279-
replacement = ":" + parameterName;
289+
replacement = ":" + updatedName;
280290
}
281291

282292
break;
@@ -308,6 +318,64 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St
308318
return resultingQuery;
309319
}
310320

321+
private static String generateUpdatedName(List<ParameterBinding> bindings, HashMap<Object, Integer> occurrencesMap,
322+
String parameterName, Type likeType) {
323+
Integer occurrences = occurrencesMap.get(parameterName);
324+
boolean parameterIsRepeated = occurrences != null && occurrences > 1;
325+
String updatedName = parameterName;
326+
if (parameterIsRepeated) {
327+
Optional<LikeParameterBinding> existingLikeBinding = findLikeBindingByNameOrIndex(parameterName, null, likeType,
328+
bindings);
329+
updatedName = existingLikeBinding.map(LikeParameterBinding::getUpdatedName)
330+
.orElseGet(() -> parameterName + LIKE_BINDING_RENAMING_PREFIX);
331+
}
332+
return updatedName;
333+
}
334+
335+
private static int generateNewPosition(List<ParameterBinding> bindings, String resultingQuery,
336+
HashMap<Object, Integer> occurrencesMap, Integer parameterIndex, Type likeType) {
337+
int updatedPosition;
338+
Integer count = occurrencesMap.get(parameterIndex);
339+
if (count != null && count > 1) {
340+
int maxCurrentIndex = tryFindGreatestParameterIndexIn(resultingQuery);
341+
updatedPosition = maxCurrentIndex + 1;
342+
} else
343+
updatedPosition = parameterIndex;
344+
345+
Optional<LikeParameterBinding> existingLikeBinding = findLikeBindingByNameOrIndex(null, parameterIndex, likeType,
346+
bindings);
347+
if (existingLikeBinding.isPresent()) {
348+
updatedPosition = existingLikeBinding.get().getUpdatedPosition();
349+
}
350+
return updatedPosition;
351+
}
352+
353+
private HashMap<Object, Integer> buildOccurrencesMap(Matcher m, String resultingQuery) {
354+
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
355+
HashMap<Object, Integer> map = new HashMap<>();
356+
while (matcher.find()) {
357+
String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP);
358+
boolean indexBasedAccess=parameterIndexString != null;
359+
String parameterName = indexBasedAccess ? null : matcher.group(NAMED_PARAMETER_GROUP);
360+
Integer parameterIndex = getParameterIndex(parameterIndexString);
361+
Object param=indexBasedAccess ?parameterIndex:parameterName;
362+
map.compute(param, (key, value) -> value == null ? 1 : value+1);
363+
}
364+
return map;
365+
}
366+
367+
private static Optional<LikeParameterBinding> findLikeBindingByNameOrIndex(@Nullable String name,@Nullable Integer index,
368+
Type type, List<ParameterBinding> bindings) {
369+
370+
return bindings.stream()//
371+
.filter(it -> it instanceof LikeParameterBinding)//
372+
.map(it -> (LikeParameterBinding) it)//
373+
.filter(it -> it.getType().equals(type))//
374+
.filter(it->name==null||name.equals(it.getRequiredName()))//
375+
.filter(it->index==null || index.equals(it.getPosition()))//
376+
.findFirst();
377+
}
378+
311379
private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex,
312380
int greatestParameterIndex) {
313381

@@ -637,6 +705,8 @@ static class LikeParameterBinding extends ParameterBinding {
637705
Type.ENDING_WITH, Type.LIKE);
638706

639707
private final Type type;
708+
private String updatedName;
709+
private Integer updatedPosition;
640710

641711
/**
642712
* Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}.
@@ -669,6 +739,23 @@ static class LikeParameterBinding extends ParameterBinding {
669739
this.type = type;
670740
}
671741

742+
/**
743+
* Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter
744+
* binding input.
745+
*
746+
* @param name must not be {@literal null} or empty.
747+
* @param updatedName the updated name of the parameter in the query. A new name should be created for the Like parameter
748+
* if a parameter shows up more than once in a query, it can be used later to set the value properly.
749+
* @param type must not be {@literal null}.
750+
* @param expression may be {@literal null}.
751+
*/
752+
LikeParameterBinding(String name, String updatedName, Type type, @Nullable String expression) {
753+
this(name, type, expression);
754+
Assert.hasText(updatedName, "Updated Name must not be null or empty");
755+
this.updatedName = updatedName;
756+
757+
}
758+
672759
/**
673760
* Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
674761
*
@@ -699,6 +786,23 @@ static class LikeParameterBinding extends ParameterBinding {
699786
this.type = type;
700787
}
701788

789+
/**
790+
* Creates a new {@link LikeParameterBinding} for the parameter with the given position, the updated position and {@link Type}.
791+
*
792+
* @param position position of the parameter in the query.
793+
* @param updatedPosition the updated position of the parameter in the query. A new position should be created for the Like parameter
794+
* if a parameter shows up more than once in a query that can be used later to set the value properly.
795+
* @param type must not be {@literal null}.
796+
* @param expression may be {@literal null}.
797+
*/
798+
LikeParameterBinding(int position, int updatedPosition, Type type, @Nullable String expression) {
799+
this(position, type, expression);
800+
801+
Assert.isTrue(updatedPosition > 0, "UpdatedPosition must be greater than zero");
802+
this.updatedPosition = updatedPosition;
803+
804+
}
805+
702806
/**
703807
* Returns the {@link Type} of the binding.
704808
*
@@ -708,6 +812,32 @@ public Type getType() {
708812
return type;
709813
}
710814

815+
/**
816+
* Returns the Updated Position of the binding in the query.
817+
*
818+
* @return the updated position
819+
*/
820+
public Integer getUpdatedPosition() {
821+
return updatedPosition;
822+
}
823+
824+
/**
825+
* Returns the Updated Name of the binding in the query.
826+
*
827+
* @return the updated name
828+
*/
829+
public String getUpdatedName() {
830+
return updatedName;
831+
}
832+
833+
/**
834+
* Check if the bindings updated name is equal to the provided name.
835+
*
836+
*/
837+
boolean hasUpdatedName(@Nullable String name) {
838+
return this.updatedPosition == null && this.updatedName != null && this.updatedName.equals(name);
839+
}
840+
711841
/**
712842
* Prepares the given raw keyword according to the like type.
713843
*/
@@ -742,7 +872,8 @@ public boolean equals(Object obj) {
742872

743873
LikeParameterBinding that = (LikeParameterBinding) obj;
744874

745-
return super.equals(obj) && this.type.equals(that.type);
875+
return super.equals(obj) && this.type.equals(that.type) && Objects.equals(this.updatedName,
876+
that.updatedName) && Objects.equals(this.updatedPosition, that.updatedPosition);
746877
}
747878

748879
@Override
@@ -751,13 +882,16 @@ public int hashCode() {
751882
int result = super.hashCode();
752883

753884
result += nullSafeHashCode(this.type);
885+
result += nullSafeHashCode(this.updatedName);
886+
result += nullSafeHashCode(this.updatedPosition);
754887

755888
return result;
756889
}
757890

758891
@Override
759892
public String toString() {
760-
return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
893+
return String.format("LikeBinding [name: %s, position: %d, type: %s, updatedName: %s, updatedPosition: %d]",
894+
getName(), getPosition(), type, updatedPosition, updatedPosition);
761895
}
762896

763897
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2008-2023 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.domain.sample;
17+
18+
import jakarta.persistence.Entity;
19+
import jakarta.persistence.GeneratedValue;
20+
import jakarta.persistence.Id;
21+
import lombok.Data;
22+
import lombok.NoArgsConstructor;
23+
24+
/**
25+
* @author Klajdi Paja
26+
*/
27+
@Entity
28+
@Data
29+
@NoArgsConstructor
30+
public class EmployeeWithMultipleFields {
31+
32+
@Id
33+
@GeneratedValue
34+
private Integer id;
35+
private String name;
36+
private String lastName;
37+
private String username;
38+
39+
public EmployeeWithMultipleFields(String name, String lastName, String username) {
40+
this.name = name;
41+
this.lastName = lastName;
42+
this.username = username;
43+
}
44+
}

0 commit comments

Comments
 (0)