From a4916d55291f2907f2959ded546cecd9df76b70a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 21 Jul 2023 10:53:19 +0200 Subject: [PATCH] Revise String Query `ParameterBinding`. We now distinguish between the binding parameter target and its origin. The parameter target represents how the binding is bound to the query, the origin points to where the binding parameter comes from (method invocation argument or an expression). The revised design removes the assumption that binding parameters must match their indices/names of the method call to introduce synthetic parameters for different binding variants while using the same underlying invocation parameters. See #3041 --- .../jpa/repository/query/DeclaredQuery.java | 4 +- .../repository/query/EmptyDeclaredQuery.java | 2 +- .../query/ParameterBinderFactory.java | 16 +- .../repository/query/ParameterBinding.java | 582 ++++++++++++++++++ .../query/QueryParameterSetter.java | 2 +- .../query/QueryParameterSetterFactory.java | 172 +----- .../jpa/repository/query/StringQuery.java | 446 +------------- .../query/LikeBindingUnitTests.java | 49 +- .../QueryParameterSetterFactoryUnitTests.java | 7 +- .../query/StringQueryUnitTests.java | 52 +- 10 files changed, 705 insertions(+), 627 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java index 4821865de..670e030fb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java @@ -71,9 +71,9 @@ interface DeclaredQuery { boolean isDefaultProjection(); /** - * Returns the {@link StringQuery.ParameterBinding}s registered. + * Returns the {@link ParameterBinding}s registered. */ - List getParameterBindings(); + List getParameterBindings(); /** * Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to be diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java index dd37c4508..dbb3be8ba 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java @@ -60,7 +60,7 @@ class EmptyDeclaredQuery implements DeclaredQuery { } @Override - public List getParameterBindings() { + public List getParameterBindings() { return Collections.emptyList(); } 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 6cdd559f3..7d10ead1e 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 @@ -19,8 +19,9 @@ import java.util.ArrayList; import java.util.List; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; +import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -46,11 +47,10 @@ 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, likeFactory, setterFactory)); + return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); } /** @@ -97,11 +97,9 @@ class ParameterBinderFactory { 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, like, basicSetterFactory), + return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory), !query.usesPaging()); } @@ -113,7 +111,11 @@ class ParameterBinderFactory { for (JpaParameter parameter : parameters) { if (parameter.isBindable()) { - result.add(new ParameterBinding(++bindableParameterIndex)); + int index = ++bindableParameterIndex; + BindingIdentifier bindingIdentifier = parameter.getName().map(it -> BindingIdentifier.of(it, index)) + .orElseGet(() -> BindingIdentifier.of(index)); + + result.add(new ParameterBinding(bindingIdentifier, ParameterOrigin.ofParameter(bindingIdentifier))); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java new file mode 100644 index 000000000..bc0086fab --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -0,0 +1,582 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.springframework.util.ObjectUtils.*; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A generic parameter binding with name or position information. + * + * @author Thomas Darimont + * @author Mark Paluch + */ +class ParameterBinding { + + private final BindingIdentifier identifier; + private final ParameterOrigin origin; + + /** + * Creates a new {@link ParameterBinding} for the parameter with the given identifier and origin. + * + * @param identifier of the parameter, must not be {@literal null}. + * @param origin the origin of the parameter (expression or method argument) + */ + ParameterBinding(BindingIdentifier identifier, ParameterOrigin origin) { + + Assert.notNull(identifier, "BindingIdentifier must not be null"); + Assert.notNull(origin, "ParameterOrigin must not be null"); + + this.identifier = identifier; + this.origin = origin; + } + + public BindingIdentifier getIdentifier() { + return identifier; + } + + public ParameterOrigin getOrigin() { + return origin; + } + + /** + * @return the name if available or {@literal null}. + */ + @Nullable + public String getName() { + return identifier.hasName() ? identifier.getName() : null; + } + + /** + * @return the name + * @throws IllegalStateException if the name is not available. + * @since 2.0 + */ + String getRequiredName() throws IllegalStateException { + + String name = getName(); + + if (name != null) { + return name; + } + + throw new IllegalStateException(String.format("Required name for %s not available", this)); + } + + /** + * @return the position if available or {@literal null}. + */ + @Nullable + Integer getPosition() { + return identifier.hasPosition() ? identifier.getPosition() : null; + } + + /** + * @return the position + * @throws IllegalStateException if the position is not available. + * @since 2.0 + */ + int getRequiredPosition() throws IllegalStateException { + + Integer position = getPosition(); + + if (position != null) { + return position; + } + + throw new IllegalStateException(String.format("Required position for %s not available", this)); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + ParameterBinding that = (ParameterBinding) o; + + if (!nullSafeEquals(identifier, that.identifier)) { + return false; + } + return nullSafeEquals(origin, that.origin); + } + + @Override + public int hashCode() { + int result = nullSafeHashCode(identifier); + result = 31 * result + nullSafeHashCode(origin); + return result; + } + + @Override + public String toString() { + return String.format("ParameterBinding [identifier: %s, origin: %s]", identifier, origin); + } + + /** + * @param valueToBind value to prepare + */ + @Nullable + public Object prepare(@Nullable Object valueToBind) { + return valueToBind; + } + + /** + * Check whether the {@code other} binding uses the same bind target. + * + * @param other + * @return + */ + public boolean bindsTo(ParameterBinding other) { + + if (identifier.hasName() && other.identifier.hasName()) { + if (identifier.getName().equals(other.identifier.getName())) { + return true; + } + } + + if (identifier.hasPosition() && other.identifier.hasPosition()) { + if (identifier.getPosition() == other.identifier.getPosition()) { + return true; + } + } + + return false; + } + + /** + * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an + * {@code IN} parameter. + * + * @author Thomas Darimont + */ + static class InParameterBinding extends ParameterBinding { + + /** + * Creates a new {@link InParameterBinding} for the parameter with the given name. + */ + InParameterBinding(BindingIdentifier identifier, ParameterOrigin origin) { + super(identifier, origin); + } + + @Override + public Object prepare(@Nullable Object value) { + + if (!ObjectUtils.isArray(value)) { + return value; + } + + int length = Array.getLength(value); + Collection result = new ArrayList<>(length); + + for (int i = 0; i < length; i++) { + result.add(Array.get(value, i)); + } + + return result; + } + } + + /** + * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE + * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ + static class LikeParameterBinding extends ParameterBinding { + + private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, + Type.ENDING_WITH, Type.LIKE); + + private final Type type; + + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter + * binding input. + * + * @param identifier must not be {@literal null} or empty. + * @param type must not be {@literal null}. + */ + LikeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Type type) { + + super(identifier, origin); + + 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; + } + + /** + * Returns the {@link Type} of the binding. + * + * @return the type + */ + public Type getType() { + return type; + } + + /** + * Extracts the raw value properly. + */ + @Nullable + @Override + public Object prepare(@Nullable Object 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 + public boolean equals(Object obj) { + + if (!(obj instanceof LikeParameterBinding)) { + return false; + } + + LikeParameterBinding that = (LikeParameterBinding) obj; + + return super.equals(obj) && this.type.equals(that.type); + } + + @Override + public int hashCode() { + + int result = super.hashCode(); + + result += nullSafeHashCode(this.type); + + return result; + } + + @Override + public String toString() { + return String.format("LikeBinding [identifier: %s, origin: %s, type: %s]", getIdentifier(), getOrigin(), + getType()); + } + + /** + * Extracts the like {@link Type} from the given JPA like expression. + * + * @param expression must not be {@literal null} or empty. + */ + static Type getLikeTypeFrom(String expression) { + + Assert.hasText(expression, "Expression must not be null or empty"); + + if (expression.matches("%.*%")) { + return Type.CONTAINING; + } + + if (expression.startsWith("%")) { + return Type.ENDING_WITH; + } + + if (expression.endsWith("%")) { + return Type.STARTING_WITH; + } + + return Type.LIKE; + } + } + + static class ParameterImpl implements jakarta.persistence.Parameter { + + private final BindingIdentifier identifier; + private final Class parameterType; + + /** + * Creates a new {@link ParameterImpl} for the given {@link JpaParameter} and {@link ParameterBinding}. + * + * @param parameter can be {@literal null}. + * @param binding must not be {@literal null}. + * @return a {@link jakarta.persistence.Parameter} object based on the information from the arguments. + */ + static jakarta.persistence.Parameter of(@Nullable JpaParameter parameter, ParameterBinding binding) { + + Class type = parameter == null ? Object.class : parameter.getType(); + + return new ParameterImpl<>(binding.getIdentifier(), type); + } + + public ParameterImpl(BindingIdentifier identifier, Class parameterType) { + this.identifier = identifier; + this.parameterType = parameterType; + } + + @Nullable + @Override + public String getName() { + return identifier.hasName() ? identifier.getName() : null; + } + + @Nullable + @Override + public Integer getPosition() { + return identifier.hasPosition() ? identifier.getPosition() : null; + } + + @Override + public Class getParameterType() { + return parameterType; + } + + } + + /** + * Identifies a binding parameter by name, position or both. Used to bind parameters to a query or to describe a + * {@link MethodInvocationArgument} origin. + */ + sealed interface BindingIdentifier permits Named,Indexed,NamedAndIndexed { + + /** + * Creates an identifier for the given {@code name}. + * + * @param name + * @return + */ + static BindingIdentifier of(String name) { + + Assert.hasText(name, "Name must not be empty"); + + return new Named(name); + } + + /** + * Creates an identifier for the given {@code position}. + * + * @param position 1-based index. + * @return + */ + static BindingIdentifier of(int position) { + + Assert.isTrue(position > 0, "Index position must be greater zero"); + + return new Indexed(position); + } + + /** + * Creates an identifier for the given {@code name} and {@code position}. + * + * @param name + * @return + */ + static BindingIdentifier of(String name, int position) { + + Assert.hasText(name, "Name must not be empty"); + + return new NamedAndIndexed(name, position); + } + + /** + * @return {@code true} if the binding is associated with a name. + */ + default boolean hasName() { + return false; + } + + /** + * @return {@code true} if the binding is associated with a position index. + */ + default boolean hasPosition() { + return false; + } + + /** + * Returns the binding name {@link #hasName() if present} or throw {@link IllegalStateException} if no name + * associated. + * + * @return the binding name. + */ + default String getName() { + throw new IllegalStateException("No name associated"); + } + + /** + * Returns the binding name {@link #hasPosition() if present} or throw {@link IllegalStateException} if no position + * associated. + * + * @return the binding position. + */ + default int getPosition() { + throw new IllegalStateException("No position associated"); + } + } + + private record Named(String name) implements BindingIdentifier { + + @Override + public boolean hasName() { + return true; + } + + @Override + public String getName() { + return name(); + } + } + + private record Indexed(int position) implements BindingIdentifier { + + @Override + public boolean hasPosition() { + return true; + } + + @Override + public int getPosition() { + return position(); + } + } + + private record NamedAndIndexed(String name, int position) implements BindingIdentifier { + + @Override + public boolean hasName() { + return true; + } + + @Override + public String getName() { + return name(); + } + + @Override + public boolean hasPosition() { + return true; + } + + @Override + public int getPosition() { + return position(); + } + } + + /** + * Value type hierarchy to describe where a binding parameter comes from, either method call or an expression. + */ + sealed interface ParameterOrigin permits Expression,MethodInvocationArgument { + + /** + * Creates a {@link Expression} for the given {@code expression} string. + * + * @param expression must not be {@literal null}. + * @return {@link Expression} for the given {@code expression} string. + */ + static Expression ofExpression(String expression) { + return new Expression(expression); + } + + /** + * Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the + * position must be given, + * + * @param name the parameter name from the method invocation, can be {@literal null}. + * @param position the parameter position (1-based) from the method invocation, can be {@literal null}. + * @return {@link MethodInvocationArgument} object for {@code name} and {@code position} + */ + static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Integer position) { + + BindingIdentifier identifier; + if (!ObjectUtils.isEmpty(name) && position != null) { + identifier = BindingIdentifier.of(name, position); + } else if (!ObjectUtils.isEmpty(name)) { + identifier = BindingIdentifier.of(name); + } else { + identifier = BindingIdentifier.of(position); + } + + return ofParameter(identifier); + } + + /** + * Creates a {@link MethodInvocationArgument} using {@link BindingIdentifier}. + * + * @param identifier must not be {@literal null}. + * @return {@link MethodInvocationArgument} for {@link BindingIdentifier}. + */ + static MethodInvocationArgument ofParameter(BindingIdentifier identifier) { + + return new MethodInvocationArgument(identifier); + } + + boolean isMethodArgument(); + + boolean isExpression(); + } + + /** + * Value object capturing the expression of which a binding parameter originates. + * + * @param expression + */ + public record Expression(String expression) implements ParameterOrigin { + + @Override + public boolean isMethodArgument() { + return false; + } + + @Override + public boolean isExpression() { + return true; + } + } + + /** + * Value object capturing the method invocation parameter reference. + * + * @param identifier + */ + public record MethodInvocationArgument(BindingIdentifier identifier) implements ParameterOrigin { + + @Override + public boolean isMethodArgument() { + return true; + } + + @Override + public boolean isExpression() { + return false; + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java index a1991f1a3..261deb4b2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java @@ -109,7 +109,7 @@ interface QueryParameterSetter { } else { - final Object value = valueExtractor.apply(accessor); + Object value = valueExtractor.apply(accessor); if (parameter instanceof ParameterExpression) { errorHandling.execute(() -> query.setParameter((Parameter) parameter, value)); 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 f2d35b84e..0ea048f32 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 @@ -22,10 +22,11 @@ import java.util.List; import java.util.function.Function; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; +import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument; +import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterImpl; 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; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -63,20 +64,6 @@ 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}. @@ -133,7 +120,7 @@ abstract class QueryParameterSetterFactory { } @Nullable - private static JpaParameter findParameterForBinding(Parameters parameters, String name) { + static JpaParameter findParameterForBinding(Parameters parameters, String name) { JpaParameters bindableParameters = parameters.getBindableParameters(); @@ -150,9 +137,20 @@ abstract class QueryParameterSetterFactory { return p.getName().orElseThrow(() -> new IllegalStateException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED)); } - @Nullable - static Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { - return accessor.getValue(parameter); + static JpaParameter findParameterForBinding(Parameters parameters, int parameterIndex) { + + JpaParameters bindableParameters = parameters.getBindableParameters(); + + Assert.isTrue( // + parameterIndex < bindableParameters.getNumberOfParameters(), // + () -> String.format( // + "At least %s parameter(s) provided but only %s parameter(s) present in query", // + parameterIndex + 1, // + bindableParameters.getNumberOfParameters() // + ) // + ); + + return bindableParameters.getParameter(parameterIndex); } /** @@ -189,11 +187,11 @@ abstract class QueryParameterSetterFactory { @Override public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { - if (!binding.isExpression()) { + if (!(binding.getOrigin()instanceof ParameterBinding.Expression e)) { return null; } - Expression expression = parser.parseExpression(binding.getExpression()); + Expression expression = parser.parseExpression(e.expression()); return createSetter(values -> evaluateExpression(expression, values), binding, null); } @@ -214,51 +212,12 @@ 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. * * @author Jens Schauder * @author Oliver Gierke + * @author Mark Paluch * @since 2.0 */ private static class BasicQueryParameterSetterFactory extends QueryParameterSetterFactory { @@ -281,24 +240,16 @@ abstract class QueryParameterSetterFactory { Assert.notNull(binding, "Binding must not be null"); JpaParameter parameter; + if (!(binding.getOrigin()instanceof MethodInvocationArgument mia)) { + return QueryParameterSetter.NOOP; + } + + BindingIdentifier identifier = mia.identifier(); if (declaredQuery.hasNamedParameter()) { - parameter = findParameterForBinding(parameters, binding.getRequiredName()); + parameter = findParameterForBinding(parameters, identifier.getName()); } else { - - int parameterIndex = binding.getRequiredPosition() - 1; - JpaParameters bindableParameters = parameters.getBindableParameters(); - - Assert.isTrue( // - parameterIndex < bindableParameters.getNumberOfParameters(), // - () -> String.format( // - "At least %s parameter(s) provided but only %s parameter(s) present in query", // - binding.getRequiredPosition(), // - bindableParameters.getNumberOfParameters() // - ) // - ); - - parameter = bindableParameters.getParameter(binding.getRequiredPosition() - 1); + parameter = findParameterForBinding(parameters, identifier.getPosition() - 1); } return parameter == null // @@ -306,6 +257,10 @@ abstract class QueryParameterSetterFactory { : createSetter(values -> getValue(values, parameter), binding, parameter); } + @Nullable + private Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { + return accessor.getValue(parameter); + } } /** @@ -368,67 +323,4 @@ abstract class QueryParameterSetterFactory { } } - private static class ParameterImpl implements jakarta.persistence.Parameter { - - private final Class parameterType; - private final @Nullable String name; - private final @Nullable Integer position; - - /** - * Creates a new {@link ParameterImpl} for the given {@link JpaParameter} and {@link ParameterBinding}. - * - * @param parameter can be {@literal null}. - * @param binding must not be {@literal null}. - * @return a {@link jakarta.persistence.Parameter} object based on the information from the arguments. - */ - static jakarta.persistence.Parameter of(@Nullable JpaParameter parameter, ParameterBinding binding) { - - Class type = parameter == null ? Object.class : parameter.getType(); - - return new ParameterImpl<>(type, getName(parameter, binding), binding.getPosition()); - } - - /** - * Creates a new {@link ParameterImpl} for the given name, position and parameter type. - * - * @param parameterType must not be {@literal null}. - * @param name can be {@literal null}. - * @param position can be {@literal null}. - */ - private ParameterImpl(Class parameterType, @Nullable String name, @Nullable Integer position) { - - this.name = name; - this.position = position; - this.parameterType = parameterType; - } - - @Nullable - @Override - public String getName() { - return name; - } - - @Nullable - @Override - public Integer getPosition() { - return position; - } - - @Override - public Class getParameterType() { - return parameterType; - } - - @Nullable - private static String getName(@Nullable JpaParameter parameter, ParameterBinding binding) { - - if (binding.hasName() || parameter == null) { - return binding.getName(); - } - - return parameter.isNamedParameter() // - ? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name")) // - : null; - } - } } 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 b3b8a3549..8d82fac55 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 @@ -15,21 +15,16 @@ */ 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 java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.function.BiFunction; 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.jpa.repository.query.ParameterBinding.BindingIdentifier; +import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; import org.springframework.data.repository.query.SpelQueryContext; import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; import org.springframework.data.repository.query.parser.Part.Type; @@ -40,6 +35,8 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import static java.util.regex.Pattern.*; + /** * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations @@ -143,7 +140,7 @@ class StringQuery implements DeclaredQuery { @Override public boolean hasNamedParameter() { - return bindings.stream().anyMatch(b -> b.getName() != null); + return bindings.stream().anyMatch(b -> b.getIdentifier().hasName()); } @Override @@ -269,18 +266,28 @@ class StringQuery implements DeclaredQuery { throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); } + BindingIdentifier identifier; + if (parameterIndex != null) { + identifier = BindingIdentifier.of(parameterIndex); + } else { + identifier = BindingIdentifier.of(parameterName); + } + ParameterOrigin origin = ObjectUtils.isEmpty(expression) + ? ParameterOrigin.ofParameter(parameterName, parameterIndex) + : ParameterOrigin.ofExpression(expression); + switch (ParameterBindingType.of(typeSource)) { case LIKE: - Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); replacement = matcher.group(3); if (parameterIndex != null) { - checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings); + checkAndRegister(new LikeParameterBinding(identifier, origin, likeType), bindings); } else { - LikeParameterBinding binding = likeParameterBindings.getOrCreate(parameterName, likeType, expression); + LikeParameterBinding binding = likeParameterBindings.getOrCreate(parameterName, likeType, origin); checkAndRegister(binding, bindings); replacement = ":" + binding.getRequiredName(); @@ -290,20 +297,14 @@ class StringQuery implements DeclaredQuery { case IN: - if (parameterIndex != null) { - checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings); - } else { - checkAndRegister(new InParameterBinding(parameterName, expression), bindings); - } + checkAndRegister(new InParameterBinding(identifier, origin), bindings); break; case AS_IS: // fall-through we don't need a special parameter binding for the given parameter. default: - bindings.add(parameterIndex != null // - ? new ParameterBinding(null, parameterIndex, expression) // - : new ParameterBinding(parameterName, null, expression)); + bindings.add(new ParameterBinding(identifier, origin)); } if (replacement != null) { @@ -374,7 +375,7 @@ class StringQuery implements DeclaredQuery { private static void checkAndRegister(ParameterBinding binding, List bindings) { bindings.stream() // - .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) // + .filter(it -> it.bindsTo(binding)) // .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); if (!bindings.contains(binding)) { @@ -432,6 +433,10 @@ class StringQuery implements DeclaredQuery { } } + private static class Metadata { + private boolean usesJdbcStyleParameters = false; + } + /** * Utility to create unique parameter bindings for LIKE that can be evaluated by * {@code LikeRewritingQueryParameterSetterFactory}. @@ -449,27 +454,26 @@ class StringQuery implements DeclaredQuery { * * @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. + * @param origin origin of the parameter. * @return the Like binding. Can return an already existing binding. */ - LikeParameterBinding getOrCreate(String parameterName, Type likeType, @Nullable String expression) { + LikeParameterBinding getOrCreate(String parameterName, Type likeType, ParameterOrigin origin) { 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) { + if (origin.isMethodArgument()) { for (LikeParameterBinding likeParameterBinding : likeParameterBindings) { - if (likeParameterBinding.type == likeType) { + if (likeParameterBinding.getType() == likeType) { reuse = likeParameterBinding; break; } } } - String declaredParameterName = parameterName; if (reuse != null) { return reuse; } @@ -478,396 +482,10 @@ class StringQuery implements DeclaredQuery { parameterName = parameterName + "_" + likeParameterBindings.size(); } - LikeParameterBinding binding = new LikeParameterBinding(parameterName, declaredParameterName, likeType, - expression); + LikeParameterBinding binding = new LikeParameterBinding(BindingIdentifier.of(parameterName), origin, likeType); likeParameterBindings.add(binding); return binding; } } - - /** - * A generic parameter binding with name or position information. - * - * @author Thomas Darimont - */ - static class ParameterBinding { - - private final @Nullable String name; - private final @Nullable String expression; - private final @Nullable Integer position; - - /** - * Creates a new {@link ParameterBinding} for the parameter with the given position. - * - * @param position must not be {@literal null}. - */ - ParameterBinding(Integer position) { - this(null, position, null); - } - - /** - * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression - * information. Either {@literal name} or {@literal position} must be not {@literal null}. - * - * @param name of the parameter may be {@literal null}. - * @param position of the parameter may be {@literal null}. - * @param expression the expression to apply to any value for this parameter. - */ - ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) { - - if (name == null) { - Assert.notNull(position, "Position must not be null"); - } - - if (position == null) { - Assert.notNull(name, "Name must not be null"); - } - - this.name = name; - this.position = position; - this.expression = expression; - } - - /** - * Returns whether the binding has the given name. Will always be {@literal false} in case the - * {@link ParameterBinding} has been set up from a position. - */ - 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. - */ - 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 - */ - @Nullable - public String getName() { - return name; - } - - /** - * @return the name - * @throws IllegalStateException if the name is not available. - * @since 2.0 - */ - String getRequiredName() throws IllegalStateException { - - String name = getName(); - - if (name != null) { - return name; - } - - throw new IllegalStateException(String.format("Required name for %s not available", this)); - } - - /** - * @return the position - */ - @Nullable - Integer getPosition() { - return position; - } - - /** - * @return the position - * @throws IllegalStateException if the position is not available. - * @since 2.0 - */ - int getRequiredPosition() throws IllegalStateException { - - Integer position = getPosition(); - - if (position != null) { - return position; - } - - throw new IllegalStateException(String.format("Required position for %s not available", this)); - } - - /** - * @return {@literal true} if this parameter binding is a synthetic SpEL expression. - */ - public boolean isExpression() { - return this.expression != null; - } - - @Override - public int hashCode() { - - int result = 17; - - result += nullSafeHashCode(this.name); - result += nullSafeHashCode(this.position); - result += nullSafeHashCode(this.expression); - - return result; - } - - @Override - public boolean equals(Object obj) { - - if (!(obj instanceof ParameterBinding)) { - return false; - } - - ParameterBinding that = (ParameterBinding) obj; - - return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position) - && nullSafeEquals(this.expression, that.expression); - } - - @Override - public String toString() { - return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(), - getExpression()); - } - - /** - * @param valueToBind value to prepare - */ - @Nullable - public Object prepare(@Nullable Object valueToBind) { - return valueToBind; - } - - @Nullable - public String getExpression() { - return expression; - } - } - - /** - * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an - * {@code IN} parameter. - * - * @author Thomas Darimont - */ - static class InParameterBinding extends ParameterBinding { - - /** - * Creates a new {@link InParameterBinding} for the parameter with the given name. - */ - InParameterBinding(String name, @Nullable String expression) { - super(name, null, expression); - } - - /** - * Creates a new {@link InParameterBinding} for the parameter with the given position. - */ - InParameterBinding(int position, @Nullable String expression) { - super(null, position, expression); - } - - @Override - public Object prepare(@Nullable Object value) { - - if (!ObjectUtils.isArray(value)) { - return value; - } - - int length = Array.getLength(value); - Collection result = new ArrayList<>(length); - - for (int i = 0; i < length; i++) { - result.add(Array.get(value, i)); - } - - return result; - } - } - - /** - * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE - * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA. - * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Mark Paluch - */ - static class LikeParameterBinding extends ParameterBinding { - - private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, - Type.ENDING_WITH, Type.LIKE); - - 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 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, 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 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, 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; - } - - /** - * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}. - * - * @param position position of the parameter in the query. - * @param type must not be {@literal null}. - */ - LikeParameterBinding(int position, Type type) { - this(position, type, null); - } - - /** - * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}. - * - * @param position position of the parameter in the query. - * @param type must not be {@literal null}. - * @param expression may be {@literal null}. - */ - LikeParameterBinding(int position, Type type, @Nullable String expression) { - - super(null, position, expression); - - Assert.isTrue(position > 0, "Position must be greater than zero"); - 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 = null; - } - - /** - * Returns the {@link Type} of the binding. - * - * @return the type - */ - public Type getType() { - return type; - } - - @Nullable - public String getDeclaredName() { - return declaredName; - } - - /** - * Prepares the given raw keyword according to the like type. - */ - @Nullable - @Override - public Object prepare(@Nullable Object 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 - public boolean equals(Object obj) { - - if (!(obj instanceof LikeParameterBinding)) { - return false; - } - - LikeParameterBinding that = (LikeParameterBinding) obj; - - return super.equals(obj) && this.type.equals(that.type); - } - - @Override - public int hashCode() { - - int result = super.hashCode(); - - result += nullSafeHashCode(this.type); - - return result; - } - - @Override - public String toString() { - return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type); - } - - /** - * Extracts the like {@link Type} from the given JPA like expression. - * - * @param expression must not be {@literal null} or empty. - */ - private static Type getLikeTypeFrom(String expression) { - - Assert.hasText(expression, "Expression must not be null or empty"); - - if (expression.matches("%.*%")) { - return Type.CONTAINING; - } - - if (expression.startsWith("%")) { - return Type.ENDING_WITH; - } - - if (expression.endsWith("%")) { - return Type.STARTING_WITH; - } - - return Type.LIKE; - } - - } - - static class Metadata { - private boolean usesJdbcStyleParameters = false; - } } 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 d94779b2c..f07f9d7e8 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 @@ -18,7 +18,9 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; -import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; +import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; import org.springframework.data.repository.query.parser.Part.Type; /** @@ -32,57 +34,39 @@ class LikeBindingUnitTests { private static void assertAugmentedValue(Type type, Object value) { - LikeParameterBinding binding = new LikeParameterBinding("foo", "foo", type); + LikeParameterBinding binding = new LikeParameterBinding(BindingIdentifier.of("foo"), + ParameterOrigin.ofExpression("foo"), type); assertThat(binding.prepare("value")).isEqualTo(value); } @Test void rejectsNullName() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding(null, "", Type.CONTAINING)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new LikeParameterBinding(null, ParameterOrigin.ofExpression(""), Type.CONTAINING)); } @Test void rejectsEmptyName() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("", "", Type.CONTAINING)); + assertThatIllegalArgumentException().isThrownBy( + () -> new LikeParameterBinding(BindingIdentifier.of(""), ParameterOrigin.ofExpression(""), Type.CONTAINING)); } @Test void rejectsNullType() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("foo", "foo", null)); + assertThatIllegalArgumentException().isThrownBy( + () -> new LikeParameterBinding(BindingIdentifier.of("foo"), ParameterOrigin.ofExpression("foo"), null)); } @Test void rejectsInvalidType() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding("foo", "foo", Type.SIMPLE_PROPERTY)); + assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding(BindingIdentifier.of("foo"), + ParameterOrigin.ofExpression("foo"), Type.SIMPLE_PROPERTY)); } @Test void rejectsInvalidPosition() { - assertThatIllegalArgumentException().isThrownBy(() -> new LikeParameterBinding(0, Type.CONTAINING)); - } - - @Test - void setsUpInstanceForName() { - - LikeParameterBinding binding = new LikeParameterBinding("foo", "foo", Type.CONTAINING); - - assertThat(binding.hasName("foo")).isTrue(); - assertThat(binding.hasName("bar")).isFalse(); - assertThat(binding.hasName(null)).isFalse(); - assertThat(binding.hasPosition(0)).isFalse(); - assertThat(binding.getType()).isEqualTo(Type.CONTAINING); - } - - @Test - void setsUpInstanceForIndex() { - - LikeParameterBinding binding = new LikeParameterBinding(1, Type.CONTAINING); - - assertThat(binding.hasName("foo")).isFalse(); - assertThat(binding.hasName(null)).isFalse(); - assertThat(binding.hasPosition(0)).isFalse(); - assertThat(binding.hasPosition(1)).isTrue(); - assertThat(binding.getType()).isEqualTo(Type.CONTAINING); + assertThatIllegalArgumentException().isThrownBy( + () -> new LikeParameterBinding(BindingIdentifier.of(0), ParameterOrigin.ofExpression(""), Type.CONTAINING)); } @Test @@ -92,6 +76,7 @@ class LikeBindingUnitTests { assertAugmentedValue(Type.ENDING_WITH, "%value"); assertAugmentedValue(Type.STARTING_WITH, "value%"); - assertThat(new LikeParameterBinding(1, Type.CONTAINING).prepare(null)).isNull(); + assertThat(new LikeParameterBinding(BindingIdentifier.of(1), ParameterOrigin.ofParameter(null, 1), Type.CONTAINING) + .prepare(null)).isNull(); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java index b10df30d3..8826ea60d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java @@ -25,9 +25,8 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; - import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; /** * Unit tests for {@link QueryParameterSetterFactory}. @@ -60,6 +59,8 @@ class QueryParameterSetterFactoryUnitTests { @Test // DATAJPA-1058 void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { + when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter("NamedParameter", 1)); + assertThatExceptionOfType(IllegalStateException.class) // .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter", false))) // .withMessageContaining("Java 8") // @@ -76,6 +77,7 @@ class QueryParameterSetterFactoryUnitTests { // one argument present in the method signature when(binding.getRequiredPosition()).thenReturn(1); + when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter", false))) // @@ -90,6 +92,7 @@ class QueryParameterSetterFactoryUnitTests { // one argument present in the method signature when(binding.getRequiredPosition()).thenReturn(1); + when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith ?1", false))) // 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 101574af6..dd523fc74 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 @@ -23,9 +23,9 @@ import java.util.List; import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; -import org.springframework.data.jpa.repository.query.StringQuery.InParameterBinding; -import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.Expression; +import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; import org.springframework.data.repository.query.parser.Part.Type; /** @@ -58,7 +58,7 @@ class StringQueryUnitTests { LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); assertThat(binding.getType()).isEqualTo(Type.LIKE); - assertThat(binding.hasName("firstname")).isTrue(); + assertThat(binding.getName()).isEqualTo("firstname"); } @Test // DATAJPA-292 @@ -76,12 +76,12 @@ class StringQueryUnitTests { LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); assertThat(binding).isNotNull(); - assertThat(binding.hasPosition(1)).isTrue(); + assertThat(binding.getRequiredPosition()).isEqualTo(1); assertThat(binding.getType()).isEqualTo(Type.CONTAINING); binding = (LikeParameterBinding) bindings.get(1); assertThat(binding).isNotNull(); - assertThat(binding.hasPosition(2)).isTrue(); + assertThat(binding.getRequiredPosition()).isEqualTo(2); assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); } @@ -98,7 +98,7 @@ class StringQueryUnitTests { LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); assertThat(binding).isNotNull(); - assertThat(binding.hasName("firstname")).isTrue(); + assertThat(binding.getRequiredName()).isEqualTo("firstname"); assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); } @@ -117,12 +117,12 @@ class StringQueryUnitTests { LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); assertThat(binding).isNotNull(); - assertThat(binding.hasName("firstname")).isTrue(); + assertThat(binding.getName()).isEqualTo("firstname"); assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); binding = (LikeParameterBinding) bindings.get(1); assertThat(binding).isNotNull(); - assertThat(binding.hasName("firstname_1")).isTrue(); + assertThat(binding.getName()).isEqualTo("firstname_1"); assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH); } @@ -142,12 +142,12 @@ class StringQueryUnitTests { LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0); assertThat(binding).isNotNull(); - assertThat(binding.hasName("firstname")).isTrue(); + assertThat(binding.getName()).isEqualTo("firstname"); assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH); binding = (LikeParameterBinding) bindings.get(1); assertThat(binding).isNotNull(); - assertThat(binding.hasName("firstname_1")).isTrue(); + assertThat(binding.getName()).isEqualTo("firstname_1"); assertThat(binding.getType()).isEqualTo(Type.CONTAINING); } @@ -359,11 +359,9 @@ class StringQueryUnitTests { StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs}", true); String queryString = query.getQueryString(); - softly.assertThat(queryString).isEqualTo("select a from A a where a.b in ?1 and a.c in ?2"); - softly.assertThat(query.getParameterBindings().get(0).getExpression()).isEqualTo("#bs"); - softly.assertThat(query.getParameterBindings().get(1).getExpression()).isEqualTo("#cs"); - - softly.assertAll(); + assertThat(queryString).isEqualTo("select a from A a where a.b in ?1 and a.c in ?2"); + assertThat(((Expression) query.getParameterBindings().get(0).getOrigin()).expression()).isEqualTo("#bs"); + assertThat(((Expression) query.getParameterBindings().get(1).getOrigin()).expression()).isEqualTo("#cs"); } @Test // DATAJPA-864 @@ -398,15 +396,13 @@ class StringQueryUnitTests { StringQuery query = new StringQuery("select a from A a where a.first = :#{#exp} or a.second = :#{#exp}", true); List bindings = query.getParameterBindings(); - softly.assertThat(bindings).isNotEmpty(); + assertThat(bindings).isNotEmpty(); for (ParameterBinding binding : bindings) { - softly.assertThat(binding.getName()).isNotNull(); - softly.assertThat(query.getQueryString()).contains(binding.getName()); - softly.assertThat(binding.getExpression()).isEqualTo("#exp"); + assertThat(binding.getName()).isNotNull(); + assertThat(query.getQueryString()).contains(binding.getName()); + assertThat(((Expression) binding.getOrigin()).expression()).isEqualTo("#exp"); } - - softly.assertAll(); } @Test // DATAJPA-1235 @@ -646,16 +642,16 @@ class StringQueryUnitTests { private void assertPositionalBinding(Class bindingType, Integer position, ParameterBinding expectedBinding) { - softly.assertThat(bindingType.isInstance(expectedBinding)).isTrue(); - softly.assertThat(expectedBinding).isNotNull(); - softly.assertThat(expectedBinding.hasPosition(position)).isTrue(); + assertThat(bindingType.isInstance(expectedBinding)).isTrue(); + assertThat(expectedBinding).isNotNull(); + assertThat(expectedBinding.getPosition()).isEqualTo(position); } private void assertNamedBinding(Class bindingType, String parameterName, ParameterBinding expectedBinding) { - softly.assertThat(bindingType.isInstance(expectedBinding)).isTrue(); - softly.assertThat(expectedBinding).isNotNull(); - softly.assertThat(expectedBinding.hasName(parameterName)).isTrue(); + assertThat(bindingType.isInstance(expectedBinding)).isTrue(); + assertThat(expectedBinding).isNotNull(); + assertThat(expectedBinding.getName()).isEqualTo(parameterName); } }