diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index e7bdb6c27..ea378384e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -327,14 +327,15 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer } /** - * Because Hibernate's {@literal TypedParameterValue} is only used to wrap a {@literal null}, swap it out with an - * empty string for query creation. + * Because Hibernate's {@literal TypedParameterValue} is only used to wrap a {@literal null}, swap it out with + * {@code null} for query creation. * * @param value * @return the original value or null. * @since 3.0 */ - public static Object unwrapTypedParameterValue(Object value) { + @Nullable + public static Object unwrapTypedParameterValue(@Nullable Object value) { return typedParameterValueClass != null && typedParameterValueClass.isInstance(value) // ? null // diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 2ff2bc46d..6cdd559f3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -46,10 +46,11 @@ class ParameterBinderFactory { Assert.notNull(parameters, "JpaParameters must not be null"); + QueryParameterSetterFactory likeFactory = QueryParameterSetterFactory.forLikeRewrite(parameters); QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters); List bindings = getBindings(parameters); - return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); + return new ParameterBinder(parameters, createSetters(bindings, likeFactory, setterFactory)); } /** @@ -95,9 +96,12 @@ class ParameterBinderFactory { List bindings = query.getParameterBindings(); QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser, evaluationContextProvider, parameters); + + QueryParameterSetterFactory like = QueryParameterSetterFactory.forLikeRewrite(parameters); QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters); - return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory), + return new ParameterBinder(parameters, + createSetters(bindings, query, expressionSetterFactory, like, basicSetterFactory), !query.usesPaging()); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index 5cc137d82..f2d35b84e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -24,6 +24,7 @@ import java.util.function.Function; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter; +import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding; import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -62,6 +63,20 @@ abstract class QueryParameterSetterFactory { return new BasicQueryParameterSetterFactory(parameters); } + /** + * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters} applying LIKE rewrite for + * renamed {@code :foo%} or {@code %:bar} bindings. + * + * @param parameters must not be {@literal null}. + * @return a basic {@link QueryParameterSetterFactory} that can handle named parameters. + */ + static QueryParameterSetterFactory forLikeRewrite(JpaParameters parameters) { + + Assert.notNull(parameters, "JpaParameters must not be null"); + + return new LikeRewritingQueryParameterSetterFactory(parameters); + } + /** * Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters} and * {@link ParameterMetadata}. @@ -117,6 +132,29 @@ abstract class QueryParameterSetterFactory { ParameterImpl.of(parameter, binding), temporalType); } + @Nullable + private static JpaParameter findParameterForBinding(Parameters parameters, String name) { + + JpaParameters bindableParameters = parameters.getBindableParameters(); + + for (JpaParameter bindableParameter : bindableParameters) { + if (name.equals(getRequiredName(bindableParameter))) { + return bindableParameter; + } + } + + return null; + } + + private static String getRequiredName(JpaParameter p) { + return p.getName().orElseThrow(() -> new IllegalStateException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED)); + } + + @Nullable + static Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { + return accessor.getValue(parameter); + } + /** * Handles bindings that are SpEL expressions by evaluating the expression to obtain a value. * @@ -176,6 +214,46 @@ abstract class QueryParameterSetterFactory { } } + /** + * Handles bindings that use Like-rewriting. + * + * @author Mark Paluch + * @since 3.1.2 + */ + private static class LikeRewritingQueryParameterSetterFactory extends QueryParameterSetterFactory { + + private final Parameters parameters; + + /** + * @param parameters must not be {@literal null}. + */ + LikeRewritingQueryParameterSetterFactory(Parameters parameters) { + + Assert.notNull(parameters, "Parameters must not be null"); + + this.parameters = parameters; + } + + @Nullable + @Override + public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { + + if (binding.isExpression() || !(binding instanceof LikeParameterBinding likeBinding) + || !declaredQuery.hasNamedParameter()) { + return null; + } + JpaParameter parameter = QueryParameterSetterFactory.findParameterForBinding((JpaParameters) parameters, + likeBinding.getDeclaredName()); + + if (parameter == null) { + return null; + } + + return createSetter(values -> values.getValue(parameter), binding, parameter); + } + + } + /** * Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters. * @@ -205,7 +283,7 @@ abstract class QueryParameterSetterFactory { JpaParameter parameter; if (declaredQuery.hasNamedParameter()) { - parameter = findParameterForBinding(binding); + parameter = findParameterForBinding(parameters, binding.getRequiredName()); } else { int parameterIndex = binding.getRequiredPosition() - 1; @@ -228,28 +306,6 @@ abstract class QueryParameterSetterFactory { : createSetter(values -> getValue(values, parameter), binding, parameter); } - @Nullable - private JpaParameter findParameterForBinding(ParameterBinding binding) { - - JpaParameters bindableParameters = parameters.getBindableParameters(); - - for (JpaParameter bindableParameter : bindableParameters) { - if (binding.getRequiredName().equals(getName(bindableParameter))) { - return bindableParameter; - } - } - - return null; - } - - @Nullable - private Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { - return accessor.getValue(parameter); - } - - private static String getName(JpaParameter p) { - return p.getName().orElseThrow(() -> new IllegalStateException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED)); - } } /** @@ -366,7 +422,7 @@ abstract class QueryParameterSetterFactory { @Nullable private static String getName(@Nullable JpaParameter parameter, ParameterBinding binding) { - if (parameter == null) { + if (binding.hasName() || parameter == null) { return binding.getName(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index e56185ab1..f12f6aff7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -28,11 +28,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.repository.query.SpelQueryContext; import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -233,6 +236,8 @@ class StringQuery implements DeclaredQuery { int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; + LikeParameterBindings likeParameterBindings = new LikeParameterBindings(); + boolean usesJpaStyleParameters = false; while (matcher.find()) { @@ -273,9 +278,11 @@ class StringQuery implements DeclaredQuery { if (parameterIndex != null) { checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings); } else { - checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings); - replacement = ":" + parameterName; + LikeParameterBinding binding = likeParameterBindings.getOrCreate(parameterName, likeType, expression); + checkAndRegister(binding, bindings); + + replacement = ":" + binding.getRequiredName(); } break; @@ -334,43 +341,7 @@ class StringQuery implements DeclaredQuery { return text; } - 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(); + return text.substring(0, index) + replacement + text.substring(index + substring.length()); } @Nullable @@ -460,6 +431,60 @@ class StringQuery implements DeclaredQuery { } } + /** + * Utility to create unique parameter bindings for LIKE that can be evaluated by + * {@code LikeRewritingQueryParameterSetterFactory}. + * + * @author Mark Paluch + * @since 3.1.2 + */ + static class LikeParameterBindings { + + private final MultiValueMap likeBindings = new LinkedMultiValueMap<>(); + + /** + * Get an existing or create a new {@link LikeParameterBinding} if a previously bound {@code LIKE} expression cannot + * be reused. + * + * @param parameterName the parameter name as declared in the actual JPQL query. + * @param likeType type of the LIKE expression. + * @param expression expression content if the LIKE comparison value is provided by a SpEL expression. + * @return the Like binding. Can return an already existing binding. + */ + LikeParameterBinding getOrCreate(String parameterName, Type likeType, @Nullable String expression) { + + List likeParameterBindings = likeBindings.computeIfAbsent(parameterName, + s -> new ArrayList<>()); + LikeParameterBinding reuse = null; + + // unique parameters only required for literals as expressions create unique parameter names + if (expression == null) { + for (LikeParameterBinding likeParameterBinding : likeParameterBindings) { + + if (likeParameterBinding.type == likeType) { + reuse = likeParameterBinding; + break; + } + } + } + + String declaredParameterName = parameterName; + if (reuse != null) { + return reuse; + } + + if (!likeParameterBindings.isEmpty()) { + parameterName = parameterName + "_" + likeParameterBindings.size(); + } + + LikeParameterBinding binding = new LikeParameterBinding(parameterName, declaredParameterName, likeType, + expression); + likeParameterBindings.add(binding); + + return binding; + } + } + /** * A generic parameter binding with name or position information. * @@ -511,6 +536,10 @@ class StringQuery implements DeclaredQuery { return this.position == null && this.name != null && this.name.equals(name); } + boolean hasName() { + return this.position == null && !ObjectUtils.isEmpty(this.name); + } + /** * Returns whether the binding has the given position. Will always be {@literal false} in case the * {@link ParameterBinding} has been set up from a name. @@ -519,6 +548,10 @@ class StringQuery implements DeclaredQuery { return position != null && this.name == null && position.equals(this.position); } + boolean hasPosition() { + return position != null && this.name == null; + } + /** * @return the name */ @@ -665,6 +698,7 @@ class StringQuery implements DeclaredQuery { * * @author Oliver Gierke * @author Thomas Darimont + * @author Mark Paluch */ static class LikeParameterBinding extends ParameterBinding { @@ -673,35 +707,45 @@ class StringQuery implements DeclaredQuery { private final Type type; + private final @Nullable String declaredName; + /** * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}. * - * @param name must not be {@literal null} or empty. + * @param name parameter name in the final query, must not be {@literal null} or empty. + * @param declaredName name of the declared parameter from the original query, referring to a + * {@link JpaParameter#getName()}, must not be {@literal null} or empty. * @param type must not be {@literal null}. */ - LikeParameterBinding(String name, Type type) { - this(name, type, null); + LikeParameterBinding(String name, String declaredName, Type type) { + this(name, declaredName, type, null); } /** * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter * binding input. * - * @param name must not be {@literal null} or empty. + * @param name parameter name in the final query, must not be {@literal null} or empty. + * @param declaredName name of the declared parameter from the original query, referring to a + * {@link JpaParameter#getName()}, must not be {@literal null} or empty. * @param type must not be {@literal null}. * @param expression may be {@literal null}. */ - LikeParameterBinding(String name, Type type, @Nullable String expression) { + LikeParameterBinding(String name, String declaredName, Type type, @Nullable String expression) { super(name, null, expression); Assert.hasText(name, "Name must not be null or empty"); + if (expression == null && !StringUtils.hasText(declaredName)) { + throw new IllegalArgumentException("Declared name must not be null or empty"); + } Assert.notNull(type, "Type must not be null"); Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); this.type = type; + this.declaredName = declaredName; } /** @@ -732,6 +776,7 @@ class StringQuery implements DeclaredQuery { String.format("Type must be one of %s", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); this.type = type; + this.declaredName = null; } /** @@ -743,13 +788,29 @@ class StringQuery implements DeclaredQuery { return type; } + @Nullable + public String getDeclaredName() { + return declaredName; + } + /** - * Extracts the raw value properly. + * Prepares the given raw keyword according to the like type. */ @Nullable @Override public Object prepare(@Nullable Object value) { - return PersistenceProvider.unwrapTypedParameterValue(value); + + Object unwrapped = PersistenceProvider.unwrapTypedParameterValue(value); + if (unwrapped == null) { + return null; + } + + return switch (type) { + case STARTING_WITH -> String.format("%s%%", unwrapped); + case ENDING_WITH -> String.format("%%%s", unwrapped); + case CONTAINING -> String.format("%%%s%%", unwrapped); + default -> unwrapped; + }; } @Override @@ -802,6 +863,7 @@ class StringQuery implements DeclaredQuery { return Type.LIKE; } + } static class Metadata { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index e8f2848dd..6ed4ee131 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -974,14 +974,13 @@ class UserRepositoryTests { assertThat(result).containsOnly(thirdUser); } - @Test // DATAJPA-292 + @Test // DATAJPA-292, GH-3041 void executesManualQueryWithNamedLikeExpressionCorrectly() { flushTestUsers(); - List result = repository.findByFirstnameLikeNamed("Da"); - - assertThat(result).containsOnly(thirdUser); + assertThat(repository.findByFirstnameLikeNamed("Da")).containsOnly(thirdUser); + assertThat(repository.findByFirstnameLikeNamed("in")).containsOnly(fourthUser); } @Test // DATAJPA-231 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java index bcd0556bf..d94779b2c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/LikeBindingUnitTests.java @@ -32,28 +32,28 @@ class LikeBindingUnitTests { private static void assertAugmentedValue(Type type, Object value) { - LikeParameterBinding binding = new LikeParameterBinding("foo", type); + LikeParameterBinding binding = new LikeParameterBinding("foo", "foo", type); assertThat(binding.prepare("value")).isEqualTo(value); } @Test void rejectsNullName() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding(null, Type.CONTAINING)); + assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding(null, "", Type.CONTAINING)); } @Test void rejectsEmptyName() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("", Type.CONTAINING)); + assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("", "", Type.CONTAINING)); } @Test void rejectsNullType() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("foo", null)); + assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("foo", "foo", null)); } @Test void rejectsInvalidType() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("foo", Type.SIMPLE_PROPERTY)); + assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("foo", "foo", Type.SIMPLE_PROPERTY)); } @Test @@ -64,7 +64,7 @@ class LikeBindingUnitTests { @Test void setsUpInstanceForName() { - LikeParameterBinding binding = new LikeParameterBinding("foo", Type.CONTAINING); + LikeParameterBinding binding = new LikeParameterBinding("foo", "foo", Type.CONTAINING); assertThat(binding.hasName("foo")).isTrue(); assertThat(binding.hasName("bar")).isFalse(); @@ -84,4 +84,14 @@ class LikeBindingUnitTests { 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/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index 3c7ece5c4..d36579bb6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -36,6 +36,7 @@ import org.springframework.data.repository.query.parser.Part.Type; * @author Nils Borrmann * @author Andriy Redko * @author Diego Krupitza + * @author Mark Paluch */ class StringQueryUnitTests { @@ -65,7 +66,7 @@ class StringQueryUnitTests { assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) - .isEqualTo("select u from User u where u.firstname like CONCAT('%',?1,'%') or u.lastname like CONCAT('%',?2)"); + .isEqualTo("select u from User u where u.firstname like ?1 or u.lastname like ?2"); List bindings = query.getParameterBindings(); assertThat(bindings).hasSize(2); @@ -87,7 +88,7 @@ class StringQueryUnitTests { 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 CONCAT('%',:firstname)"); + assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname"); List bindings = query.getParameterBindings(); assertThat(bindings).hasSize(1); @@ -98,6 +99,55 @@ class StringQueryUnitTests { assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); } + @Test // DATAJPA-292 + void rewritesNamedLikeToUniqueParametersIfNecessary() { + + StringQuery query = new StringQuery( + "select u from User u where u.firstname like %:firstname or u.firstname like :firstname%", true); + + assertThat(query.hasParameterBindings()).isTrue(); + assertThat(query.getQueryString()) + .isEqualTo("select u from User u where u.firstname like :firstname or u.firstname like :firstname_1"); + + List bindings = query.getParameterBindings(); + assertThat(bindings).hasSize(2); + + LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); + assertThat(binding).isNotNull(); + assertThat(binding.hasName("firstname")).isTrue(); + assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); + + binding = (LikeParameterBinding) bindings.get(1); + assertThat(binding).isNotNull(); + assertThat(binding.hasName("firstname_1")).isTrue(); + assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH); + } + + @Test // DATAJPA-292 + void reusesLikeBindingsWherePossible() { + + StringQuery query = new StringQuery( + "select u from User u where u.firstname like %:firstname or u.firstname like %:firstname% or u.firstname like %:firstname% or u.firstname like %:firstname", + true); + + assertThat(query.hasParameterBindings()).isTrue(); + assertThat(query.getQueryString()).isEqualTo( + "select u from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname like :firstname_1 or u.firstname like :firstname"); + + List bindings = query.getParameterBindings(); + assertThat(bindings).hasSize(2); + + LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); + assertThat(binding).isNotNull(); + assertThat(binding.hasName("firstname")).isTrue(); + assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); + + binding = (LikeParameterBinding) bindings.get(1); + assertThat(binding).isNotNull(); + assertThat(binding.hasName("firstname_1")).isTrue(); + assertThat(binding.getType()).isEqualTo(Type.CONTAINING); + } + @Test // DATAJPA-461 void detectsNamedInParameterBindings() { @@ -199,9 +249,8 @@ class StringQueryUnitTests { assertNamedBinding(LikeParameterBinding.class, "escapedWord", bindings.get(0)); assertNamedBinding(ParameterBinding.class, "word", bindings.get(1)); - assertThat(query.getQueryString()) - .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"); + 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"); } @Test // DATAJPA-483 @@ -275,6 +324,17 @@ class StringQueryUnitTests { assertThat(queryString).isEqualTo("select a from A a where a.b in :__$synthetic$__1 and a.c in :__$synthetic$__2"); } + @Test // DATAJPA-712 + void shouldReplaceExpressionWithLikeParameters() { + + StringQuery query = new StringQuery( + "select a from A a where a.b LIKE :#{#filter.login}% and a.c LIKE %:#{#filter.login}", true); + String queryString = query.getQueryString(); + + assertThat(queryString) + .isEqualTo("select a from A a where a.b LIKE :__$synthetic$__1 and a.c LIKE :__$synthetic$__2"); + } + @Test // DATAJPA-712 void shouldReplaceAllPositionExpressionParametersWithInClause() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index b5182b078..9b8c73494 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -164,8 +164,8 @@ public interface UserRepository extends JpaRepository, JpaSpecifi @Query("select u from User u where u.firstname like ?1%") List findByFirstnameLike(String firstname); - // DATAJPA-292 - @Query("select u from User u where u.firstname like :firstname%") + // DATAJPA-292, GH-3041 + @Query("select u from User u where u.firstname like :firstname% or u.firstname like %:firstname") List findByFirstnameLikeNamed(@Param("firstname") String firstname); /**