DATAJPA-1140 - Allow mixing of SpEL expression and normal query parameters.
Restructured the complete parameter binding. There are now two principle ways to perform the binding: - if no query is known to the binder the method parameters get iterated and assigned to query parameters. This is implemented by the bind method of the ParameterBinder. - if a query is known all the bindings, i.e the query parameters are iterated and matching values are searched. This is implemented in the QueryAwareParameterBinder. For finding a suitable value there exists various QueryParameterSetterStrategies. The code calling the various Query.setParameter(…) overloads is extracted and gathered in QueryParameterSetter. Original pull request: #206.
This commit is contained in:
committed by
Oliver Gierke
parent
37aabff481
commit
8dbbe553e1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2008-2016 the original author or authors.
|
||||
* Copyright 2008-2017 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.
|
||||
@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
|
||||
@@ -87,7 +88,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
*/
|
||||
@Override
|
||||
protected ParameterBinder createBinder(Object[] values) {
|
||||
return new SpelExpressionStringQueryParameterBinder(getQueryMethod().getParameters(), values, query,
|
||||
return new QueryAwareParameterBinder(getQueryMethod().getParameters(), values, query,
|
||||
evaluationContextProvider, parser);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,13 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
|
||||
import javax.persistence.Parameter;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
|
||||
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.ParameterExpressionQueryParameterSetter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -33,6 +31,7 @@ import org.springframework.util.Assert;
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Mark Paluch
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class CriteriaQueryParameterBinder extends ParameterBinder {
|
||||
|
||||
@@ -60,19 +59,18 @@ class CriteriaQueryParameterBinder extends ParameterBinder {
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void bind(Query query, JpaParameter parameter, Object value, int position) {
|
||||
protected QueryParameterSetter bind(JpaParameter parameter, Object value, Integer position) {
|
||||
|
||||
ParameterMetadata<Object> metadata = (ParameterMetadata<Object>) expressions.next();
|
||||
|
||||
if (metadata.isIsNullParameter()) {
|
||||
return;
|
||||
return QueryParameterSetter.NOOP;
|
||||
}
|
||||
|
||||
if (parameter.isTemporalParameter()) {
|
||||
query.setParameter((Parameter<Date>) (Object) metadata.getExpression(), (Date) metadata.prepare(value),
|
||||
parameter.getTemporalType());
|
||||
} else {
|
||||
query.setParameter(metadata.getExpression(), metadata.prepare(value));
|
||||
return new ParameterExpressionQueryParameterSetter(metadata.getExpression(), parameter.getTemporalType(), metadata.prepare(value));
|
||||
}
|
||||
|
||||
return new ParameterExpressionQueryParameterSetter(metadata.getExpression(), null, metadata.prepare(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.persistence.Query;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -35,18 +33,19 @@ import org.springframework.util.Assert;
|
||||
* @author Thomas Darimont
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class ParameterBinder {
|
||||
|
||||
/** Meta information for the method parameters */
|
||||
private final JpaParameters parameters;
|
||||
private final ParameterAccessor accessor;
|
||||
private final Object[] values;
|
||||
protected final ParameterAccessor accessor;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ParameterBinder}.
|
||||
*
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param values must not be {@literal null}.
|
||||
* @param values values of the parameters passed to the method. Must not be {@literal null}.
|
||||
*/
|
||||
public ParameterBinder(JpaParameters parameters, Object[] values) {
|
||||
|
||||
@@ -56,8 +55,7 @@ public class ParameterBinder {
|
||||
Assert.isTrue(parameters.getNumberOfParameters() == values.length, "Invalid number of parameters given!");
|
||||
|
||||
this.parameters = parameters;
|
||||
this.values = values.clone();
|
||||
this.accessor = new ParametersParameterAccessor(parameters, this.values);
|
||||
this.accessor = new ParametersParameterAccessor(parameters, values.clone());
|
||||
}
|
||||
|
||||
ParameterBinder(JpaParameters parameters) {
|
||||
@@ -66,20 +64,16 @@ public class ParameterBinder {
|
||||
|
||||
/**
|
||||
* Returns the {@link Pageable} of the parameters, if available. Returns {@code null} otherwise.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Pageable getPageable() {
|
||||
Pageable getPageable() {
|
||||
return accessor.getPageable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sort instance to be used for query creation. Will use a {@link Sort} parameter if available or the
|
||||
* {@link Sort} contained in a {@link Pageable} if available. Returns {@code null} if no {@link Sort} can be found.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Sort getSort() {
|
||||
Sort getSort() {
|
||||
return accessor.getSort();
|
||||
}
|
||||
|
||||
@@ -87,21 +81,21 @@ public class ParameterBinder {
|
||||
* Binds the parameters to the given {@link Query}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public <T extends Query> T bind(T query) {
|
||||
protected <T extends Query> T bind(T query) {
|
||||
|
||||
Assert.notNull(query, "Query must not be null!");
|
||||
|
||||
int bindableParameterIndex = 0;
|
||||
int queryParameterPosition = 1;
|
||||
|
||||
for (JpaParameter parameter : parameters) {
|
||||
|
||||
if (canBindParameter(parameter)) {
|
||||
if (parameter.isBindable()) {
|
||||
|
||||
Object value = accessor.getBindableValue(bindableParameterIndex);
|
||||
bind(query, parameter, value, queryParameterPosition++);
|
||||
|
||||
bind(parameter, value, bindableParameterIndex + 1).setParameter(query);
|
||||
|
||||
bindableParameterIndex++;
|
||||
}
|
||||
}
|
||||
@@ -110,62 +104,33 @@ public class ParameterBinder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@literal true} if the given parameter can be bound.
|
||||
*
|
||||
* @param parameter
|
||||
* @return
|
||||
* Creates {@link QueryParameterSetter} for the given {@link JpaParameter}. This implementation uses the name or index
|
||||
* of the passed in parameter. This method is intended to be overwritten by subclasses in order to implement
|
||||
* alternative binding strategies.
|
||||
*
|
||||
* @param parameter Method parameter from which to create a {@link QueryParameterSetter}
|
||||
* @param value The value of the method parameter.
|
||||
* @param position the index of the query parameter. Note that there is no simple relationg between index of the
|
||||
* method parameter and the position of the bind parameter due to unbindable parameters.
|
||||
* @return guaranteed not to be {@literal null}.
|
||||
*/
|
||||
protected boolean canBindParameter(JpaParameter parameter) {
|
||||
return parameter.isBindable();
|
||||
}
|
||||
protected QueryParameterSetter bind(JpaParameter parameter, Object value, Integer position) {
|
||||
|
||||
/**
|
||||
* Perform the actual query parameter binding.
|
||||
*
|
||||
* @param query
|
||||
* @param parameter
|
||||
* @param value
|
||||
* @param position
|
||||
*/
|
||||
protected void bind(Query query, JpaParameter parameter, Object value, int position) {
|
||||
|
||||
if (parameter.isTemporalParameter()) {
|
||||
if (hasNamedParameter(query) && parameter.isNamedParameter()) {
|
||||
query.setParameter(
|
||||
parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O paraneter needs to have a name!")),
|
||||
(Date) value, parameter.getTemporalType());
|
||||
} else {
|
||||
query.setParameter(position, (Date) value, parameter.getTemporalType());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNamedParameter(query) && parameter.isNamedParameter()) {
|
||||
query.setParameter(
|
||||
parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O paraneter needs to have a name!")),
|
||||
value);
|
||||
} else {
|
||||
query.setParameter(position, value);
|
||||
}
|
||||
return QueryParameterSetter.fromJpaParameter(parameter, position, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the parameters to the given query and applies special parameter types (e.g. pagination).
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Query bindAndPrepare(Query query) {
|
||||
Query bindAndPrepare(Query query) {
|
||||
|
||||
Assert.notNull(query, "Query must not be null!");
|
||||
|
||||
return bindAndPrepare(query, parameters);
|
||||
}
|
||||
|
||||
boolean hasNamedParameter(Query query) {
|
||||
return QueryUtils.hasNamedParameter(query);
|
||||
}
|
||||
|
||||
private Query bindAndPrepare(Query query, Parameters<?, ?> parameters) {
|
||||
|
||||
Query result = bind(query);
|
||||
@@ -179,17 +144,4 @@ public class ParameterBinder {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
JpaParameters getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
protected Object[] getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2013-2017 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
|
||||
*
|
||||
* http://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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.persistence.Query;
|
||||
|
||||
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ParameterBinder} has a {@link StringQuery} and therefor knows which query parameters need values. For binding
|
||||
* values to a query it iterates the required bindings and tries to find a value for each binding. For finding those
|
||||
* values it uses a list of strategies.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Christoph Strobl
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class QueryAwareParameterBinder extends ParameterBinder {
|
||||
|
||||
private final StringQuery query;
|
||||
|
||||
private final List<QueryParameterSetterStrategy> queryParameterSetterStrategies = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a new {@link QueryAwareParameterBinder} from the given {@link Parameters}, method arguments and
|
||||
* {@link StringQuery}.
|
||||
*
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param values must not be {@literal null}.
|
||||
* @param query must not be {@literal null}.
|
||||
*/
|
||||
QueryAwareParameterBinder(JpaParameters parameters, Object[] values, StringQuery query,
|
||||
EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
|
||||
|
||||
super(parameters, values);
|
||||
|
||||
Assert.notNull(query, "StringQuery must not be null!");
|
||||
this.query = query;
|
||||
|
||||
queryParameterSetterStrategies
|
||||
.add(new QueryParameterSetterStrategy.ExpressionParameterStrategy(evaluationContextProvider, parser, parameters, values.clone()));
|
||||
queryParameterSetterStrategies.add(new QueryParameterSetterStrategy.BasicParameterStrategy(parameters, accessor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Query> T bind(T jpaQuery) {
|
||||
|
||||
for (ParameterBinding binding : query.getParameterBindings()) {
|
||||
createQueryParameterSetter(jpaQuery, binding).setParameter(jpaQuery);
|
||||
}
|
||||
|
||||
return jpaQuery;
|
||||
}
|
||||
|
||||
private <T extends Query> QueryParameterSetter createQueryParameterSetter(T jpaQuery, ParameterBinding binding) {
|
||||
|
||||
for (QueryParameterSetterStrategy strategy : queryParameterSetterStrategies) {
|
||||
QueryParameterSetter candidate = strategy.create(jpaQuery, binding);
|
||||
if (candidate != null)
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return QueryParameterSetter.NOOP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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 java.util.Date;
|
||||
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.TemporalType;
|
||||
import javax.persistence.criteria.ParameterExpression;
|
||||
|
||||
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
|
||||
|
||||
/**
|
||||
* The interface encapsulates the setting of query parameters which might use a significant number of variations of
|
||||
* {@literal Query.setParameter}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 2.0
|
||||
*/
|
||||
interface QueryParameterSetter {
|
||||
|
||||
void setParameter(Query query);
|
||||
|
||||
/** Noop implementation */
|
||||
QueryParameterSetter NOOP = query -> {};
|
||||
|
||||
/**
|
||||
* Creates a {@link QueryParameterSetter} from a {@link JpaParameter}. Handles named and indexed parameters and
|
||||
* TemporalType annotations.
|
||||
*
|
||||
* @param parameter the method parameter to bind
|
||||
* @param position position of the bind parameter, might be {@literal null} only if parameters are named.
|
||||
* @param value the value passed to the method parameter.
|
||||
*/
|
||||
static QueryParameterSetter fromJpaParameter(JpaParameter parameter, Integer position, Object value) {
|
||||
|
||||
TemporalType temporalType = parameter.isTemporalParameter() ? parameter.getTemporalType() : null;
|
||||
String name = parameter.isNamedParameter()
|
||||
? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name!"))
|
||||
: null;
|
||||
return new NamedOrIndexedQueryParameterSetter(name, position, temporalType, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link QueryParameterSetter} which ignores {@link IllegalArgumentException}s when calling
|
||||
* {@literal setParameter}. Useful because certain JPA implementations do not correctly report the presence of
|
||||
* parameters in a query.
|
||||
*
|
||||
* @param name the name of the parameter to bind. Might be {@literal null}.
|
||||
* @param position position of the bind parameter, might be {@literal null} only if parameters are named.
|
||||
* @param value the value passed to the method parameter.
|
||||
*/
|
||||
static QueryParameterSetter lenientQueryParameterSetter(String name, Integer position, Object value) {
|
||||
|
||||
return (Query q) -> {
|
||||
try {
|
||||
if (name != null) {
|
||||
q.setParameter(name, value);
|
||||
} else {
|
||||
q.setParameter(position, value);
|
||||
}
|
||||
} catch (IllegalArgumentException iae) {
|
||||
|
||||
// Since Eclipse doesn't reliably report whether a query has parameters
|
||||
// we simply try to set the parameters and ignore possible failures.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified.
|
||||
*/
|
||||
class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter {
|
||||
|
||||
private final boolean useParameterAccessByName;
|
||||
private final String name;
|
||||
private final Integer index;
|
||||
private final TemporalType temporalType;
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* @param name of the parameter, if {@literal null} index based parameter access will be used.
|
||||
* @param index index of the parameter. Only used when {@literal name) is {@literal null}
|
||||
* @param temporalType can be {@literal null}
|
||||
* @param value the value to be set, might be {@literal null}
|
||||
*/
|
||||
NamedOrIndexedQueryParameterSetter(String name, Integer index, TemporalType temporalType, Object value) {
|
||||
|
||||
this.useParameterAccessByName = name != null;
|
||||
this.name = name;
|
||||
this.index = index;
|
||||
this.temporalType = temporalType;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void setParameter(Query query) {
|
||||
|
||||
if (temporalType != null) {
|
||||
|
||||
if (useParameterAccessByName && QueryUtils.hasNamedParameter(query)) {
|
||||
query.setParameter(name, (Date) value, temporalType);
|
||||
} else {
|
||||
query.setParameter(index, (Date) value, temporalType);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (useParameterAccessByName && QueryUtils.hasNamedParameter(query)) {
|
||||
query.setParameter(name, value);
|
||||
} else {
|
||||
query.setParameter(index, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link QueryParameterSetter} identifying parameters by {@link ParameterExpression}
|
||||
*/
|
||||
class ParameterExpressionQueryParameterSetter implements QueryParameterSetter {
|
||||
|
||||
private final ParameterExpression parameterExpression;
|
||||
private final TemporalType temporalType;
|
||||
private final Object value;
|
||||
|
||||
ParameterExpressionQueryParameterSetter(ParameterExpression parameterExpression, TemporalType temporalType,
|
||||
Object value) {
|
||||
|
||||
this.temporalType = temporalType;
|
||||
this.value = value;
|
||||
this.parameterExpression = parameterExpression;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setParameter(Query query) {
|
||||
if (temporalType != null) {
|
||||
|
||||
query.setParameter(parameterExpression, (Date) value, temporalType);
|
||||
} else {
|
||||
|
||||
query.setParameter(parameterExpression, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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 java.util.function.Predicate;
|
||||
|
||||
import javax.persistence.Query;
|
||||
|
||||
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
|
||||
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Encapsulates different strategies for the creation of a {@link QueryParameterSetter} from a {@link Query} and a
|
||||
* {@link ParameterBinding}
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 2.0
|
||||
*/
|
||||
interface QueryParameterSetterStrategy {
|
||||
|
||||
QueryParameterSetter create(Query jpaQuery, ParameterBinding binding);
|
||||
|
||||
/**
|
||||
* Handles bindings that are SpEL expressions by evalutating the expression to obtain a value.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class ExpressionParameterStrategy implements QueryParameterSetterStrategy {
|
||||
|
||||
private final EvaluationContextProvider evaluationContextProvider;
|
||||
private final SpelExpressionParser parser;
|
||||
private final Parameters<?, ?> parameters;
|
||||
private final Object[] values;
|
||||
|
||||
ExpressionParameterStrategy(EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser,
|
||||
Parameters<?, ?> parameters, Object[] values) {
|
||||
|
||||
this.parameters = parameters;
|
||||
this.values = values;
|
||||
|
||||
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
|
||||
Assert.notNull(parser, "SpelExpressionParser must not be null!");
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryParameterSetter create(Query jpaQuery, ParameterBinding binding) {
|
||||
|
||||
if (binding.isExpression()) {
|
||||
|
||||
Expression expr = parseExpressionString(binding.getExpression());
|
||||
|
||||
Object value = evaluateExpression(expr);
|
||||
|
||||
return QueryParameterSetter.lenientQueryParameterSetter(binding.getName(), binding.getPosition(),
|
||||
binding.prepare(value));
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression parseExpressionString(String expressionString) {
|
||||
return parser.parseExpression(expressionString);
|
||||
}
|
||||
|
||||
private Object evaluateExpression(Expression expr) {
|
||||
return expr.getValue(getEvaluationContext(), Object.class);
|
||||
}
|
||||
|
||||
private EvaluationContext getEvaluationContext() {
|
||||
return evaluationContextProvider.getEvaluationContext(parameters, values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 2.0
|
||||
*/
|
||||
class BasicParameterStrategy implements QueryParameterSetterStrategy {
|
||||
|
||||
private final JpaParameters parameters;
|
||||
private final ParameterAccessor accessor;
|
||||
|
||||
BasicParameterStrategy(JpaParameters parameters, ParameterAccessor accessor) {
|
||||
|
||||
this.parameters = parameters;
|
||||
this.accessor = accessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryParameterSetter create(Query jpaQuery, ParameterBinding binding) {
|
||||
|
||||
JpaParameter parameter = findParameterForBinding( //
|
||||
QueryUtils.hasNamedParameter(jpaQuery) //
|
||||
? matchByName(binding) : matchByIndex(binding) //
|
||||
);
|
||||
|
||||
if (parameter == null) {
|
||||
return QueryParameterSetter.NOOP;
|
||||
}
|
||||
|
||||
Object preparedValue = binding.prepare(accessor.getBindableValue(parameter.getIndex()));
|
||||
|
||||
return QueryParameterSetter.fromJpaParameter(parameter, binding.getPosition(), preparedValue);
|
||||
}
|
||||
|
||||
private JpaParameter findParameterForBinding(Predicate<JpaParameter> predicate) {
|
||||
|
||||
for (JpaParameter methodParameterCandidate : parameters.getBindableParameters()) {
|
||||
if (predicate.test(methodParameterCandidate)) {
|
||||
return methodParameterCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Predicate<JpaParameter> matchByIndex(ParameterBinding binding) {
|
||||
return p -> p.getIndex() + 1 == binding.getPosition();
|
||||
}
|
||||
|
||||
private static Predicate<JpaParameter> matchByName(ParameterBinding binding) {
|
||||
|
||||
return p -> binding.getName().equals( //
|
||||
p.getName().orElseThrow( //
|
||||
() -> new IllegalArgumentException("Parameter needs to be named!") //
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014 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
|
||||
*
|
||||
* http://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 java.util.List;
|
||||
|
||||
import javax.persistence.Query;
|
||||
|
||||
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
|
||||
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link StringQueryParameterBinder} that is able to bind synthetic query parameters.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
class SpelExpressionStringQueryParameterBinder extends StringQueryParameterBinder {
|
||||
|
||||
private final StringQuery query;
|
||||
private final EvaluationContextProvider evaluationContextProvider;
|
||||
private final SpelExpressionParser parser;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SpelExpressionStringQueryParameterBinder}.
|
||||
*
|
||||
* @param parameters must not be {@literal null}
|
||||
* @param values must not be {@literal null}
|
||||
* @param query must not be {@literal null}
|
||||
* @param evaluationContextProvider must not be {@literal null}
|
||||
* @param parser must not be {@literal null}
|
||||
*/
|
||||
public SpelExpressionStringQueryParameterBinder(JpaParameters parameters, Object[] values, StringQuery query,
|
||||
EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
|
||||
|
||||
super(parameters, values, query);
|
||||
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
|
||||
Assert.notNull(parser, "SpelExpressionParser must not be null!");
|
||||
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.query = query;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query)
|
||||
*/
|
||||
@Override
|
||||
public <T extends Query> T bind(T jpaQuery) {
|
||||
return potentiallyBindExpressionParameters(super.bind(jpaQuery));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jpaQuery must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
private <T extends Query> T potentiallyBindExpressionParameters(T jpaQuery) {
|
||||
|
||||
if (isJpaParameterInformationReliable(jpaQuery) && jpaQuery.getParameters().isEmpty()) {
|
||||
// We can rely on the fact there are no parameters in the given query.
|
||||
return jpaQuery;
|
||||
}
|
||||
|
||||
for (ParameterBinding binding : query.getParameterBindings()) {
|
||||
|
||||
if (binding.isExpression()) {
|
||||
|
||||
Expression expr = parseExpressionString(binding.getExpression());
|
||||
|
||||
Object value = evaluateExpression(expr);
|
||||
|
||||
try {
|
||||
if (binding.getName() != null) {
|
||||
jpaQuery.setParameter(binding.getName(), binding.prepare(value));
|
||||
} else {
|
||||
jpaQuery.setParameter(binding.getPosition(), binding.prepare(value));
|
||||
}
|
||||
} catch (IllegalArgumentException iae) {
|
||||
|
||||
// Since Eclipse doesn't reliably report whether a query has parameters
|
||||
// we simply try to set the parameters and ignore possible failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return jpaQuery;
|
||||
}
|
||||
|
||||
private <T extends Query> boolean isJpaParameterInformationReliable(T jpaQuery) {
|
||||
|
||||
String className = jpaQuery.getClass().getName();
|
||||
return className.startsWith("org.apache.openjpa") || className.startsWith("org.hibernate");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given {@code expressionString} into a SpEL {@link Expression}.
|
||||
*
|
||||
* @param expressionString
|
||||
* @return
|
||||
*/
|
||||
private Expression parseExpressionString(String expressionString) {
|
||||
return parser.parseExpression(expressionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given SpEL {@link Expression}.
|
||||
*
|
||||
* @param expr
|
||||
* @return
|
||||
*/
|
||||
private Object evaluateExpression(Expression expr) {
|
||||
return expr.getValue(getEvaluationContext(), Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link StandardEvaluationContext} to use for evaluation.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private EvaluationContext getEvaluationContext() {
|
||||
return evaluationContextProvider.getEvaluationContext(getParameters(), getValues());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jpa.repository.query.ParameterBinder#canBindParameter(org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canBindParameter(JpaParameter parameter) {
|
||||
|
||||
List<ParameterBinding> parameterBindings = query.getParameterBindings();
|
||||
|
||||
// if no parameter bindings are present, we simply rely on the check in super.
|
||||
if (parameterBindings.isEmpty()) {
|
||||
return super.canBindParameter(parameter);
|
||||
}
|
||||
|
||||
// otherwise determine whether there are any non expression parameters left to be bound.
|
||||
int expressionParameterCount = 0;
|
||||
for (ParameterBinding binding : parameterBindings) {
|
||||
|
||||
if (binding.isExpression()) {
|
||||
expressionParameterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
boolean allParametersAreUsedInExpressions = parameterBindings.size() - expressionParameterCount == 0;
|
||||
|
||||
// if all parameters are used in expressions, then we can skip their bindings now, since they'll get bound later.
|
||||
return !allParametersAreUsedInExpressions && super.canBindParameter(parameter);
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,19 @@ import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Encapsulation of a String JPA query.
|
||||
* 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
|
||||
*
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Wehrens
|
||||
* @author Mark Paluch
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class StringQuery {
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2017 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
|
||||
*
|
||||
* http://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 javax.persistence.Query;
|
||||
|
||||
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
|
||||
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.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ParameterBinder} that takes {@link LikeParameterBinding}s encapsulated in a {@link StringQuery} into account.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class StringQueryParameterBinder extends ParameterBinder {
|
||||
|
||||
private final StringQuery query;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StringQueryParameterBinder} from the given {@link Parameters}, method arguments and
|
||||
* {@link StringQuery}.
|
||||
*
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param values must not be {@literal null}.
|
||||
* @param query must not be {@literal null}.
|
||||
*/
|
||||
public StringQueryParameterBinder(JpaParameters parameters, Object[] values, StringQuery query) {
|
||||
|
||||
super(parameters, values);
|
||||
|
||||
Assert.notNull(query, "StringQuery must not be null!");
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query, org.springframework.data.repository.query.Parameter, java.lang.Object, int)
|
||||
*/
|
||||
@Override
|
||||
protected void bind(Query jpaQuery, JpaParameter methodParameter, Object value, int position) {
|
||||
|
||||
ParameterBinding binding = getBindingFor(jpaQuery, position, methodParameter);
|
||||
super.bind(jpaQuery, methodParameter, binding.prepare(value), position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the {@link LikeParameterBinding} to be applied before binding a parameter value to the query.
|
||||
*
|
||||
* @param jpaQuery must not be {@literal null}.
|
||||
* @param position
|
||||
* @param parameter must not be {@literal null}.
|
||||
* @return the {@link ParameterBinding} for the given parameters or {@literal null} if none available.
|
||||
*/
|
||||
private ParameterBinding getBindingFor(Query jpaQuery, int position, Parameter parameter) {
|
||||
|
||||
Assert.notNull(jpaQuery, "Query must not be null!");
|
||||
Assert.notNull(parameter, "Parameter must not be null!");
|
||||
|
||||
if (hasNamedParameter(jpaQuery)) {
|
||||
return query.getBindingFor(
|
||||
parameter.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named!")));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
jpaQuery.getParameter(position);
|
||||
return query.getBindingFor(position);
|
||||
|
||||
} catch (IllegalArgumentException o_O) {
|
||||
|
||||
// We should actually reject parameters unavailable, but as EclipseLink doesn't implement ….getParameter(int) for
|
||||
// native queries correctly we need to fall back to an indexed parameter
|
||||
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=427892
|
||||
|
||||
return new ParameterBinding(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static javax.persistence.TemporalType.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.ArgumentMatchers.anyObject;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -27,6 +29,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.persistence.Embeddable;
|
||||
import javax.persistence.Parameter;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.TemporalType;
|
||||
|
||||
@@ -45,6 +48,7 @@ import org.springframework.data.repository.query.Param;
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ParameterBinderUnitTests {
|
||||
@@ -150,14 +154,13 @@ public class ParameterBinderUnitTests {
|
||||
public void usesParameterNameIfAnnotated() throws Exception {
|
||||
|
||||
when(query.setParameter(eq("username"), anyObject())).thenReturn(query);
|
||||
new ParameterBinder(new JpaParameters(valid), new Object[] { "foo" }) {
|
||||
|
||||
@Override
|
||||
boolean hasNamedParameter(Query query) {
|
||||
Parameter parameter = mock(Parameter.class);
|
||||
when(parameter.getName()).thenReturn("username");
|
||||
when(query.getParameters()).thenReturn(singleton(parameter));
|
||||
|
||||
new ParameterBinder(new JpaParameters(valid), new Object[] { "foo" }).bind(query);
|
||||
|
||||
return true;
|
||||
}
|
||||
}.bind(query);
|
||||
verify(query).setParameter(eq("username"), anyObject());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user