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
This commit is contained in:
Mark Paluch
2023-07-21 10:53:19 +02:00
committed by Greg L. Turnquist
parent c5950e3f42
commit a4916d5529
10 changed files with 705 additions and 627 deletions

View File

@@ -71,9 +71,9 @@ interface DeclaredQuery {
boolean isDefaultProjection();
/**
* Returns the {@link StringQuery.ParameterBinding}s registered.
* Returns the {@link ParameterBinding}s registered.
*/
List<StringQuery.ParameterBinding> getParameterBindings();
List<ParameterBinding> getParameterBindings();
/**
* Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to be

View File

@@ -60,7 +60,7 @@ class EmptyDeclaredQuery implements DeclaredQuery {
}
@Override
public List<StringQuery.ParameterBinding> getParameterBindings() {
public List<ParameterBinding> getParameterBindings() {
return Collections.emptyList();
}

View File

@@ -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<ParameterBinding> 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)));
}
}

View File

@@ -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<Object> 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<Type> 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<T> implements jakarta.persistence.Parameter<T> {
private final BindingIdentifier identifier;
private final Class<T> 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<T> 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<T> 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;
}
}
}

View File

@@ -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<Object>) parameter, value));

View File

@@ -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<JpaParameters, JpaParameter> parameters, String name) {
static JpaParameter findParameterForBinding(Parameters<JpaParameters, JpaParameter> 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<JpaParameters, JpaParameter> 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<T> implements jakarta.persistence.Parameter<T> {
private final Class<T> 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<T> 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<T> 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;
}
}
}

View File

@@ -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<ParameterBinding> 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<LikeParameterBinding> 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<Object> 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<Type> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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))) //

View File

@@ -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<ParameterBinding> 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<? extends ParameterBinding> 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<? extends ParameterBinding> 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);
}
}