Skip to content

Commit 4605cd2

Browse files
committed
Rewrite LIKE clauses with wildcards as CONCAT functions.
We support wrapping parameters (named or positional) with optional wildcards when doing LIKE patterns. This is out-of-band and requires moving the wildcards into the bindings. To stop doing this and causing race conditions, we can instead rewrite the queries using the CONCAT function. This function is standard across relational database (native queries) as well as JPA providers (Hibernate and EclipseLink). See #2939 See #2760 Original Pull Request: #2940 Superceding Pull Request: #2944
1 parent 0f09803 commit 4605cd2

File tree

4 files changed

+83
-37
lines changed

4 files changed

+83
-37
lines changed

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

+41-22
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static java.util.regex.Pattern.CASE_INSENSITIVE;
19-
import static org.springframework.util.ObjectUtils.nullSafeEquals;
20-
import static org.springframework.util.ObjectUtils.nullSafeHashCode;
18+
import static java.util.regex.Pattern.*;
19+
import static org.springframework.util.ObjectUtils.*;
2120

2221
import java.lang.reflect.Array;
2322
import java.util.ArrayList;
@@ -371,7 +370,43 @@ private static String replaceFirst(String text, String substring, String replace
371370
return text;
372371
}
373372

374-
return text.substring(0, index) + replacement + text.substring(index + substring.length());
373+
return text.substring(0, index) + potentiallyWrapWithWildcards(replacement, substring)
374+
+ text.substring(index + substring.length());
375+
}
376+
377+
/**
378+
* If there are any pre- or post-wildcards ({@literal %}), replace them with a {@literal CONCAT} function and proper
379+
* wildcards as string literals. NOTE: {@literal CONCAT} appears to be a standard function across relational
380+
* databases as well as JPA providers.
381+
*
382+
* @param replacement
383+
* @param substring
384+
* @return the replacement string properly wrapped in a {@literal CONCAT} function with wildcards applied.
385+
* @since 3.1
386+
*/
387+
private static String potentiallyWrapWithWildcards(String replacement, String substring) {
388+
389+
boolean wildcards = substring.startsWith("%") || substring.endsWith("%");
390+
391+
if (!wildcards) {
392+
return replacement;
393+
}
394+
395+
StringBuilder concatWrapper = new StringBuilder("CONCAT(");
396+
397+
if (substring.startsWith("%")) {
398+
concatWrapper.append("'%',");
399+
}
400+
401+
concatWrapper.append(replacement);
402+
403+
if (substring.endsWith("%")) {
404+
concatWrapper.append(",'%'");
405+
}
406+
407+
concatWrapper.append(")");
408+
409+
return concatWrapper.toString();
375410
}
376411

377412
@Nullable
@@ -761,28 +796,12 @@ public Type getType() {
761796
}
762797

763798
/**
764-
* Prepares the given raw keyword according to the like type.
799+
* Extracts the raw value properly.
765800
*/
766801
@Nullable
767802
@Override
768803
public Object prepare(@Nullable Object value) {
769-
770-
Object condensedValue = PersistenceProvider.condense(value);
771-
if (condensedValue == null) {
772-
return null;
773-
}
774-
775-
switch (type) {
776-
case STARTING_WITH:
777-
return String.format("%s%%", condensedValue);
778-
case ENDING_WITH:
779-
return String.format("%%%s", condensedValue);
780-
case CONTAINING:
781-
return String.format("%%%s%%", condensedValue);
782-
case LIKE:
783-
default:
784-
return condensedValue;
785-
}
804+
return PersistenceProvider.condense(value);
786805
}
787806

788807
/*

src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java

-10
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,4 @@ void setsUpInstanceForIndex() {
8484
assertThat(binding.hasPosition(1)).isTrue();
8585
assertThat(binding.getType()).isEqualTo(Type.CONTAINING);
8686
}
87-
88-
@Test
89-
void augmentsValueCorrectly() {
90-
91-
assertAugmentedValue(Type.CONTAINING, "%value%");
92-
assertAugmentedValue(Type.ENDING_WITH, "%value");
93-
assertAugmentedValue(Type.STARTING_WITH, "value%");
94-
95-
assertThat(new LikeParameterBinding(1, Type.CONTAINING).prepare(null)).isNull();
96-
}
9787
}

src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeHibernateIntegrationTests.java

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

18-
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.*;
1919

2020
import java.util.Arrays;
2121
import java.util.List;
@@ -102,6 +102,40 @@ void customQueryWithNullMatch() {
102102
assertThat(Employees).extracting(EmployeeWithName::getName).isEmpty();
103103
}
104104

105+
@Test // GH-2939
106+
void customQueryWithMultipleMatchInNative() {
107+
108+
List<EmployeeWithName> Employees = repository.customQueryWithNullableParamInNative("Baggins");
109+
110+
assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins",
111+
"Bilbo Baggins");
112+
}
113+
114+
@Test // GH-2939
115+
void customQueryWithSingleMatchInNative() {
116+
117+
List<EmployeeWithName> Employees = repository.customQueryWithNullableParamInNative("Frodo");
118+
119+
assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins");
120+
}
121+
122+
@Test
123+
void customQueryWithEmptyStringMatchInNative() {
124+
125+
List<EmployeeWithName> Employees = repository.customQueryWithNullableParamInNative("");
126+
127+
assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins",
128+
"Bilbo Baggins");
129+
}
130+
131+
@Test // GH-2939
132+
void customQueryWithNullMatchInNative() {
133+
134+
List<EmployeeWithName> Employees = repository.customQueryWithNullableParamInNative(null);
135+
136+
assertThat(Employees).extracting(EmployeeWithName::getName).isEmpty();
137+
}
138+
105139
@Test
106140
void derivedQueryStartsWithSingleMatch() {
107141

@@ -235,6 +269,9 @@ public interface EmployeeWithNullLikeRepository extends JpaRepository<EmployeeWi
235269
@Query("select e from EmployeeWithName e where e.name like %:partialName%")
236270
List<EmployeeWithName> customQueryWithNullableParam(@Nullable @Param("partialName") String partialName);
237271

272+
@Query(value = "select * from EmployeeWithName as e where e.name like %:partialName%", nativeQuery = true)
273+
List<EmployeeWithName> customQueryWithNullableParamInNative(@Nullable @Param("partialName") String partialName);
274+
238275
List<EmployeeWithName> findByNameStartsWith(@Nullable String partialName);
239276

240277
List<EmployeeWithName> findByNameEndsWith(@Nullable String partialName);

src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ void detectsPositionalLikeBindings() {
6868

6969
assertThat(query.hasParameterBindings()).isTrue();
7070
assertThat(query.getQueryString())
71-
.isEqualTo("select u from User u where u.firstname like ?1 or u.lastname like ?2");
71+
.isEqualTo("select u from User u where u.firstname like CONCAT('%',?1,'%') or u.lastname like CONCAT('%',?2)");
7272

7373
List<ParameterBinding> bindings = query.getParameterBindings();
7474
assertThat(bindings).hasSize(2);
@@ -90,7 +90,7 @@ void detectsNamedLikeBindings() {
9090
StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true);
9191

9292
assertThat(query.hasParameterBindings()).isTrue();
93-
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname");
93+
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like CONCAT('%',:firstname)");
9494

9595
List<ParameterBinding> bindings = query.getParameterBindings();
9696
assertThat(bindings).hasSize(1);
@@ -209,8 +209,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() {
209209
assertNamedBinding(ParameterBinding.class, "word", bindings.get(1));
210210

211211
softly.assertThat(query.getQueryString())
212-
.isEqualTo("SELECT a FROM Article a WHERE a.overview LIKE :escapedWord ESCAPE '~'"
213-
+ " OR a.content LIKE :escapedWord ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC");
212+
.isEqualTo("SELECT a FROM Article a WHERE a.overview LIKE CONCAT('%',:escapedWord,'%') ESCAPE '~'"
213+
+ " OR a.content LIKE CONCAT('%',:escapedWord,'%') ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC");
214214

215215
softly.assertAll();
216216
}

0 commit comments

Comments
 (0)