Skip to content

Commit 42831d4

Browse files
committed
Use COALESCE for LIKE with wildcards.
To handle potential NULL values when LIKE is combined with wildcards, insert a COALESCE function. See #3041
1 parent 684ed61 commit 42831d4

File tree

3 files changed

+42
-14
lines changed

3 files changed

+42
-14
lines changed

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

+32-7
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class StringQuery implements DeclaredQuery {
7878

7979
Metadata queryMeta = new Metadata();
8080
this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
81-
this.bindings, queryMeta);
81+
this.bindings, queryMeta, isNative);
8282

8383
this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
8484

@@ -212,7 +212,7 @@ enum ParameterBindingParser {
212212
* the cleaned up query.
213213
*/
214214
private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
215-
List<ParameterBinding> bindings, Metadata queryMeta) {
215+
List<ParameterBinding> bindings, Metadata queryMeta, boolean isNative) {
216216

217217
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
218218
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
@@ -299,7 +299,7 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St
299299
}
300300

301301
if (replacement != null) {
302-
resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement);
302+
resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement, isNative);
303303
}
304304

305305
}
@@ -327,14 +327,14 @@ private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean p
327327
return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
328328
}
329329

330-
private static String replaceFirst(String text, String substring, String replacement) {
330+
private static String replaceFirst(String text, String substring, String replacement, boolean isNative) {
331331

332332
int index = text.indexOf(substring);
333333
if (index < 0) {
334334
return text;
335335
}
336336

337-
return text.substring(0, index) + potentiallyWrapWithWildcards(replacement, substring)
337+
return text.substring(0, index) + potentiallyWrapWithWildcards(replacement, substring, isNative)
338338
+ text.substring(index + substring.length());
339339
}
340340

@@ -348,7 +348,7 @@ private static String replaceFirst(String text, String substring, String replace
348348
* @return the replacement string properly wrapped in a {@literal CONCAT} function with wildcards applied.
349349
* @since 3.1
350350
*/
351-
private static String potentiallyWrapWithWildcards(String replacement, String substring) {
351+
private static String potentiallyWrapWithWildcards(String replacement, String substring, boolean isNative) {
352352

353353
boolean wildcards = substring.startsWith("%") || substring.endsWith("%");
354354

@@ -362,7 +362,11 @@ private static String potentiallyWrapWithWildcards(String replacement, String su
362362
concatWrapper.append("'%',");
363363
}
364364

365-
concatWrapper.append(replacement);
365+
if (isNative) {
366+
concatWrapper.append(coalesce(replacement, "''"));
367+
} else {
368+
concatWrapper.append(coalesce(castAsText(replacement), "''"));
369+
}
366370

367371
if (substring.endsWith("%")) {
368372
concatWrapper.append(",'%'");
@@ -373,6 +377,27 @@ private static String potentiallyWrapWithWildcards(String replacement, String su
373377
return concatWrapper.toString();
374378
}
375379

380+
/**
381+
* Apply the {@literal COALESCE} function with a default value.
382+
*
383+
* @param expression
384+
* @param defaultValue
385+
* @return
386+
*/
387+
private static String coalesce(String expression, String defaultValue) {
388+
return "COALESCE(" + expression + "," + defaultValue + ")";
389+
}
390+
391+
/**
392+
* {@literal CAST} the expression into a {@literal text}.
393+
*
394+
* @param expression
395+
* @return
396+
*/
397+
private static String castAsText(String expression) {
398+
return "CAST(" + expression + " as text)";
399+
}
400+
376401
@Nullable
377402
private static Integer getParameterIndex(@Nullable String parameterIndexString) {
378403

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ void customQueryWithNullMatch() {
9999

100100
List<EmployeeWithName> Employees = repository.customQueryWithNullableParam(null);
101101

102-
assertThat(Employees).extracting(EmployeeWithName::getName).isEmpty();
102+
assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins",
103+
"Bilbo Baggins");
103104
}
104105

105106
@Test // GH-2939
@@ -133,7 +134,8 @@ void customQueryWithNullMatchInNative() {
133134

134135
List<EmployeeWithName> Employees = repository.customQueryWithNullableParamInNative(null);
135136

136-
assertThat(Employees).extracting(EmployeeWithName::getName).isEmpty();
137+
assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins",
138+
"Bilbo Baggins");
137139
}
138140

139141
@Test

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ void detectsPositionalLikeBindings() {
6464
true);
6565

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

7070
List<ParameterBinding> bindings = query.getParameterBindings();
7171
assertThat(bindings).hasSize(2);
@@ -87,7 +87,8 @@ void detectsNamedLikeBindings() {
8787
StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true);
8888

8989
assertThat(query.hasParameterBindings()).isTrue();
90-
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like CONCAT('%',:firstname)");
90+
assertThat(query.getQueryString())
91+
.isEqualTo("select u from User u where u.firstname like CONCAT('%',COALESCE(:firstname,''))");
9192

9293
List<ParameterBinding> bindings = query.getParameterBindings();
9394
assertThat(bindings).hasSize(1);
@@ -200,8 +201,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() {
200201
assertNamedBinding(ParameterBinding.class, "word", bindings.get(1));
201202

202203
assertThat(query.getQueryString())
203-
.isEqualTo("SELECT a FROM Article a WHERE a.overview LIKE CONCAT('%',:escapedWord,'%') ESCAPE '~'"
204-
+ " OR a.content LIKE CONCAT('%',:escapedWord,'%') ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC");
204+
.isEqualTo("SELECT a FROM Article a WHERE a.overview LIKE CONCAT('%',COALESCE(:escapedWord,''),'%') ESCAPE '~'"
205+
+ " OR a.content LIKE CONCAT('%',COALESCE(:escapedWord,''),'%') ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC");
205206
}
206207

207208
@Test // DATAJPA-483

0 commit comments

Comments
 (0)