From a1e6bbcd986de697d0d246bac4f079a5f943865d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 13 Jul 2023 11:52:42 +0200 Subject: [PATCH] Use unique named parameter bindings for like parameters. We now replace LIKE expressions according to their type with individual bindings if an existing binding cannot be used because of how the bound value is being transformed. WHERE foo like %:name or bar like :name becomes WHERE foo like :name (i.e. '%' + :name) or bar like :name_1 (i.e. :name) See #3041 --- .../jpa/provider/PersistenceProvider.java | 7 +- .../query/ParameterBinderFactory.java | 8 +- .../query/QueryParameterSetterFactory.java | 104 +++++++++--- .../jpa/repository/query/StringQuery.java | 154 ++++++++++++------ .../jpa/repository/UserRepositoryTests.java | 7 +- .../query/LikeBindingUnitTests.java | 22 ++- .../query/StringQueryUnitTests.java | 70 +++++++- .../jpa/repository/sample/UserRepository.java | 4 +- 8 files changed, 284 insertions(+), 92 deletions(-) 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); /**