Skip to content

Commit

Permalink
Use unique named parameter bindings for like parameters.
Browse files Browse the repository at this point in the history
We now replace LIKE expressions according to their type with individual bindings if an existing binding cannot be used because of how the bound value is being transformed.

WHERE foo like %:name or bar like :name becomes
WHERE foo like :name (i.e. '%' + :name) or bar like :name_1 (i.e. :name)

See #3041
  • Loading branch information
mp911de authored and gregturn committed Jul 21, 2023
1 parent 7aadb14 commit c5950e3
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -327,14 +327,15 @@ public boolean canExtractQuery() {
}

/**
* Because Hibernate's {@literal TypedParameterValue} is only used to wrap a {@literal null}, swap it out with an
* empty string for query creation.
* Because Hibernate's {@literal TypedParameterValue} is only used to wrap a {@literal null}, swap it out with
* {@code null} for query creation.
*
* @param value
* @return the original value or null.
* @since 3.0
*/
public static Object unwrapTypedParameterValue(Object value) {
@Nullable
public static Object unwrapTypedParameterValue(@Nullable Object value) {

return typedParameterValueClass != null && typedParameterValueClass.isInstance(value) //
? null //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ static ParameterBinder createBinder(JpaParameters parameters) {

Assert.notNull(parameters, "JpaParameters must not be null");

QueryParameterSetterFactory likeFactory = QueryParameterSetterFactory.forLikeRewrite(parameters);
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters);
List<ParameterBinding> bindings = getBindings(parameters);

return new ParameterBinder(parameters, createSetters(bindings, setterFactory));
return new ParameterBinder(parameters, createSetters(bindings, likeFactory, setterFactory));
}

/**
Expand Down Expand Up @@ -95,9 +96,12 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Declared
List<ParameterBinding> bindings = query.getParameterBindings();
QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser,
evaluationContextProvider, parameters);

QueryParameterSetterFactory like = QueryParameterSetterFactory.forLikeRewrite(parameters);
QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters);

return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory),
return new ParameterBinder(parameters,
createSetters(bindings, query, expressionSetterFactory, like, basicSetterFactory),
!query.usesPaging());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding;
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
Expand Down Expand Up @@ -62,6 +63,20 @@ static QueryParameterSetterFactory basic(JpaParameters parameters) {
return new BasicQueryParameterSetterFactory(parameters);
}

/**
* Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters} applying LIKE rewrite for
* renamed {@code :foo%} or {@code %:bar} bindings.
*
* @param parameters must not be {@literal null}.
* @return a basic {@link QueryParameterSetterFactory} that can handle named parameters.
*/
static QueryParameterSetterFactory forLikeRewrite(JpaParameters parameters) {

Assert.notNull(parameters, "JpaParameters must not be null");

return new LikeRewritingQueryParameterSetterFactory(parameters);
}

/**
* Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters} and
* {@link ParameterMetadata}.
Expand Down Expand Up @@ -117,6 +132,29 @@ private static QueryParameterSetter createSetter(Function<JpaParametersParameter
ParameterImpl.of(parameter, binding), temporalType);
}

@Nullable
private static JpaParameter findParameterForBinding(Parameters<JpaParameters, JpaParameter> parameters, String name) {

JpaParameters bindableParameters = parameters.getBindableParameters();

for (JpaParameter bindableParameter : bindableParameters) {
if (name.equals(getRequiredName(bindableParameter))) {
return bindableParameter;
}
}

return null;
}

private static String getRequiredName(JpaParameter p) {
return p.getName().orElseThrow(() -> new IllegalStateException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED));
}

@Nullable
static Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) {
return accessor.getValue(parameter);
}

/**
* Handles bindings that are SpEL expressions by evaluating the expression to obtain a value.
*
Expand Down Expand Up @@ -176,6 +214,46 @@ private Object evaluateExpression(Expression expression, JpaParametersParameterA
}
}

/**
* Handles bindings that use Like-rewriting.
*
* @author Mark Paluch
* @since 3.1.2
*/
private static class LikeRewritingQueryParameterSetterFactory extends QueryParameterSetterFactory {

private final Parameters<?, ?> parameters;

/**
* @param parameters must not be {@literal null}.
*/
LikeRewritingQueryParameterSetterFactory(Parameters<?, ?> parameters) {

Assert.notNull(parameters, "Parameters must not be null");

this.parameters = parameters;
}

@Nullable
@Override
public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {

if (binding.isExpression() || !(binding instanceof LikeParameterBinding likeBinding)
|| !declaredQuery.hasNamedParameter()) {
return null;
}
JpaParameter parameter = QueryParameterSetterFactory.findParameterForBinding((JpaParameters) parameters,
likeBinding.getDeclaredName());

if (parameter == null) {
return null;
}

return createSetter(values -> values.getValue(parameter), binding, parameter);
}

}

/**
* Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters.
*
Expand Down Expand Up @@ -205,7 +283,7 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla
JpaParameter parameter;

if (declaredQuery.hasNamedParameter()) {
parameter = findParameterForBinding(binding);
parameter = findParameterForBinding(parameters, binding.getRequiredName());
} else {

int parameterIndex = binding.getRequiredPosition() - 1;
Expand All @@ -228,28 +306,6 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla
: createSetter(values -> getValue(values, parameter), binding, parameter);
}

@Nullable
private JpaParameter findParameterForBinding(ParameterBinding binding) {

JpaParameters bindableParameters = parameters.getBindableParameters();

for (JpaParameter bindableParameter : bindableParameters) {
if (binding.getRequiredName().equals(getName(bindableParameter))) {
return bindableParameter;
}
}

return null;
}

@Nullable
private Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) {
return accessor.getValue(parameter);
}

private static String getName(JpaParameter p) {
return p.getName().orElseThrow(() -> new IllegalStateException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED));
}
}

/**
Expand Down Expand Up @@ -366,7 +422,7 @@ public Class<T> getParameterType() {
@Nullable
private static String getName(@Nullable JpaParameter parameter, ParameterBinding binding) {

if (parameter == null) {
if (binding.hasName() || parameter == null) {
return binding.getName();
}

Expand Down
Loading

0 comments on commit c5950e3

Please sign in to comment.