From c5950e3f42974810e0fde30237e61c0b25f7c6cc 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 | 72 +++++++- .../jpa/repository/sample/UserRepository.java | 4 +- 8 files changed, 284 insertions(+), 94 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 e7bdb6c272..ea378384eb 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 boolean canExtractQuery() { } /** - * 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 2ff2bc46dd..6cdd559f30 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 @@ static ParameterBinder createBinder(JpaParameters parameters) { 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 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Declared 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 5cc137d820..f2d35b84e5 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 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 @@ static QueryParameterSetterFactory basic(JpaParameters parameters) { 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 @@ private static QueryParameterSetter createSetter(Function 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 @@ private Object evaluateExpression(Expression expression, JpaParametersParameterA } } + /** + * 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 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla JpaParameter parameter; if (declaredQuery.hasNamedParameter()) { - parameter = findParameterForBinding(binding); + parameter = findParameterForBinding(parameters, binding.getRequiredName()); } else { int parameterIndex = binding.getRequiredPosition() - 1; @@ -228,28 +306,6 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla : 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 @@ public Class getParameterType() { @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 78cbc655cb..b3b8a35493 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 @@ -29,11 +29,14 @@ 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; @@ -234,6 +237,8 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; + LikeParameterBindings likeParameterBindings = new LikeParameterBindings(); + boolean usesJpaStyleParameters = false; while (matcher.find()) { @@ -274,9 +279,11 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St 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; @@ -335,43 +342,7 @@ private static String replaceFirst(String text, String substring, String replace 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 @@ -461,6 +432,60 @@ static ParameterBindingType of(String typeSource) { } } + /** + * 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. * @@ -512,6 +537,10 @@ boolean hasName(@Nullable String name) { 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. @@ -520,6 +549,10 @@ boolean hasPosition(@Nullable Integer position) { return position != null && this.name == null && position.equals(this.position); } + boolean hasPosition() { + return position != null && this.name == null; + } + /** * @return the name */ @@ -666,6 +699,7 @@ public Object prepare(@Nullable Object value) { * * @author Oliver Gierke * @author Thomas Darimont + * @author Mark Paluch */ static class LikeParameterBinding extends ParameterBinding { @@ -674,35 +708,45 @@ static class LikeParameterBinding extends ParameterBinding { 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; } /** @@ -733,6 +777,7 @@ static class LikeParameterBinding extends ParameterBinding { String.format("Type must be one of %s", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); this.type = type; + this.declaredName = null; } /** @@ -744,13 +789,29 @@ public Type getType() { 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 @@ -803,6 +864,7 @@ private static Type getLikeTypeFrom(String expression) { 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 c27667bf21..17e8e2638d 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 @@ -977,14 +977,13 @@ void executesManualQueryWithPositionLikeExpressionCorrectly() { 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 bcd0556bff..d94779b2c0 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 @@ void rejectsInvalidPosition() { @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 @@ 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/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 de671b3367..101574af6d 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 @@ -37,6 +37,7 @@ * @author Nils Borrmann * @author Andriy Redko * @author Diego Krupitza + * @author Mark Paluch */ class StringQueryUnitTests { @@ -68,7 +69,7 @@ void detectsPositionalLikeBindings() { 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); @@ -90,7 +91,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 CONCAT('%',:firstname)"); + assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname"); List bindings = query.getParameterBindings(); assertThat(bindings).hasSize(1); @@ -101,6 +102,55 @@ void detectsNamedLikeBindings() { 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() { @@ -208,11 +258,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() { assertNamedBinding(LikeParameterBinding.class, "escapedWord", bindings.get(0)); assertNamedBinding(ParameterBinding.class, "word", bindings.get(1)); - softly.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"); - - softly.assertAll(); + 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 @@ -295,6 +342,17 @@ void shouldReplaceAllNamedExpressionParametersWithInClause() { 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 a91605edcc..1cfff59c28 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 @@ -156,8 +156,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); /**