Ensure correct bindings for parameter reuse.
We now verify all bindings to ensure that a like-parameter doesn't mix up with plain bindings (e.g. firstname = %:myparam or lastname = :myparam). We also create unique synthetic parameters for positional bindings. Also, fix Regex to properly detect named, anonymous and positional bind markers. See #3041
This commit is contained in:
committed by
Greg L. Turnquist
parent
55813a867a
commit
2cd3aca078
@@ -24,7 +24,6 @@ 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;
|
||||
@@ -151,8 +150,8 @@ class ParameterBinding {
|
||||
/**
|
||||
* Check whether the {@code other} binding uses the same bind target.
|
||||
*
|
||||
* @param other
|
||||
* @return
|
||||
* @param other must not be {@literal null}.
|
||||
* @return {@code true} if the other binding uses the same parameter to bind to as this one.
|
||||
*/
|
||||
public boolean bindsTo(ParameterBinding other) {
|
||||
|
||||
@@ -171,6 +170,17 @@ class ParameterBinding {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this binding can be bound as the {@code other} binding by checking its type and origin. Subclasses
|
||||
* may override this method to include other properties for the compatibility check.
|
||||
*
|
||||
* @param other
|
||||
* @return {@code true} if the other binding is compatible with this one.
|
||||
*/
|
||||
public boolean isCompatibleWith(ParameterBinding other) {
|
||||
return other.getClass() == getClass() && other.getOrigin().equals(getOrigin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
|
||||
* {@code IN} parameter.
|
||||
@@ -294,6 +304,16 @@ class ParameterBinding {
|
||||
getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompatibleWith(ParameterBinding binding) {
|
||||
|
||||
if (super.isCompatibleWith(binding) && binding instanceof LikeParameterBinding other) {
|
||||
return getType() == other.getType();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the like {@link Type} from the given JPA like expression.
|
||||
*
|
||||
@@ -319,52 +339,12 @@ class ParameterBinding {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
sealed interface BindingIdentifier permits Named,Indexed,NamedAndIndexed {
|
||||
|
||||
@@ -453,6 +433,11 @@ class ParameterBinding {
|
||||
public String getName() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name();
|
||||
}
|
||||
}
|
||||
|
||||
private record Indexed(int position) implements BindingIdentifier {
|
||||
@@ -466,6 +451,11 @@ class ParameterBinding {
|
||||
public int getPosition() {
|
||||
return position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + position() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private record NamedAndIndexed(String name, int position) implements BindingIdentifier {
|
||||
@@ -489,10 +479,18 @@ class ParameterBinding {
|
||||
public int getPosition() {
|
||||
return position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + name() + ", " + position() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value type hierarchy to describe where a binding parameter comes from, either method call or an expression.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
sealed interface ParameterOrigin permits Expression,MethodInvocationArgument {
|
||||
|
||||
@@ -508,11 +506,11 @@ class ParameterBinding {
|
||||
|
||||
/**
|
||||
* Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the
|
||||
* position must be given,
|
||||
* 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}
|
||||
* @return {@link MethodInvocationArgument} object for {@code name} and {@code position}.
|
||||
*/
|
||||
static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Integer position) {
|
||||
|
||||
@@ -528,6 +526,16 @@ class ParameterBinding {
|
||||
return ofParameter(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MethodInvocationArgument} object for {@code position}.
|
||||
*
|
||||
* @param position the parameter position (1-based) from the method invocation.
|
||||
* @return {@link MethodInvocationArgument} object for {@code position}.
|
||||
*/
|
||||
static MethodInvocationArgument ofParameter(int position) {
|
||||
return ofParameter(BindingIdentifier.of(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MethodInvocationArgument} using {@link BindingIdentifier}.
|
||||
*
|
||||
@@ -535,12 +543,17 @@ class ParameterBinding {
|
||||
* @return {@link MethodInvocationArgument} for {@link BindingIdentifier}.
|
||||
*/
|
||||
static MethodInvocationArgument ofParameter(BindingIdentifier identifier) {
|
||||
|
||||
return new MethodInvocationArgument(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the origin is a method argument reference.
|
||||
*/
|
||||
boolean isMethodArgument();
|
||||
|
||||
/**
|
||||
* @return {@code true} if the origin is an expression.
|
||||
*/
|
||||
boolean isExpression();
|
||||
}
|
||||
|
||||
@@ -548,6 +561,8 @@ class ParameterBinding {
|
||||
* Value object capturing the expression of which a binding parameter originates.
|
||||
*
|
||||
* @param expression
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
public record Expression(String expression) implements ParameterOrigin {
|
||||
|
||||
@@ -566,6 +581,8 @@ class ParameterBinding {
|
||||
* Value object capturing the method invocation parameter reference.
|
||||
*
|
||||
* @param identifier
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
public record MethodInvocationArgument(BindingIdentifier identifier) implements ParameterOrigin {
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ 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.repository.query.Parameter;
|
||||
@@ -323,4 +322,47 @@ abstract class QueryParameterSetterFactory {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,14 +18,18 @@ package org.springframework.data.jpa.repository.query;
|
||||
import static java.util.regex.Pattern.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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.MethodInvocationArgument;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
|
||||
import org.springframework.data.repository.query.SpelQueryContext;
|
||||
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
|
||||
@@ -41,7 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
* 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
|
||||
* in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
|
||||
* with synthetic bind parameters
|
||||
* with synthetic bind parameters.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
@@ -169,9 +173,9 @@ class StringQuery implements DeclaredQuery {
|
||||
// .............................................................^ start with a question mark.
|
||||
private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
|
||||
private static final Pattern PARAMETER_BINDING_PATTERN;
|
||||
private static final Pattern JDBC_STYLE_PARAM = Pattern.compile(" \\?(?!\\d)"); // <space>?[no digit]
|
||||
private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile(" \\?(?=\\d)"); // <space>?[digit]
|
||||
private static final Pattern NAMED_STYLE_PARAM = Pattern.compile(" :\\w+"); // <space>:[text]
|
||||
private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit]
|
||||
private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit]
|
||||
private static final Pattern NAMED_STYLE_PARAM = Pattern.compile("(?!\\\\):\\w+"); // no \ and :[text]
|
||||
|
||||
private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type; "
|
||||
+ "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding";
|
||||
@@ -233,10 +237,21 @@ class StringQuery implements DeclaredQuery {
|
||||
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
|
||||
|
||||
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
|
||||
int syntheticParameterIndex = expressionParameterIndex + spelExtractor.size();
|
||||
|
||||
LikeParameterBindings likeParameterBindings = new LikeParameterBindings();
|
||||
ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings),
|
||||
syntheticParameterIndex);
|
||||
int currentIndex = 0;
|
||||
|
||||
boolean usesJpaStyleParameters = false;
|
||||
if (JDBC_STYLE_PARAM.matcher(resultingQuery).find()) {
|
||||
queryMeta.usesJdbcStyleParameters = true;
|
||||
}
|
||||
|
||||
if (NUMBERED_STYLE_PARAM.matcher(resultingQuery).find() || NAMED_STYLE_PARAM.matcher(resultingQuery).find()) {
|
||||
usesJpaStyleParameters = true;
|
||||
}
|
||||
|
||||
while (matcher.find()) {
|
||||
|
||||
if (spelExtractor.isQuoted(matcher.start())) {
|
||||
@@ -253,10 +268,6 @@ class StringQuery implements DeclaredQuery {
|
||||
String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName);
|
||||
String replacement = null;
|
||||
|
||||
queryMeta.usesJdbcStyleParameters = JDBC_STYLE_PARAM.matcher(resultingQuery).find();
|
||||
usesJpaStyleParameters = NUMBERED_STYLE_PARAM.matcher(resultingQuery).find()
|
||||
|| NAMED_STYLE_PARAM.matcher(resultingQuery).find();
|
||||
|
||||
expressionParameterIndex++;
|
||||
if ("".equals(parameterIndexString)) {
|
||||
parameterIndex = expressionParameterIndex;
|
||||
@@ -266,51 +277,65 @@ class StringQuery implements DeclaredQuery {
|
||||
throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported");
|
||||
}
|
||||
|
||||
BindingIdentifier identifier;
|
||||
BindingIdentifier queryParameter;
|
||||
if (parameterIndex != null) {
|
||||
identifier = BindingIdentifier.of(parameterIndex);
|
||||
queryParameter = BindingIdentifier.of(parameterIndex);
|
||||
} else {
|
||||
identifier = BindingIdentifier.of(parameterName);
|
||||
queryParameter = BindingIdentifier.of(parameterName);
|
||||
}
|
||||
ParameterOrigin origin = ObjectUtils.isEmpty(expression)
|
||||
? ParameterOrigin.ofParameter(parameterName, parameterIndex)
|
||||
: ParameterOrigin.ofExpression(expression);
|
||||
|
||||
BindingIdentifier targetBinding = queryParameter;
|
||||
switch (ParameterBindingType.of(typeSource)) {
|
||||
|
||||
case LIKE:
|
||||
|
||||
Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
|
||||
replacement = matcher.group(3);
|
||||
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
|
||||
|
||||
if (parameterIndex != null) {
|
||||
checkAndRegister(new LikeParameterBinding(identifier, origin, likeType), bindings);
|
||||
if (origin.isExpression()) {
|
||||
parameterBindings.register(new LikeParameterBinding(queryParameter, origin, likeType));
|
||||
} else {
|
||||
|
||||
LikeParameterBinding binding = likeParameterBindings.getOrCreate(parameterName, likeType, origin);
|
||||
checkAndRegister(binding, bindings);
|
||||
|
||||
replacement = ":" + binding.getRequiredName();
|
||||
targetBinding = parameterBindings.register(queryParameter, origin,
|
||||
(identifier) -> new LikeParameterBinding(identifier, origin, likeType));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case IN:
|
||||
|
||||
checkAndRegister(new InParameterBinding(identifier, origin), bindings);
|
||||
parameterBindings.register(new InParameterBinding(queryParameter, origin));
|
||||
|
||||
break;
|
||||
|
||||
case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
|
||||
case AS_IS: // fall-through we don't need a special parameter queryParameter for the given parameter.
|
||||
default:
|
||||
|
||||
bindings.add(new ParameterBinding(identifier, origin));
|
||||
if (origin.isExpression()) {
|
||||
parameterBindings.register(new ParameterBinding(queryParameter, origin));
|
||||
} else {
|
||||
targetBinding = parameterBindings.register(queryParameter, origin,
|
||||
(identifier) -> new ParameterBinding(identifier, origin));
|
||||
}
|
||||
}
|
||||
|
||||
if (replacement != null) {
|
||||
resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement);
|
||||
replacement = targetBinding.hasName() ? ":" + targetBinding.getName()
|
||||
: ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?"
|
||||
: "?" + targetBinding.getPosition());
|
||||
String result;
|
||||
String substring = matcher.group(2);
|
||||
|
||||
int index = resultingQuery.indexOf(substring, currentIndex);
|
||||
if (index < 0) {
|
||||
result = resultingQuery;
|
||||
} else {
|
||||
currentIndex = index + replacement.length();
|
||||
result = resultingQuery.substring(0, index) + replacement
|
||||
+ resultingQuery.substring(index + substring.length());
|
||||
}
|
||||
|
||||
resultingQuery = result;
|
||||
}
|
||||
|
||||
return resultingQuery;
|
||||
@@ -336,16 +361,6 @@ class StringQuery implements DeclaredQuery {
|
||||
return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
|
||||
}
|
||||
|
||||
private static String replaceFirst(String text, String substring, String replacement) {
|
||||
|
||||
int index = text.indexOf(substring);
|
||||
if (index < 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.substring(0, index) + replacement + text.substring(index + substring.length());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Integer getParameterIndex(@Nullable String parameterIndexString) {
|
||||
|
||||
@@ -438,54 +453,96 @@ class StringQuery implements DeclaredQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create unique parameter bindings for LIKE that can be evaluated by
|
||||
* {@code LikeRewritingQueryParameterSetterFactory}.
|
||||
* Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are
|
||||
* bound to potentially unique query parameters for {@link LikeParameterBinding#prepare(Object) LIKE rewrite}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
static class LikeParameterBindings {
|
||||
static class ParameterBindings {
|
||||
|
||||
private final MultiValueMap<String, LikeParameterBinding> likeBindings = new LinkedMultiValueMap<>();
|
||||
private final MultiValueMap<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
|
||||
|
||||
private final Consumer<ParameterBinding> registration;
|
||||
private int syntheticParameterIndex;
|
||||
|
||||
public ParameterBindings(List<ParameterBinding> bindings, Consumer<ParameterBinding> registration,
|
||||
int syntheticParameterIndex) {
|
||||
|
||||
for (ParameterBinding binding : bindings) {
|
||||
this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding)));
|
||||
}
|
||||
|
||||
this.registration = registration;
|
||||
this.syntheticParameterIndex = syntheticParameterIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an existing or create a new {@link LikeParameterBinding} if a previously bound {@code LIKE} expression cannot
|
||||
* be reused.
|
||||
* Return whether the identifier is already bound.
|
||||
*
|
||||
* @param parameterName the parameter name as declared in the actual JPQL query.
|
||||
* @param likeType type of the LIKE expression.
|
||||
* @param origin origin of the parameter.
|
||||
* @return the Like binding. Can return an already existing binding.
|
||||
* @param identifier
|
||||
* @return
|
||||
*/
|
||||
LikeParameterBinding getOrCreate(String parameterName, Type likeType, ParameterOrigin origin) {
|
||||
public boolean isBound(BindingIdentifier identifier) {
|
||||
return !getBindings(identifier).isEmpty();
|
||||
}
|
||||
|
||||
List<LikeParameterBinding> likeParameterBindings = likeBindings.computeIfAbsent(parameterName,
|
||||
s -> new ArrayList<>());
|
||||
LikeParameterBinding reuse = null;
|
||||
BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
|
||||
Function<BindingIdentifier, ParameterBinding> bindingFactory) {
|
||||
|
||||
// unique parameters only required for literals as expressions create unique parameter names
|
||||
if (origin.isMethodArgument()) {
|
||||
for (LikeParameterBinding likeParameterBinding : likeParameterBindings) {
|
||||
Assert.isInstanceOf(MethodInvocationArgument.class, origin);
|
||||
|
||||
if (likeParameterBinding.getType() == likeType) {
|
||||
reuse = likeParameterBinding;
|
||||
break;
|
||||
}
|
||||
BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier();
|
||||
List<ParameterBinding> bindingsForOrigin = getBindings(methodArgument);
|
||||
|
||||
if (!isBound(identifier)) {
|
||||
|
||||
ParameterBinding binding = bindingFactory.apply(identifier);
|
||||
registration.accept(binding);
|
||||
bindingsForOrigin.add(binding);
|
||||
return binding.getIdentifier();
|
||||
}
|
||||
|
||||
ParameterBinding binding = bindingFactory.apply(identifier);
|
||||
|
||||
for (ParameterBinding existing : bindingsForOrigin) {
|
||||
|
||||
if (existing.isCompatibleWith(binding)) {
|
||||
return existing.getIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
if (reuse != null) {
|
||||
return reuse;
|
||||
BindingIdentifier syntheticIdentifier;
|
||||
if (identifier.hasName() && methodArgument.hasName()) {
|
||||
|
||||
int index = 0;
|
||||
String newName = methodArgument.getName();
|
||||
while (existsBoundParameter(newName)) {
|
||||
index++;
|
||||
newName = methodArgument.getName() + "_" + index;
|
||||
}
|
||||
syntheticIdentifier = BindingIdentifier.of(newName);
|
||||
} else {
|
||||
syntheticIdentifier = BindingIdentifier.of(++syntheticParameterIndex);
|
||||
}
|
||||
|
||||
if (!likeParameterBindings.isEmpty()) {
|
||||
parameterName = parameterName + "_" + likeParameterBindings.size();
|
||||
}
|
||||
ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier);
|
||||
registration.accept(newBinding);
|
||||
bindingsForOrigin.add(newBinding);
|
||||
return newBinding.getIdentifier();
|
||||
}
|
||||
|
||||
LikeParameterBinding binding = new LikeParameterBinding(BindingIdentifier.of(parameterName), origin, likeType);
|
||||
likeParameterBindings.add(binding);
|
||||
private boolean existsBoundParameter(String key) {
|
||||
return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream)
|
||||
.anyMatch(it -> key.equals(it.getName()));
|
||||
}
|
||||
|
||||
return binding;
|
||||
private List<ParameterBinding> getBindings(BindingIdentifier identifier) {
|
||||
return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>());
|
||||
}
|
||||
|
||||
public void register(ParameterBinding parameterBinding) {
|
||||
registration.accept(parameterBinding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,6 +978,9 @@ class UserRepositoryTests {
|
||||
|
||||
assertThat(repository.findByFirstnameLikeNamed("Da")).containsOnly(thirdUser);
|
||||
assertThat(repository.findByFirstnameLikeNamed("in")).containsOnly(fourthUser);
|
||||
|
||||
assertThat(repository.findByFirstnameLikePositional("Da")).containsOnly(thirdUser);
|
||||
assertThat(repository.findByFirstnameLikePositional("in")).containsOnly(fourthUser);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-231
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.query;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -25,6 +27,8 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
|
||||
import org.springframework.data.repository.query.parser.Part.Type;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
|
||||
/**
|
||||
@@ -101,6 +105,7 @@ class ExpressionBasedStringQueryUnitTests {
|
||||
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
|
||||
metadata, SPEL_PARSER, true);
|
||||
|
||||
System.out.println(query.getQueryString());
|
||||
assertThat(query.isNativeQuery()).isFalse();
|
||||
}
|
||||
|
||||
@@ -119,4 +124,55 @@ class ExpressionBasedStringQueryUnitTests {
|
||||
|
||||
assertThat(query.isNativeQuery()).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-3041
|
||||
void namedExpressionsShouldCreateLikeBindings() {
|
||||
|
||||
StringQuery query = new ExpressionBasedStringQuery(
|
||||
"select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, SPEL_PARSER,
|
||||
false);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString()).isEqualTo(
|
||||
"select u from User u where u.firstname like :__$synthetic$__1 or u.firstname like :__$synthetic$__2");
|
||||
|
||||
List<ParameterBinding> bindings = query.getParameterBindings();
|
||||
assertThat(bindings).hasSize(2);
|
||||
|
||||
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getName()).isEqualTo("__$synthetic$__1");
|
||||
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
|
||||
|
||||
binding = (LikeParameterBinding) bindings.get(1);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getName()).isEqualTo("__$synthetic$__2");
|
||||
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
|
||||
}
|
||||
|
||||
@Test // GH-3041
|
||||
void indexedExpressionsShouldCreateLikeBindings() {
|
||||
|
||||
StringQuery query = new ExpressionBasedStringQuery(
|
||||
"select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, SPEL_PARSER,
|
||||
false);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString())
|
||||
.isEqualTo("select u from User u where u.firstname like ?1 or u.firstname like ?2");
|
||||
|
||||
List<ParameterBinding> bindings = query.getParameterBindings();
|
||||
assertThat(bindings).hasSize(2);
|
||||
|
||||
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getPosition()).isEqualTo(1);
|
||||
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
|
||||
|
||||
binding = (LikeParameterBinding) bindings.get(1);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getPosition()).isEqualTo(2);
|
||||
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@ import java.util.List;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
|
||||
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.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
|
||||
import org.springframework.data.repository.query.parser.Part.Type;
|
||||
|
||||
/**
|
||||
@@ -82,7 +85,33 @@ class StringQueryUnitTests {
|
||||
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-292
|
||||
@Test // DATAJPA-292, GH-3041
|
||||
void detectsAnonymousLikeBindings() {
|
||||
|
||||
StringQuery query = new StringQuery(
|
||||
"select u from User u where u.firstname like %?% or u.lastname like %? or u.lastname=?", true);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString())
|
||||
.isEqualTo("select u from User u where u.firstname like ? or u.lastname like ? or u.lastname=?");
|
||||
|
||||
List<ParameterBinding> bindings = query.getParameterBindings();
|
||||
assertThat(bindings).hasSize(3);
|
||||
|
||||
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter(1));
|
||||
assertThat(binding.getRequiredPosition()).isEqualTo(1);
|
||||
assertThat(binding.getType()).isEqualTo(Type.CONTAINING);
|
||||
|
||||
binding = (LikeParameterBinding) bindings.get(1);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter(2));
|
||||
assertThat(binding.getRequiredPosition()).isEqualTo(2);
|
||||
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-292, GH-3041
|
||||
void detectsNamedLikeBindings() {
|
||||
|
||||
StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true);
|
||||
@@ -99,18 +128,20 @@ class StringQueryUnitTests {
|
||||
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-292
|
||||
@Test // GH-3041
|
||||
void rewritesNamedLikeToUniqueParametersIfNecessary() {
|
||||
|
||||
StringQuery query = new StringQuery(
|
||||
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname%", true);
|
||||
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname",
|
||||
true);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString())
|
||||
.isEqualTo("select u from User u where u.firstname like :firstname or u.firstname like :firstname_1");
|
||||
.isEqualTo(
|
||||
"select u from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname = :firstname_2");
|
||||
|
||||
List<ParameterBinding> bindings = query.getParameterBindings();
|
||||
assertThat(bindings).hasSize(2);
|
||||
assertThat(bindings).hasSize(3);
|
||||
|
||||
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
|
||||
assertThat(binding).isNotNull();
|
||||
@@ -123,8 +154,22 @@ class StringQueryUnitTests {
|
||||
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-292
|
||||
void reusesLikeBindingsWherePossible() {
|
||||
@Test // GH-3041
|
||||
void rewritesPositionalLikeToUniqueParametersIfNecessary() {
|
||||
|
||||
StringQuery query = new StringQuery(
|
||||
"select u from User u where u.firstname like %?1 or u.firstname like ?1% or u.firstname = ?1", true);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString())
|
||||
.isEqualTo("select u from User u where u.firstname like ?1 or u.firstname like ?2 or u.firstname = ?3");
|
||||
|
||||
List<ParameterBinding> bindings = query.getParameterBindings();
|
||||
assertThat(bindings).hasSize(3);
|
||||
}
|
||||
|
||||
@Test // GH-3041
|
||||
void reusesNamedLikeBindingsWherePossible() {
|
||||
|
||||
StringQuery query = new StringQuery(
|
||||
"select u from User u where u.firstname like %:firstname or u.firstname like %:firstname% or u.firstname like %:firstname% or u.firstname like %:firstname",
|
||||
@@ -134,18 +179,51 @@ class StringQueryUnitTests {
|
||||
assertThat(query.getQueryString()).isEqualTo(
|
||||
"select u from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname like :firstname_1 or u.firstname like :firstname");
|
||||
|
||||
|
||||
query = new StringQuery("select u from User u where u.firstname like %:firstname or u.firstname =:firstname", true);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString())
|
||||
.isEqualTo("select u from User u where u.firstname like :firstname or u.firstname =:firstname_1");
|
||||
}
|
||||
|
||||
@Test // GH-3041
|
||||
void reusesPositionalLikeBindingsWherePossible() {
|
||||
|
||||
StringQuery query = new StringQuery(
|
||||
"select u from User u where u.firstname like %?1 or u.firstname like %?1% or u.firstname like %?1% or u.firstname like %?1",
|
||||
false);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString()).isEqualTo(
|
||||
"select u from User u where u.firstname like ?1 or u.firstname like ?2 or u.firstname like ?2 or u.firstname like ?1");
|
||||
|
||||
query = new StringQuery("select u from User u where u.firstname like %?1 or u.firstname =?1", false);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like ?1 or u.firstname =?2");
|
||||
}
|
||||
|
||||
@Test // GH-3041
|
||||
void shouldRewritePositionalBindingsWithParameterReuse() {
|
||||
|
||||
StringQuery query = new StringQuery(
|
||||
"select u from User u where u.firstname like ?2 or u.firstname like %?2% or u.firstname like %?1% or u.firstname like %?1 OR u.firstname like ?1",
|
||||
false);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
assertThat(query.getQueryString()).isEqualTo(
|
||||
"select u from User u where u.firstname like ?2 or u.firstname like ?3 or u.firstname like ?1 or u.firstname like ?4 OR u.firstname like ?5");
|
||||
|
||||
List<ParameterBinding> bindings = query.getParameterBindings();
|
||||
assertThat(bindings).hasSize(2);
|
||||
assertThat(bindings).hasSize(5);
|
||||
|
||||
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getName()).isEqualTo("firstname");
|
||||
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
|
||||
|
||||
binding = (LikeParameterBinding) bindings.get(1);
|
||||
assertThat(binding).isNotNull();
|
||||
assertThat(binding.getName()).isEqualTo("firstname_1");
|
||||
assertThat(binding.getType()).isEqualTo(Type.CONTAINING);
|
||||
assertThat(bindings).extracting(ParameterBinding::getRequiredPosition).containsOnly(1, 2, 3, 4, 5);
|
||||
assertThat(bindings).extracting(ParameterBinding::getOrigin) //
|
||||
.map(MethodInvocationArgument.class::cast) //
|
||||
.map(MethodInvocationArgument::identifier) //
|
||||
.map(BindingIdentifier::getPosition) //
|
||||
.containsOnly(1, 2);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-461
|
||||
@@ -220,12 +298,6 @@ class StringQueryUnitTests {
|
||||
new StringQuery("select u from User u where u.firstname like %:firstname or foo like :bar", true);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-292, DATAJPA-362
|
||||
void rejectsDifferentBindingsForRepeatedParameter() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new StringQuery("select u from User u where u.firstname like %?1 and u.lastname like ?1%", true));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-461
|
||||
void treatsGreaterThanBindingAsSimpleBinding() {
|
||||
|
||||
@@ -309,12 +381,6 @@ class StringQueryUnitTests {
|
||||
assertNamedBinding(InParameterBinding.class, "ab1babc생일233", bindings.get(0));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-362
|
||||
void rejectsDifferentBindingsForRepeatedParameter2() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new StringQuery("select u from User u where u.firstname like ?1 and u.lastname like %?1", true));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-712
|
||||
void shouldReplaceAllNamedExpressionParametersWithInClause() {
|
||||
|
||||
@@ -503,6 +569,8 @@ class StringQueryUnitTests {
|
||||
|
||||
assertThat(new StringQuery("from Something something where something = ?", false).usesJdbcStyleParameters())
|
||||
.isTrue();
|
||||
assertThat(new StringQuery("from Something something where something =?", false).usesJdbcStyleParameters())
|
||||
.isTrue();
|
||||
|
||||
List<String> testQueries = Arrays.asList( //
|
||||
"from Something something where something = ?1", //
|
||||
@@ -514,6 +582,7 @@ class StringQueryUnitTests {
|
||||
|
||||
assertThat(new StringQuery(testQuery, false) //
|
||||
.usesJdbcStyleParameters()) //
|
||||
.describedAs(testQuery)
|
||||
.describedAs(testQuery) //
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@@ -169,6 +169,10 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
@Query("select u from User u where u.firstname like :firstname% or u.firstname like %:firstname")
|
||||
List<User> findByFirstnameLikeNamed(@Param("firstname") String firstname);
|
||||
|
||||
// DATAJPA-292, GH-3041
|
||||
@Query("select u from User u where u.firstname like ?1% or u.firstname like %?1")
|
||||
List<User> findByFirstnameLikePositional(String firstname);
|
||||
|
||||
/**
|
||||
* Manipulating query to set all {@link User}'s names to the given one.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user