diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index 82df40c3b3..894eb1efe1 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -15,9 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static java.util.regex.Pattern.CASE_INSENSITIVE; -import static org.springframework.util.ObjectUtils.nullSafeEquals; -import static org.springframework.util.ObjectUtils.nullSafeHashCode; +import static java.util.regex.Pattern.*; +import static org.springframework.util.ObjectUtils.*; import java.lang.reflect.Array; import java.util.ArrayList; @@ -371,7 +370,43 @@ private static String replaceFirst(String text, String substring, String replace return text; } - return text.substring(0, index) + replacement + text.substring(index + substring.length()); + return text.substring(0, index) + potentiallyWrapWithWildcards(replacement, substring) + + text.substring(index + substring.length()); + } + + /** + * If there are any pre- or post-wildcards ({@literal %}), replace them with a {@literal CONCAT} function and proper + * wildcards as string literals. NOTE: {@literal CONCAT} appears to be a standard function across relational + * databases as well as JPA providers. + * + * @param replacement + * @param substring + * @return the replacement string properly wrapped in a {@literal CONCAT} function with wildcards applied. + * @since 3.1 + */ + private static String potentiallyWrapWithWildcards(String replacement, String substring) { + + boolean wildcards = substring.startsWith("%") || substring.endsWith("%"); + + if (!wildcards) { + return replacement; + } + + StringBuilder concatWrapper = new StringBuilder("CONCAT("); + + if (substring.startsWith("%")) { + concatWrapper.append("'%',"); + } + + concatWrapper.append(replacement); + + if (substring.endsWith("%")) { + concatWrapper.append(",'%'"); + } + + concatWrapper.append(")"); + + return concatWrapper.toString(); } @Nullable @@ -761,28 +796,12 @@ public Type getType() { } /** - * Prepares the given raw keyword according to the like type. + * Extracts the raw value properly. */ @Nullable @Override public Object prepare(@Nullable Object value) { - - Object condensedValue = PersistenceProvider.condense(value); - if (condensedValue == null) { - return null; - } - - switch (type) { - case STARTING_WITH: - return String.format("%s%%", condensedValue); - case ENDING_WITH: - return String.format("%%%s", condensedValue); - case CONTAINING: - return String.format("%%%s%%", condensedValue); - case LIKE: - default: - return condensedValue; - } + return PersistenceProvider.condense(value); } /* diff --git a/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java index 555aa23d96..bcd0556bff 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java @@ -84,14 +84,4 @@ void setsUpInstanceForIndex() { assertThat(binding.hasPosition(1)).isTrue(); assertThat(binding.getType()).isEqualTo(Type.CONTAINING); } - - @Test - void augmentsValueCorrectly() { - - assertAugmentedValue(Type.CONTAINING, "%value%"); - assertAugmentedValue(Type.ENDING_WITH, "%value"); - assertAugmentedValue(Type.STARTING_WITH, "value%"); - - assertThat(new LikeParameterBinding(1, Type.CONTAINING).prepare(null)).isNull(); - } } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeHibernateIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeHibernateIntegrationTests.java index 8e67cb2ab5..a96cd9dc18 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeHibernateIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeHibernateIntegrationTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import java.util.List; @@ -102,6 +102,40 @@ void customQueryWithNullMatch() { assertThat(Employees).extracting(EmployeeWithName::getName).isEmpty(); } + @Test // GH-2939 + void customQueryWithMultipleMatchInNative() { + + List Employees = repository.customQueryWithNullableParamInNative("Baggins"); + + assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins", + "Bilbo Baggins"); + } + + @Test // GH-2939 + void customQueryWithSingleMatchInNative() { + + List Employees = repository.customQueryWithNullableParamInNative("Frodo"); + + assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins"); + } + + @Test + void customQueryWithEmptyStringMatchInNative() { + + List Employees = repository.customQueryWithNullableParamInNative(""); + + assertThat(Employees).extracting(EmployeeWithName::getName).containsExactlyInAnyOrder("Frodo Baggins", + "Bilbo Baggins"); + } + + @Test // GH-2939 + void customQueryWithNullMatchInNative() { + + List Employees = repository.customQueryWithNullableParamInNative(null); + + assertThat(Employees).extracting(EmployeeWithName::getName).isEmpty(); + } + @Test void derivedQueryStartsWithSingleMatch() { @@ -235,6 +269,9 @@ public interface EmployeeWithNullLikeRepository extends JpaRepository customQueryWithNullableParam(@Nullable @Param("partialName") String partialName); + @Query(value = "select * from EmployeeWithName as e where e.name like %:partialName%", nativeQuery = true) + List customQueryWithNullableParamInNative(@Nullable @Param("partialName") String partialName); + List findByNameStartsWith(@Nullable String partialName); List findByNameEndsWith(@Nullable String partialName); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index 9203e9dbbd..de671b3367 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -68,7 +68,7 @@ void detectsPositionalLikeBindings() { assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) - .isEqualTo("select u from User u where u.firstname like ?1 or u.lastname like ?2"); + .isEqualTo("select u from User u where u.firstname like CONCAT('%',?1,'%') or u.lastname like CONCAT('%',?2)"); List bindings = query.getParameterBindings(); assertThat(bindings).hasSize(2); @@ -90,7 +90,7 @@ void detectsNamedLikeBindings() { StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true); assertThat(query.hasParameterBindings()).isTrue(); - assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname"); + assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like CONCAT('%',:firstname)"); List bindings = query.getParameterBindings(); assertThat(bindings).hasSize(1); @@ -209,8 +209,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() { assertNamedBinding(ParameterBinding.class, "word", bindings.get(1)); softly.assertThat(query.getQueryString()) - .isEqualTo("SELECT a FROM Article a WHERE a.overview LIKE :escapedWord ESCAPE '~'" - + " OR a.content LIKE :escapedWord ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC"); + .isEqualTo("SELECT a FROM Article a WHERE a.overview LIKE CONCAT('%',:escapedWord,'%') ESCAPE '~'" + + " OR a.content LIKE CONCAT('%',:escapedWord,'%') ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC"); softly.assertAll(); }