diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 980c2b26c..05f35588e 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -188,7 +188,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { } protected ParameterBinder createBinder() { - return ParameterBinderFactory.createParameterBinder(getQueryMethod().getParameters()); + return ParameterBinderFactory.createBinder(getQueryMethod().getParameters()); } protected Query createQuery(Object[] values) { diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 600b49091..0c25d23e9 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -91,8 +91,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { @Override protected ParameterBinder createBinder() { - return ParameterBinderFactory.createQueryAwareParameterBinder(getQueryMethod().getParameters(), query, - evaluationContextProvider, parser); + return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, + parser, evaluationContextProvider); } /* diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java index 00f412925..73e131437 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.query; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; @@ -37,7 +38,7 @@ public class JpaParametersParameterAccessor extends ParametersParameterAccessor super(parameters, values); } - public T getValue(JpaParameter parameter) { + public T getValue(Parameter parameter) { return super.getValue(parameter.getIndex()); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java index f2385b7dd..13253a2e4 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java @@ -15,15 +15,11 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.List; - import javax.persistence.Query; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.util.Assert; -import lombok.RequiredArgsConstructor; - /** * {@link ParameterBinder} is used to bind method parameters to a {@link Query}. This is usually done whenever an * {@link AbstractJpaQuery} is executed. @@ -34,17 +30,29 @@ import lombok.RequiredArgsConstructor; * @author Christoph Strobl * @author Jens Schauder */ -@RequiredArgsConstructor public class ParameterBinder { private final JpaParameters parameters; - private final List parameterSetters; + private final Iterable parameterSetters; + + /** + * Creates a new {@link ParameterBinder} for the given {@link JpaParameters} and {@link QueryParameterSetter}s. + * + * @param parameters must not be {@literal null}. + * @param parameterSetters must not be {@literal null}. + */ + public ParameterBinder(JpaParameters parameters, Iterable parameterSetters) { + + Assert.notNull(parameters, "JpaParameters must not be null!"); + Assert.notNull(parameterSetters, "Parameter setters must not be null!"); + + this.parameters = parameters; + this.parameterSetters = parameterSetters; + } public T bind(T jpaQuery, Object[] values) { - for (QueryParameterSetter setter : parameterSetters) { - setter.setParameter(jpaQuery, values); - } + parameterSetters.forEach(it -> it.setParameter(jpaQuery, values)); return jpaQuery; } @@ -72,5 +80,4 @@ public class ParameterBinder { return result; } - } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index d4547c6b6..1ed01b4e0 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -16,87 +16,94 @@ package org.springframework.data.jpa.repository.query; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; 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.QueryParameterSetterFactory.BasicQueryParameterSetterFactory; -import org.springframework.data.jpa.repository.query.QueryParameterSetterFactory.CriteriaQueryParameterSetterFactory; -import org.springframework.data.jpa.repository.query.QueryParameterSetterFactory.ExpressionBasedQueryParameterSetterFactory; import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.util.StreamUtils; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; /** * Factory for differently configured {@link ParameterBinder}. * * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 */ class ParameterBinderFactory { /** - * create a {@link ParameterBinder} that just matches parameter by name if those are available, or by index/position + * Create a {@link ParameterBinder} that just matches parameter by name if those are available, or by index/position * otherwise. * - * @param parameters method parameters that are available for binding + * @param parameters method parameters that are available for binding, must not be {@literal null}. * @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a * {@link javax.persistence.Query} */ - static ParameterBinder createParameterBinder(JpaParameters parameters) { + static ParameterBinder createBinder(JpaParameters parameters) { - List setters = createParameterSetters( // - getParameterBindings(parameters), // - null, // - new BasicQueryParameterSetterFactory(parameters) // - ); + Assert.notNull(parameters, "JpaParameters must not be null!"); - return new ParameterBinder(parameters, setters); + QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters); + List bindings = getBindings(parameters); + + return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); } /** - * create a {@link ParameterBinder} that just matches method parameter to parameters of a - * javax.{@link javax.persistence.criteria.CriteriaQuery}. + * Creates a {@link ParameterBinder} that just matches method parameter to parameters of a + * {@link javax.persistence.criteria.CriteriaQuery}. * - * @param parameters method parameters that are available for binding + * @param parameters method parameters that are available for binding, must not be {@literal null}. + * @param metadata must not be {@literal null}. * @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a * {@link javax.persistence.criteria.CriteriaQuery} */ - static ParameterBinder createCriteriaParameterBinder(JpaParameters parameters, - List> expressions) { + static ParameterBinder createCriteriaBinder(JpaParameters parameters, List> metadata) { - List setters = createParameterSetters( // - getParameterBindings(parameters), // - null, // - new CriteriaQueryParameterSetterFactory(parameters, expressions) // - ); + Assert.notNull(parameters, "JpaParameters must not be null!"); + Assert.notNull(metadata, "Parameter metadata must not be null!"); - return new ParameterBinder(parameters, setters); + QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forCriteriaQuery(parameters, metadata); + List bindings = getBindings(parameters); + + return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); } /** - * create a {@link ParameterBinder} that just matches parameter by name if those are available, or by index/position + * Creates a {@link ParameterBinder} that just matches parameter by name if those are available, or by index/position * otherwise. The resulting {@link ParameterBinder} can also handle SpEL expressions in the query. Uses the supplied * query in order to ensure that all query parameters are bound. * - * @param parameters method parameters that are available for binding + * @param parameters method parameters that are available for binding, must not be {@literal null}. + * @param query the {@link StringQuery} the binders shall be created for, must not be {@literal null}. + * @param parser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. * @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a * {@link javax.persistence.Query} while processing SpEL expressions where applicable. */ - static ParameterBinder createQueryAwareParameterBinder(JpaParameters parameters, StringQuery query, - EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { + static ParameterBinder createQueryAwareBinder(JpaParameters parameters, StringQuery query, + SpelExpressionParser parser, EvaluationContextProvider evaluationContextProvider) { - List parameterSetters = createParameterSetters( // - query.getParameterBindings(), // - query.getQueryString(), // - new ExpressionBasedQueryParameterSetterFactory(evaluationContextProvider, parser, parameters), // - new BasicQueryParameterSetterFactory(parameters) // - ); + Assert.notNull(parameters, "JpaParameters must not be null!"); + Assert.notNull(query, "StringQuery must not be null!"); + Assert.notNull(parser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); - return new ParameterBinder(parameters, parameterSetters); + List bindings = query.getParameterBindings(); + QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser, + evaluationContextProvider, parameters); + QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters); + + return new ParameterBinder(parameters, + createSetters(query.getQueryString(), bindings, expressionSetterFactory, basicSetterFactory)); } - private static List getParameterBindings(JpaParameters parameters) { + private static List getBindings(JpaParameters parameters) { List result = new ArrayList<>(); int bindableParameterIndex = 0; @@ -111,24 +118,26 @@ class ParameterBinderFactory { return result; } - private static List createParameterSetters(List parameterBindings, - String queryString, QueryParameterSetterFactory... strategies) { + private static Iterable createSetters(List parameterBindings, + QueryParameterSetterFactory... factories) { + return createSetters(null, parameterBindings, factories); + } + + private static Iterable createSetters(String queryString, + List parameterBindings, QueryParameterSetterFactory... strategies) { return parameterBindings.stream() // - .map((pb) -> createQueryParameterSetter(pb, strategies, queryString)) // - .collect(Collectors.toList()); + .map(it -> createQueryParameterSetter(it, strategies, queryString)) // + .collect(StreamUtils.toUnmodifiableList()); } private static QueryParameterSetter createQueryParameterSetter(ParameterBinding binding, QueryParameterSetterFactory[] strategies, String queryString) { - for (QueryParameterSetterFactory strategy : strategies) { - QueryParameterSetter candidate = strategy.create(binding, queryString); - if (candidate != null) - return candidate; - } - - return QueryParameterSetter.NOOP; + return Arrays.stream(strategies)// + .map(it -> it.create(binding, queryString))// + .filter(it -> it != null)// + .findFirst()// + .orElse(QueryParameterSetter.NOOP); } - } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index 122046282..d199bef94 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -233,7 +233,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { } private ParameterBinder getBinder(List> expressions) { - return ParameterBinderFactory.createCriteriaParameterBinder(parameters, expressions); + return ParameterBinderFactory.createCriteriaBinder(parameters, expressions); } private Sort getDynamicSort(Object[] values) { diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java index 39fdc9593..cccb49d45 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java @@ -23,11 +23,7 @@ import javax.persistence.Query; import javax.persistence.TemporalType; import javax.persistence.criteria.ParameterExpression; -import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; - -import lombok.RequiredArgsConstructor; -import lombok.Value; +import org.springframework.util.Assert; /** * The interface encapsulates the setting of query parameters which might use a significant number of variations of @@ -43,136 +39,80 @@ interface QueryParameterSetter { /** Noop implementation */ QueryParameterSetter NOOP = (query, values) -> {}; - /** - * 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 valueExtractor function that converts the list of all method parameters to the value used for setting the - * query parameter. - * @param binding the binding of the query parameter to set. - * @return QueryParameterSetter that can set the appropriate query parameter given a list of values for the method - * parameters. - */ - static QueryParameterSetter createLenient(Function valueExtractor, ParameterBinding binding) { - return create(valueExtractor, binding, null, true); - } - - /** - * Creates a {@link QueryParameterSetter} which uses information based on the {@link JpaParameter} passed as an - * argument in order to fine tune the way the parameter is set. - * - * @param valueExtractor function that converts the list of all method parameters to the value used for setting the - * query parameter. - * @param binding The binding of the query parameter to set. - * @param parameter The name of this method parameter is used for setting the query parameter and also TemporalType - * annotations on that parameter is used when setting the query parameter. - * @return QueryParameterSetter that can set the appropriate query parameter given a list of values for the method - * parameters. - */ - static QueryParameterSetter create(Function valueExtractor, ParameterBinding binding, - JpaParameter parameter) { - return create(valueExtractor, binding, parameter, false); - } - - static String getName(JpaParameter parameter, ParameterBinding binding) { - - if (parameter != null) - return parameter.isNamedParameter() - ? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name!")) - : null; - - return binding.getName(); - } - - /** - * Creates a {@link QueryParameterSetter} from a {@link JpaParameter}. Handles named and indexed parameters, - * TemporalType annotations and might ignore certain exception when requested to do so. - * - * @param valueExtractor extracts the relevant value from an array of method parameter values. - * @param binding the binding of the query parameter to be set. - * @param parameter the method parameter to bind. - * @param lenient when true certain exceptions thrown when setting the query parameters get ignored. - */ - static QueryParameterSetter create(Function valueExtractor, ParameterBinding binding, - JpaParameter parameter, boolean lenient) { - - TemporalType temporalType = parameter != null && parameter.isTemporalParameter() ? parameter.getTemporalType() - : null; - - return new NamedOrIndexedQueryParameterSetter(valueExtractor.andThen(binding::prepare), - createParameter(binding, parameter), temporalType, lenient); - } - - static Parameter createParameter(ParameterBinding binding, JpaParameter parameter) { - return new ParameterImpl(parameter, binding); - } - /** * {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified. */ - @Value class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter { private final Function valueExtractor; - private final Parameter parameter; + private final Parameter parameter; private final TemporalType temporalType; private final boolean lenient; + /** + * @param valueExtractor must not be {@literal null}. + * @param parameter must not be {@literal null}. + * @param temporalType must not be {@literal null}. + * @param lenient must not be {@literal null}. + */ + NamedOrIndexedQueryParameterSetter(Function valueExtractor, Parameter parameter, + TemporalType temporalType, boolean lenient) { + + Assert.notNull(valueExtractor, "ValueExtractor must not be null!"); + + this.valueExtractor = valueExtractor; + this.parameter = parameter; + this.temporalType = temporalType; + this.lenient = lenient; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.QueryParameterSetter#setParameter(javax.persistence.Query, java.lang.Object[]) + */ @SuppressWarnings("unchecked") public void setParameter(Query query, Object[] values) { Object value = valueExtractor.apply(values); try { + if (temporalType != null) { - // one would think we can simply use parameter to identify the parameter we want to set. + + // One would think we can simply use parameter to identify the parameter we want to set. // But that does not work with list valued parameters. At least Hibernate tries to bind them by name. // TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is // fixed. + if (parameter instanceof ParameterExpression) { - query.setParameter(parameter, (Date) value, temporalType); + query.setParameter((Parameter) parameter, (Date) value, temporalType); } else if (parameter.getName() != null && QueryUtils.hasNamedParameter(query)) { query.setParameter(parameter.getName(), (Date) value, temporalType); } else { query.setParameter(parameter.getPosition(), (Date) value, temporalType); } + } else { if (parameter instanceof ParameterExpression) { - query.setParameter(parameter, value); + query.setParameter((Parameter) parameter, value); } else if (parameter.getName() != null && QueryUtils.hasNamedParameter(query)) { query.setParameter(parameter.getName(), value); } else { query.setParameter(parameter.getPosition(), value); } } - } catch (IllegalArgumentException iae) { + } catch (IllegalArgumentException o_O) { + if (!lenient) { - throw iae; + throw o_O; } - // Since Eclipse doesn't reliably report whether a query has parameters + + // Since EclipseLink doesn't reliably report whether a query has parameters // we simply try to set the parameters and ignore possible failures. // this is relevant for queries with SpEL expressions, where the method parameters don't have to match the // parameters in the query. } } } - - @Value - @RequiredArgsConstructor - class ParameterImpl implements Parameter { - - private final String name; - private final Integer position; - private final Class parameterType; - - ParameterImpl(JpaParameter parameter, ParameterBinding binding) { - - this( // - QueryParameterSetter.getName(parameter, binding), // - binding.getPosition(), // - parameter == null ? Object.class : parameter.getType()); // - } - } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index a2cf9e927..9155ae88d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -26,56 +26,151 @@ import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.P import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter; import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.Parameter; 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 lombok.RequiredArgsConstructor; +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 + * @author Oliver Gierke * @since 2.0 */ -interface QueryParameterSetterFactory { +abstract class QueryParameterSetterFactory { - QueryParameterSetter create(ParameterBinding binding, String queryString); + abstract QueryParameterSetter create(ParameterBinding binding, String queryString); + + /** + * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. + * + * @param parameters must not be {@literal null}. + * @return + */ + public static QueryParameterSetterFactory basic(JpaParameters parameters) { + + Assert.notNull(parameters, "JpaParameters must not be null!"); + + return new BasicQueryParameterSetterFactory(parameters); + } + + /** + * Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters} and + * {@link ParameterMetadata}. + * + * @param parameters must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @return + */ + public static QueryParameterSetterFactory forCriteriaQuery(JpaParameters parameters, + List> metadata) { + + Assert.notNull(parameters, "JpaParameters must not be null!"); + Assert.notNull(metadata, "ParameterMetadata must not be null!"); + + return new CriteriaQueryParameterSetterFactory(parameters, metadata); + } + + /** + * Creates a new {@link QueryParameterSetterFactory} for the given {@link SpelExpressionParser}, + * {@link EvaluationContextProvider} and {@link Parameters}. + * + * @param parser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @return + */ + public static QueryParameterSetterFactory parsing(SpelExpressionParser parser, + EvaluationContextProvider evaluationContextProvider, Parameters parameters) { + + Assert.notNull(parser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + + return new ExpressionBasedQueryParameterSetterFactory(parser, evaluationContextProvider, parameters); + } + + /** + * Creates a {@link QueryParameterSetter} from a {@link JpaParameter}. Handles named and indexed parameters, + * TemporalType annotations and might ignore certain exception when requested to do so. + * + * @param valueExtractor extracts the relevant value from an array of method parameter values. + * @param binding the binding of the query parameter to be set. + * @param parameter the method parameter to bind. + * @param lenient when true certain exceptions thrown when setting the query parameters get ignored. + */ + private static QueryParameterSetter createSetter(Function valueExtractor, ParameterBinding binding, + JpaParameter parameter, boolean lenient) { + + TemporalType temporalType = parameter != null && parameter.isTemporalParameter() // + ? parameter.getTemporalType() // + : null; + + return new NamedOrIndexedQueryParameterSetter(valueExtractor.andThen(binding::prepare), + ParameterImpl.of(parameter, binding), temporalType, lenient); + } /** * Handles bindings that are SpEL expressions by evaluating the expression to obtain a value. * * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 */ - @RequiredArgsConstructor - class ExpressionBasedQueryParameterSetterFactory implements QueryParameterSetterFactory { + private static class ExpressionBasedQueryParameterSetterFactory extends QueryParameterSetterFactory { - private final EvaluationContextProvider evaluationContextProvider; private final SpelExpressionParser parser; + private final EvaluationContextProvider evaluationContextProvider; private final Parameters parameters; + /** + * @param parser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + * @param parameters must not be {@literal null}. + */ + ExpressionBasedQueryParameterSetterFactory(SpelExpressionParser parser, + EvaluationContextProvider evaluationContextProvider, Parameters parameters) { + + Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); + Assert.notNull(parser, "SpelExpressionParser must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + + this.evaluationContextProvider = evaluationContextProvider; + this.parser = parser; + this.parameters = parameters; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.QueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, java.lang.String) + */ @Override public QueryParameterSetter create(ParameterBinding binding, String queryString) { - if (binding.isExpression()) { - - Expression expr = parseExpressionString(binding.getExpression()); - Function valueExtractor = vs -> { - - EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, vs); - return expr.getValue(context, Object.class); - }; - - return QueryParameterSetter.createLenient(valueExtractor, binding); + if (!binding.isExpression()) { + return null; } - return null; + Expression expression = parser.parseExpression(binding.getExpression()); + + return createSetter(values -> evaluateExpression(expression, values), binding, null, true); } - private Expression parseExpressionString(String expressionString) { - return parser.parseExpression(expressionString); + /** + * Evaluates the given {@link Expression} against the given values. + * + * @param expression must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + private Object evaluateExpression(Expression expression, Object[] values) { + + EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, values); + return expression.getValue(context, Object.class); } } @@ -83,44 +178,48 @@ interface QueryParameterSetterFactory { * Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters. * * @author Jens Schauder + * @author Oliver Gierke * @since 2.0 */ - @RequiredArgsConstructor - class BasicQueryParameterSetterFactory implements QueryParameterSetterFactory { + private static class BasicQueryParameterSetterFactory extends QueryParameterSetterFactory { private final JpaParameters parameters; + /** + * @param parameters must not be {@literal null}. + */ + BasicQueryParameterSetterFactory(JpaParameters parameters) { + + Assert.notNull(parameters, "JpaParameters must not be null!"); + + this.parameters = parameters; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.QueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, java.lang.String) + */ @Override public QueryParameterSetter create(ParameterBinding binding, String queryString) { JpaParameter parameter = QueryUtils.hasNamedParameter(queryString) // - ? findParameterForBinding(binding) + ? findParameterForBinding(binding) // : parameters.getBindableParameter(binding.getPosition() - 1); - if (parameter == null) { - return QueryParameterSetter.NOOP; - } - - Function valueExtractor = values -> { - - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(parameters, values); - - return accessor.getValue(parameter); - }; - - return QueryParameterSetter.create(valueExtractor, binding, parameter); + return parameter == null // + ? QueryParameterSetter.NOOP // + : createSetter(values -> getValue(values, parameter), binding, parameter, false); } private JpaParameter findParameterForBinding(ParameterBinding binding) { - for (JpaParameter methodParameterCandidate : parameters.getBindableParameters()) { + return parameters.getBindableParameters().stream() // + .filter(candidate -> binding.getName().equals(getName(candidate))) // + .findFirst().orElse(null); + } - if (binding.getName().equals(getName(methodParameterCandidate))) { - return methodParameterCandidate; - } - } - - return null; + private Object getValue(Object[] values, Parameter parameter) { + return new JpaParametersParameterAccessor(parameters, values).getValue(parameter); } private static String getName(JpaParameter p) { @@ -128,12 +227,37 @@ interface QueryParameterSetterFactory { } } - @RequiredArgsConstructor - class CriteriaQueryParameterSetterFactory implements QueryParameterSetterFactory { + /** + * {@link QueryParameterSetterFactory} + * + * @author Jens Schauder + * @author Oliver Gierke + */ + private static class CriteriaQueryParameterSetterFactory extends QueryParameterSetterFactory { private final JpaParameters parameters; private final List> expressions; + /** + * Creates a new {@link QueryParameterSetterFactory} from the given {@link JpaParameters} and + * {@link ParameterMetadata}. + * + * @param parameters must not be {@literal null}. + * @param metadata must not be {@literal null}. + */ + CriteriaQueryParameterSetterFactory(JpaParameters parameters, List> metadata) { + + Assert.notNull(parameters, "JpaParameters must not be null!"); + Assert.notNull(metadata, "Expressions must not be null!"); + + this.parameters = parameters; + this.expressions = metadata; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.QueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, java.lang.String) + */ @Override public QueryParameterSetter create(ParameterBinding binding, String queryString) { @@ -144,17 +268,90 @@ interface QueryParameterSetterFactory { } JpaParameter parameter = parameters.getBindableParameter(binding.getPosition() - 1); + TemporalType temporalType = parameter.isTemporalParameter() ? parameter.getTemporalType() : null; - Function valueExtractor = vs -> { + return new NamedOrIndexedQueryParameterSetter(values -> getAndPrepare(parameter, metadata, values), + metadata.getExpression(), temporalType, false); + } - Object value = new JpaParametersParameterAccessor(parameters, vs).getValue(parameter); - return metadata.prepare(value); - }; + private Object getAndPrepare(JpaParameter parameter, ParameterMetadata metadata, Object[] values) { - TemporalType temporalTypeOrNull = parameter.isTemporalParameter() ? parameter.getTemporalType() : null; + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(parameters, values); - return new NamedOrIndexedQueryParameterSetter(valueExtractor, metadata.getExpression(), temporalTypeOrNull, - false); + return metadata.prepare(accessor.getValue(parameter)); + } + } + + private static class ParameterImpl implements javax.persistence.Parameter { + + private final Class parameterType; + private final String name; + private final 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 + */ + static javax.persistence.Parameter of(JpaParameter parameter, ParameterBinding binding) { + + Class type = parameter == null ? Object.class : parameter.getType(); + + return new ParameterImpl<>(type, getName(parameter, binding), binding.getPosition()); + } + + /** + * Creates a new {@link ParameterImpl} for the given name, position and parameter type. + * + * @param parameterType must not be {@literal null}. + * @param name can be {@literal null}. + * @param position can be {@literal null}. + */ + private ParameterImpl(Class parameterType, String name, Integer position) { + + this.name = name; + this.position = position; + this.parameterType = parameterType; + } + + /* + * (non-Javadoc) + * @see javax.persistence.Parameter#getName() + */ + @Override + public String getName() { + return name; + } + + /* + * (non-Javadoc) + * @see javax.persistence.Parameter#getPosition() + */ + @Override + public Integer getPosition() { + return position; + } + + /* + * (non-Javadoc) + * @see javax.persistence.Parameter#getParameterType() + */ + @Override + public Class getParameterType() { + return parameterType; + } + + private static String getName(JpaParameter parameter, ParameterBinding binding) { + + if (parameter == null) { + return binding.getName(); + } + + return parameter.isNamedParameter() // + ? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name!")) // + : null; } } } diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/Site.java b/src/test/java/org/springframework/data/jpa/domain/sample/Site.java index 743c50a67..5b40090cf 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/Site.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/Site.java @@ -15,10 +15,6 @@ */ package org.springframework.data.jpa.domain.sample; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @@ -26,17 +22,25 @@ import javax.persistence.Table; /** * @author Mark Paluch + * @author Oliver Gierke * @see Final JPA 2.1 * Specification 2.4.1.3 Derived Identities Example 2 */ -@Data @Entity @Table -@AllArgsConstructor -@NoArgsConstructor public class Site implements java.io.Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue Integer id; + + public Site() {} + + public Site(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 579d2946e..0997ca794 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -54,6 +54,8 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher; +import org.springframework.data.domain.ExampleMatcher.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -62,7 +64,6 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; -import org.springframework.data.domain.ExampleMatcher.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; @@ -1536,7 +1537,7 @@ public class UserRepositoryTests { assertThat(users.getContent()).hasSize(2).containsExactly(thirdUser, fourthUser); } - + @Test // DATAJPA-1140 public void shouldFindUsersByUserFirstnameAsSpELExpressionAndLastnameAsStringInStringBasedQuery() { @@ -1547,7 +1548,7 @@ public class UserRepositoryTests { assertThat(users).containsOnly(firstUser); } - + @Test // DATAJPA-1140 public void shouldFindUsersByFirstnameAsStringAndUserLastnameAsSpELExpressionInStringBasedQuery() { @@ -1569,7 +1570,7 @@ public class UserRepositoryTests { assertThat(users).containsOnly(firstUser); } - + @Test // DATAJPA-1140 public void shouldFindUsersByFirstnameAsFakeSpELExpressionAndUserLastnameAsSpELExpressionInStringBasedQuery() { diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java index c19e2e65d..26fefe61a 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java @@ -99,7 +99,7 @@ public class ParameterBinderUnitTests { Method validWithSort = SampleRepository.class.getMethod("validWithSort", String.class, Sort.class); Object[] values = { "foo", null }; - ParameterBinderFactory.createParameterBinder(new JpaParameters(validWithSort)).bind(query, values); + ParameterBinderFactory.createBinder(new JpaParameters(validWithSort)).bind(query, values); verify(query).setParameter(eq(1), eq("foo")); } @@ -109,7 +109,7 @@ public class ParameterBinderUnitTests { Method validWithPageable = SampleRepository.class.getMethod("validWithPageable", String.class, Pageable.class); Object[] values = { "foo", null }; - ParameterBinderFactory.createParameterBinder(new JpaParameters(validWithPageable)).bind(query, values); + ParameterBinderFactory.createBinder(new JpaParameters(validWithPageable)).bind(query, values); verify(query).setParameter(eq(1), eq("foo")); } @@ -117,7 +117,7 @@ public class ParameterBinderUnitTests { public void usesIndexedParametersIfNoParamAnnotationPresent() throws Exception { Object[] values = { "foo" }; - ParameterBinderFactory.createParameterBinder(new JpaParameters(useIndexedParameters)).bind(query, values); + ParameterBinderFactory.createBinder(new JpaParameters(useIndexedParameters)).bind(query, values); verify(query).setParameter(eq(1), anyObject()); } @@ -132,7 +132,7 @@ public class ParameterBinderUnitTests { when(query.getParameters()).thenReturn(singleton(parameter)); Object[] values = { "foo" }; - ParameterBinderFactory.createParameterBinder(new JpaParameters(valid)).bind(query, values); + ParameterBinderFactory.createBinder(new JpaParameters(valid)).bind(query, values); verify(query).setParameter(eq("username"), anyObject()); } @@ -145,7 +145,7 @@ public class ParameterBinderUnitTests { SampleEmbeddable embeddable = new SampleEmbeddable(); Object[] values = { embeddable }; - ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); + ParameterBinderFactory.createBinder(parameters).bind(query, values); verify(query).setParameter(1, embeddable); } @@ -158,7 +158,7 @@ public class ParameterBinderUnitTests { Date date = new Date(); Object[] values = { date }; - ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); + ParameterBinderFactory.createBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq(date), eq(TemporalType.DATE)); } @@ -171,7 +171,7 @@ public class ParameterBinderUnitTests { Date date = new Date(); Object[] values = { date }; - ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); + ParameterBinderFactory.createBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq(date), eq(TemporalType.TIMESTAMP)); } @@ -182,7 +182,7 @@ public class ParameterBinderUnitTests { Method method = SampleRepository.class.getMethod("invalidWithTemporalTypeParameter", String.class); JpaParameters parameters = new JpaParameters(method); - ParameterBinderFactory.createParameterBinder(parameters); + ParameterBinderFactory.createBinder(parameters); } @Test // DATAJPA-461 @@ -192,7 +192,7 @@ public class ParameterBinderUnitTests { JpaParameters parameters = new JpaParameters(method); Integer[] ids = new Integer[] { 1, 2, 3 }; Object[] values = { ids }; - ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); + ParameterBinderFactory.createBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq(ids)); } @@ -204,7 +204,7 @@ public class ParameterBinderUnitTests { JpaParameters parameters = new JpaParameters(method); Object[] values = { Optional.of("Foo") }; - ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); + ParameterBinderFactory.createBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq("Foo")); }