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 0f9a46853..980c2b26c 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 @@ -41,6 +41,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.Lazy; import org.springframework.util.Assert; /** @@ -50,6 +51,7 @@ import org.springframework.util.Assert; * @author Thomas Darimont * @author Mark Paluch * @author Christoph Strobl + * @author Jens Schauder */ public abstract class AbstractJpaQuery implements RepositoryQuery { @@ -57,6 +59,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { private final EntityManager em; private final JpaMetamodel metamodel; + Lazy parameterBinder = new Lazy<>(this::createBinder); + /** * Creates a new {@link AbstractJpaQuery} from the given {@link JpaQueryMethod}. * @@ -183,8 +187,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { return lockModeType == null ? query : query.setLockMode(lockModeType); } - protected ParameterBinder createBinder(Object[] values) { - return new ParameterBinder(getQueryMethod().getParameters(), values); + protected ParameterBinder createBinder() { + return ParameterBinderFactory.createParameterBinder(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 fcf757f29..600b49091 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 @@ -79,7 +79,9 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { Query query = createJpaQuery(sortedQueryString); - return createBinder(values).bindAndPrepare(query); + // it is ok to reuse the binding contained in the ParameterBinder although we create a new query String because the + // parameters in the query do not change. + return parameterBinder.get().bindAndPrepare(query, values); } /* @@ -87,8 +89,9 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createBinder(java.lang.Object[]) */ @Override - protected ParameterBinder createBinder(Object[] values) { - return new QueryAwareParameterBinder(getQueryMethod().getParameters(), values, query, + protected ParameterBinder createBinder() { + + return ParameterBinderFactory.createQueryAwareParameterBinder(getQueryMethod().getParameters(), query, evaluationContextProvider, parser); } @@ -102,8 +105,9 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { String queryString = countQuery.getQueryString(); EntityManager em = getEntityManager(); - return createBinder(values).bind( - getQueryMethod().isNativeQuery() ? em.createNativeQuery(queryString) : em.createQuery(queryString, Long.class)); + return parameterBinder.get().bind( + getQueryMethod().isNativeQuery() ? em.createNativeQuery(queryString) : em.createQuery(queryString, Long.class), + values); } /** @@ -123,9 +127,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { /** * Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery} * type. - * - * @param queryString - * @return */ protected Query createJpaQuery(String queryString) { @@ -141,6 +142,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { getMetamodel().isJpaManaged(returnedType.getReturnedType()); return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) - ? em.createQuery(queryString, Tuple.class) : em.createQuery(queryString); + ? em.createQuery(queryString, Tuple.class) + : em.createQuery(queryString); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java deleted file mode 100644 index 99de54c1a..000000000 --- a/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2011-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.Iterator; - -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; - -/** - * Special {@link ParameterBinder} to bind {@link CriteriaQuery} parameters. parameters. - * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Mark Paluch - * @author Jens Schauder - */ -class CriteriaQueryParameterBinder extends ParameterBinder { - - private final Iterator> expressions; - - /** - * Creates a new {@link CriteriaQueryParameterBinder} for the given {@link Parameters}, values and some - * {@link javax.persistence.criteria.ParameterExpression}. - * - * @param parameters must not be {@literal null}. - * @param values must not be {@literal null}. - * @param expressions must not be {@literal null}. - */ - CriteriaQueryParameterBinder(JpaParameters parameters, Object[] values, Iterable> expressions) { - - super(parameters, values); - - Assert.notNull(expressions, "Iterable of ParameterMetadata must not be null!"); - this.expressions = expressions.iterator(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query, org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter, java.lang.Object, int) - */ - @Override - @SuppressWarnings("unchecked") - protected QueryParameterSetter bind(JpaParameter parameter, Object value, Integer position) { - - ParameterMetadata metadata = (ParameterMetadata) expressions.next(); - - if (metadata.isIsNullParameter()) { - return QueryParameterSetter.NOOP; - } - - if (parameter.isTemporalParameter()) { - return new ParameterExpressionQueryParameterSetter(metadata.getExpression(), parameter.getTemporalType(), metadata.prepare(value)); - } - - return new ParameterExpressionQueryParameterSetter(metadata.getExpression(), null, metadata.prepare(value)); - } -} 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 new file mode 100644 index 000000000..00f412925 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java @@ -0,0 +1,43 @@ +/* + * 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 org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; + +/** + * {@link org.springframework.data.repository.query.ParameterAccessor} based on an {@link Parameters} instance. It also + * offers access to all the values, not just the bindable ones based on a {@link JpaParameter} instance. + * + * @author Jens Schauder + */ +public class JpaParametersParameterAccessor extends ParametersParameterAccessor { + + /** + * Creates a new {@link ParametersParameterAccessor}. + * + * @param parameters must not be {@literal null}. + * @param values must not be {@literal null}. + */ + JpaParametersParameterAccessor(Parameters parameters, Object[] values) { + super(parameters, values); + } + + public T getValue(JpaParameter parameter) { + return super.getValue(parameter.getIndex()); + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index 230e52fa1..860c6aed7 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -21,7 +21,6 @@ import javax.persistence.TypedQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryCreationException; @@ -140,7 +139,7 @@ final class NamedQuery extends AbstractJpaQuery { protected Query doCreateQuery(Object[] values) { Query query = getEntityManager().createNamedQuery(queryName); - return createBinder(values).bindAndPrepare(query); + return parameterBinder.get().bindAndPrepare(query, values); } /* @@ -161,6 +160,6 @@ final class NamedQuery extends AbstractJpaQuery { countQuery = em.createQuery(QueryUtils.createCountQueryFor(queryString, countProjection), Long.class); } - return createBinder(values).bind(countQuery); + return parameterBinder.get().bind(countQuery, values); } } 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 b1401cf7c..f2385b7dd 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,16 +15,15 @@ */ package org.springframework.data.jpa.repository.query; +import java.util.List; + import javax.persistence.Query; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; -import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.Parameters; 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. @@ -35,113 +34,43 @@ import org.springframework.util.Assert; * @author Christoph Strobl * @author Jens Schauder */ +@RequiredArgsConstructor public class ParameterBinder { - /** Meta information for the method parameters */ private final JpaParameters parameters; - protected final ParameterAccessor accessor; + private final List parameterSetters; - /** - * Creates a new {@link ParameterBinder}. - * - * @param parameters 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) { + public T bind(T jpaQuery, Object[] values) { - Assert.notNull(parameters, "JpaParameters must not be null!"); - Assert.notNull(values, "Values must not be null!"); - - Assert.isTrue(parameters.getNumberOfParameters() == values.length, "Invalid number of parameters given!"); - - this.parameters = parameters; - this.accessor = new ParametersParameterAccessor(parameters, values.clone()); - } - - ParameterBinder(JpaParameters parameters) { - this(parameters, new Object[0]); - } - - /** - * Returns the {@link Pageable} of the parameters, if available. Returns {@code null} otherwise. - */ - 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. - */ - Sort getSort() { - return accessor.getSort(); - } - - /** - * Binds the parameters to the given {@link Query}. - * - * @param query must not be {@literal null}. - */ - protected T bind(T query) { - - Assert.notNull(query, "Query must not be null!"); - - int bindableParameterIndex = 0; - - for (JpaParameter parameter : parameters) { - - if (parameter.isBindable()) { - - Object value = accessor.getBindableValue(bindableParameterIndex); - - bind(parameter, value, bindableParameterIndex + 1).setParameter(query); - - bindableParameterIndex++; - } + for (QueryParameterSetter setter : parameterSetters) { + setter.setParameter(jpaQuery, values); } - return query; - } - - /** - * 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 QueryParameterSetter bind(JpaParameter parameter, Object value, Integer position) { - - return QueryParameterSetter.fromJpaParameter(parameter, position, value); + return jpaQuery; } /** * Binds the parameters to the given query and applies special parameter types (e.g. pagination). - * + * * @param query must not be {@literal null}. + * @param values values of method parameters to be assigned to the query parame */ - Query bindAndPrepare(Query query) { + Query bindAndPrepare(Query query, Object[] values) { Assert.notNull(query, "Query must not be null!"); - return bindAndPrepare(query, parameters); - } + ParametersParameterAccessor accessor = new ParametersParameterAccessor(parameters, values); - private Query bindAndPrepare(Query query, Parameters parameters) { + Query result = bind(query, values); - Query result = bind(query); - - if (!parameters.hasPageableParameter() || getPageable().isUnpaged()) { + if (!parameters.hasPageableParameter() || accessor.getPageable().isUnpaged()) { return result; } - result.setFirstResult((int) getPageable().getOffset()); - result.setMaxResults(getPageable().getPageSize()); + result.setFirstResult((int) accessor.getPageable().getOffset()); + result.setMaxResults(accessor.getPageable().getPageSize()); 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 new file mode 100644 index 000000000..d4547c6b6 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -0,0 +1,134 @@ +/* + * 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.ArrayList; +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.expression.spel.standard.SpelExpressionParser; + +/** + * Factory for differently configured {@link ParameterBinder}. + * + * @author Jens Schauder + */ +class ParameterBinderFactory { + + /** + * 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 + * @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) { + + List setters = createParameterSetters( // + getParameterBindings(parameters), // + null, // + new BasicQueryParameterSetterFactory(parameters) // + ); + + return new ParameterBinder(parameters, setters); + } + + /** + * create a {@link ParameterBinder} that just matches method parameter to parameters of a + * javax.{@link javax.persistence.criteria.CriteriaQuery}. + * + * @param parameters method parameters that are available for binding + * @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) { + + List setters = createParameterSetters( // + getParameterBindings(parameters), // + null, // + new CriteriaQueryParameterSetterFactory(parameters, expressions) // + ); + + return new ParameterBinder(parameters, setters); + } + + /** + * create 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 + * @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) { + + List parameterSetters = createParameterSetters( // + query.getParameterBindings(), // + query.getQueryString(), // + new ExpressionBasedQueryParameterSetterFactory(evaluationContextProvider, parser, parameters), // + new BasicQueryParameterSetterFactory(parameters) // + ); + + return new ParameterBinder(parameters, parameterSetters); + } + + private static List getParameterBindings(JpaParameters parameters) { + + List result = new ArrayList<>(); + int bindableParameterIndex = 0; + + for (JpaParameter parameter : parameters) { + + if (parameter.isBindable()) { + result.add(new ParameterBinding(++bindableParameterIndex)); + } + } + + return result; + } + + private static List createParameterSetters(List parameterBindings, + String queryString, QueryParameterSetterFactory... strategies) { + + return parameterBindings.stream() // + .map((pb) -> createQueryParameterSetter(pb, strategies, queryString)) // + .collect(Collectors.toList()); + } + + 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; + } + +} 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 76c3959b5..122046282 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 @@ -40,10 +40,10 @@ import org.springframework.data.repository.query.parser.PartTree; * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Jens Schauder */ public class PartTreeJpaQuery extends AbstractJpaQuery { - private final Class domainClass; private final PartTree tree; private final JpaParameters parameters; @@ -58,12 +58,12 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { * @param em must not be {@literal null}. * @param persistenceProvider must not be {@literal null}. */ - public PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider) { + PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider) { super(method, em); this.em = em; - this.domainClass = method.getEntityInformation().getJavaType(); + Class domainClass = method.getEntityInformation().getJavaType(); this.tree = new PartTree(method.getName(), domainClass); this.parameters = method.getParameters(); @@ -117,48 +117,51 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { private class QueryPreparer { private final CriteriaQuery cachedCriteriaQuery; + private final ParameterBinder cachedParameterBinder; private final List> expressions; private final PersistenceProvider persistenceProvider; - public QueryPreparer(PersistenceProvider persistenceProvider, boolean recreateQueries) { + QueryPreparer(PersistenceProvider persistenceProvider, boolean recreateQueries) { this.persistenceProvider = persistenceProvider; JpaQueryCreator creator = createCreator(persistenceProvider, Optional.empty()); - this.cachedCriteriaQuery = recreateQueries ? null : creator.createQuery(); - this.expressions = recreateQueries ? null : creator.getParameterExpressions(); + if (recreateQueries) { + this.cachedCriteriaQuery = null; + this.expressions = null; + this.cachedParameterBinder = null; + } else { + this.cachedCriteriaQuery = creator.createQuery(); + this.expressions = creator.getParameterExpressions(); + this.cachedParameterBinder = getBinder(expressions); + } } /** * Creates a new {@link Query} for the given parameter values. - * - * @param values - * @return */ public Query createQuery(Object[] values) { CriteriaQuery criteriaQuery = cachedCriteriaQuery; - List> expressions = this.expressions; + ParameterBinder parameterBinder = cachedParameterBinder; ParametersParameterAccessor accessor = new ParametersParameterAccessor(parameters, values); if (cachedCriteriaQuery == null || accessor.hasBindableNullValue()) { JpaQueryCreator creator = createCreator(persistenceProvider, Optional.of(accessor)); criteriaQuery = creator.createQuery(getDynamicSort(values)); - expressions = creator.getParameterExpressions(); + List> expressions = creator.getParameterExpressions(); + parameterBinder = getBinder(expressions); } TypedQuery jpaQuery = createQuery(criteriaQuery); - return restrictMaxResultsIfNecessary(invokeBinding(getBinder(values, expressions), jpaQuery)); + return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, jpaQuery, values)); } /** * Restricts the max results of the given {@link Query} if the current {@code tree} marks this {@code query} as * limited. - * - * @param query - * @return */ private Query restrictMaxResultsIfNecessary(Query query) { @@ -192,7 +195,6 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { * of some persistence providers (i.e. Hibernate in this case), see DATAJPA-396. * * @param criteriaQuery must not be {@literal null}. - * @return */ private TypedQuery createQuery(CriteriaQuery criteriaQuery) { @@ -216,7 +218,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { .orElseGet(() -> new ParameterMetadataProvider(builder, parameters, persistenceProvider)); ResultProcessor processor = getQueryMethod().getResultProcessor(); - ReturnedType returnedType = accessor.map(it -> processor.withDynamicProjection(it))// + ReturnedType returnedType = accessor.map(processor::withDynamicProjection)// .orElse(processor).getReturnedType(); return new JpaQueryCreator(tree, returnedType, builder, provider); @@ -224,18 +226,14 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { /** * Invokes parameter binding on the given {@link TypedQuery}. - * - * @param binder - * @param query - * @return */ - protected Query invokeBinding(ParameterBinder binder, TypedQuery query) { + protected Query invokeBinding(ParameterBinder binder, TypedQuery query, Object[] values) { - return binder.bindAndPrepare(query); + return binder.bindAndPrepare(query, values); } - private ParameterBinder getBinder(Object[] values, List> expressions) { - return new CriteriaQueryParameterBinder(parameters, values, expressions); + private ParameterBinder getBinder(List> expressions) { + return ParameterBinderFactory.createCriteriaParameterBinder(parameters, expressions); } private Sort getDynamicSort(Object[] values) { @@ -253,7 +251,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { */ private class CountQueryPreparer extends QueryPreparer { - public CountQueryPreparer(PersistenceProvider persistenceProvider, boolean recreateQueries) { + CountQueryPreparer(PersistenceProvider persistenceProvider, boolean recreateQueries) { super(persistenceProvider, recreateQueries); } @@ -278,12 +276,11 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { /** * Customizes binding by skipping the pagination. * - * @see org.springframework.data.jpa.repository.query.PartTreeJpaQuery.QueryPreparer#invokeBinding(org.springframework.data.jpa.repository.query.ParameterBinder, - * javax.persistence.TypedQuery) + * @see QueryPreparer#invokeBinding(ParameterBinder, TypedQuery, Object[]) */ @Override - protected Query invokeBinding(ParameterBinder binder, javax.persistence.TypedQuery query) { - return binder.bind(query); + protected Query invokeBinding(ParameterBinder binder, TypedQuery query, Object[] values) { + return binder.bind(query, values); } } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java deleted file mode 100644 index cb5915a3d..000000000 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java +++ /dev/null @@ -1,86 +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 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 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 bind(T jpaQuery) { - - for (ParameterBinding binding : query.getParameterBindings()) { - createQueryParameterSetter(jpaQuery, binding).setParameter(jpaQuery); - } - - return jpaQuery; - } - - private QueryParameterSetter createQueryParameterSetter(T jpaQuery, ParameterBinding binding) { - - for (QueryParameterSetterStrategy strategy : queryParameterSetterStrategies) { - QueryParameterSetter candidate = strategy.create(jpaQuery, binding); - if (candidate != null) - return candidate; - } - - return QueryParameterSetter.NOOP; - } -} 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 3165d5526..39fdc9593 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 @@ -16,12 +16,18 @@ package org.springframework.data.jpa.repository.query; import java.util.Date; +import java.util.function.Function; +import javax.persistence.Parameter; 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; /** * The interface encapsulates the setting of query parameters which might use a significant number of variations of @@ -32,126 +38,141 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; */ interface QueryParameterSetter { - void setParameter(Query query); + void setParameter(Query query, Object[] values); /** 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); - } + 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 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. + * @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 lenientQueryParameterSetter(String name, Integer position, Object value) { + static QueryParameterSetter createLenient(Function valueExtractor, ParameterBinding binding) { + return create(valueExtractor, binding, null, true); + } - return (Query q) -> { - try { - if (name != null) { - q.setParameter(name, value); - } else { - q.setParameter(position, value); - } - } catch (IllegalArgumentException iae) { + /** + * 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); + } - // Since Eclipse doesn't reliably report whether a query has parameters - // we simply try to set the parameters and ignore possible failures. - } - }; + 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 boolean useParameterAccessByName; - private final String name; - private final Integer index; + private final Function valueExtractor; + private final Parameter parameter; private final TemporalType temporalType; - private final Object value; + private final boolean lenient; - /** - * @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) { + @SuppressWarnings("unchecked") + public void setParameter(Query query, Object[] values) { - this.useParameterAccessByName = name != null; - this.name = name; - this.index = index; - this.temporalType = temporalType; - this.value = value; - } + Object value = valueExtractor.apply(values); - public void setParameter(Query query) { - - if (temporalType != null) { - - if (useParameterAccessByName && QueryUtils.hasNamedParameter(query)) { - query.setParameter(name, (Date) value, temporalType); + try { + if (temporalType != null) { + // 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); + } else if (parameter.getName() != null && QueryUtils.hasNamedParameter(query)) { + query.setParameter(parameter.getName(), (Date) value, temporalType); + } else { + query.setParameter(parameter.getPosition(), (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); + if (parameter instanceof ParameterExpression) { + query.setParameter(parameter, value); + } else if (parameter.getName() != null && QueryUtils.hasNamedParameter(query)) { + query.setParameter(parameter.getName(), value); + } else { + query.setParameter(parameter.getPosition(), value); + } } + } catch (IllegalArgumentException iae) { + if (!lenient) { + throw iae; + } + // Since Eclipse 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. } } } - /** - * {@link QueryParameterSetter} identifying parameters by {@link ParameterExpression} - */ - class ParameterExpressionQueryParameterSetter implements QueryParameterSetter { + @Value + @RequiredArgsConstructor + class ParameterImpl implements Parameter { - private final ParameterExpression parameterExpression; - private final TemporalType temporalType; - private final Object value; + private final String name; + private final Integer position; + private final Class parameterType; - ParameterExpressionQueryParameterSetter(ParameterExpression parameterExpression, TemporalType temporalType, - Object value) { + ParameterImpl(JpaParameter parameter, ParameterBinding binding) { - 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); - } + 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 new file mode 100644 index 000000000..a2cf9e927 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -0,0 +1,160 @@ +/* + * 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.List; +import java.util.function.Function; + +import javax.persistence.Query; +import javax.persistence.TemporalType; + +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.ParameterBinding; +import org.springframework.data.repository.query.EvaluationContextProvider; +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; + +/** + * 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 QueryParameterSetterFactory { + + QueryParameterSetter create(ParameterBinding binding, String queryString); + + /** + * Handles bindings that are SpEL expressions by evaluating the expression to obtain a value. + * + * @author Jens Schauder + */ + @RequiredArgsConstructor + class ExpressionBasedQueryParameterSetterFactory implements QueryParameterSetterFactory { + + private final EvaluationContextProvider evaluationContextProvider; + private final SpelExpressionParser parser; + private final Parameters parameters; + + @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); + } + + return null; + } + + private Expression parseExpressionString(String expressionString) { + return parser.parseExpression(expressionString); + } + } + + /** + * Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters. + * + * @author Jens Schauder + * @since 2.0 + */ + @RequiredArgsConstructor + class BasicQueryParameterSetterFactory implements QueryParameterSetterFactory { + + private final JpaParameters parameters; + + @Override + public QueryParameterSetter create(ParameterBinding binding, String queryString) { + + JpaParameter parameter = QueryUtils.hasNamedParameter(queryString) // + ? 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); + } + + private JpaParameter findParameterForBinding(ParameterBinding binding) { + + for (JpaParameter methodParameterCandidate : parameters.getBindableParameters()) { + + if (binding.getName().equals(getName(methodParameterCandidate))) { + return methodParameterCandidate; + } + } + + return null; + } + + private static String getName(JpaParameter p) { + return p.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named!")); + } + } + + @RequiredArgsConstructor + class CriteriaQueryParameterSetterFactory implements QueryParameterSetterFactory { + + private final JpaParameters parameters; + private final List> expressions; + + @Override + public QueryParameterSetter create(ParameterBinding binding, String queryString) { + + ParameterMetadata metadata = expressions.get(binding.getPosition() - 1); + + if (metadata.isIsNullParameter()) { + return QueryParameterSetter.NOOP; + } + + JpaParameter parameter = parameters.getBindableParameter(binding.getPosition() - 1); + + Function valueExtractor = vs -> { + + Object value = new JpaParametersParameterAccessor(parameters, vs).getValue(parameter); + return metadata.prepare(value); + }; + + TemporalType temporalTypeOrNull = parameter.isTemporalParameter() ? parameter.getTemporalType() : null; + + return new NamedOrIndexedQueryParameterSetter(valueExtractor, metadata.getExpression(), temporalTypeOrNull, + false); + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java deleted file mode 100644 index 0eabe34a9..000000000 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 predicate) { - - for (JpaParameter methodParameterCandidate : parameters.getBindableParameters()) { - if (predicate.test(methodParameterCandidate)) { - return methodParameterCandidate; - } - } - - return null; - } - - private static Predicate matchByIndex(ParameterBinding binding) { - return p -> p.getIndex() + 1 == binding.getPosition(); - } - - private static Predicate matchByName(ParameterBinding binding) { - - return p -> binding.getName().equals( // - p.getName().orElseThrow( // - () -> new IllegalArgumentException("Parameter needs to be named!") // - )); - } - - } -} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java index 0713c074e..23d74c54e 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java @@ -34,6 +34,7 @@ import org.springframework.util.StringUtils; * @author Thomas Darimont * @author Oliver Gierke * @author Christoph Strobl + * @author Jens Schauder * @since 1.6 */ class StoredProcedureJpaQuery extends AbstractJpaQuery { @@ -47,7 +48,7 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { * @param method must not be {@literal null} * @param em must not be {@literal null} */ - public StoredProcedureJpaQuery(JpaQueryMethod method, EntityManager em) { + StoredProcedureJpaQuery(JpaQueryMethod method, EntityManager em) { super(method, em); this.procedureAttributes = method.getProcedureAttributes(); @@ -87,7 +88,7 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { */ @Override protected StoredProcedureQuery doCreateQuery(Object[] values) { - return createBinder(values).bind(createStoredProcedure()); + return parameterBinder.get().bind(createStoredProcedure(), values); } /* @@ -159,7 +160,9 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { } if (useNamedParameters) { - procedureQuery.registerStoredProcedureParameter(param.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named!")), param.getType(), ParameterMode.IN); + procedureQuery.registerStoredProcedureParameter( + param.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named!")), + param.getType(), ParameterMode.IN); } else { procedureQuery.registerStoredProcedureParameter(param.getIndex() + 1, param.getType(), ParameterMode.IN); } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index 9eb641f7f..5b35ef55b 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -32,14 +32,11 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Encapsulation of a JPA query String. + * 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 * - * 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 @@ -48,8 +45,6 @@ import org.springframework.util.StringUtils; */ class StringQuery { - private static final String PARAMETER_NAME_MISSING = "Name for parameter binding must not be null or empty! On JDKs < 8, you need to use @Param for named parameters, on JDK 8 or better, be sure to compile with -parameters."; - private final String query; private final List bindings; private final String alias; @@ -60,11 +55,11 @@ class StringQuery { * * @param query must not be {@literal null} or empty. */ - public StringQuery(String query) { + StringQuery(String query) { Assert.hasText(query, "Query must not be null or empty!"); - this.bindings = new ArrayList(); + this.bindings = new ArrayList<>(); this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, this.bindings); this.alias = QueryUtils.detectAlias(query); @@ -167,7 +162,7 @@ class StringQuery { * * @author Thomas Darimont */ - public static enum ParameterBindingParser { + public enum ParameterBindingParser { INSTANCE; 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 2a232cf22..534d99060 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -715,14 +715,13 @@ public class UserRepositoryTests { * Ignored until the query declaration is supported by OpenJPA. */ @Test - @Ignore public void executesAnnotatedCollectionMethodCorrectly() { flushTestUsers(); firstUser.addColleague(thirdUser); repository.save(firstUser); - List result = null; // repository.findColleaguesFor(firstUser); + List result = repository.findColleaguesFor(firstUser); assertThat(result).containsOnly(thirdUser); } @@ -1579,6 +1578,15 @@ public class UserRepositoryTests { flushTestUsers(); List users = repository.findUsersByFirstnameAsFakeSpELExpressionAndUserLastnameAsSpELExpression(firstUser.getFirstname(), firstUser); + assertThat(users).containsOnly(firstUser); + } + + @Test // DATAJPA-1140 + public void shouldFindUsersByFirstnameWithLeadingPageableParameter() { + + flushTestUsers(); + + List users = repository.findUsersByFirstnamePaginated(PageRequest.of(0, 2), firstUser.getFirstname()); assertThat(users).containsOnly(firstUser); } 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 3b6edf5d2..c19e2e65d 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 @@ -17,8 +17,6 @@ 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.anyObject; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -95,42 +93,13 @@ public class ParameterBinderUnitTests { User optionalParameter(Optional name); } - @Test(expected = IllegalArgumentException.class) - public void rejectsToManyParameters() throws Exception { - - new ParameterBinder(new JpaParameters(valid), new Object[] { "foo", "bar" }); - } - - @Test(expected = IllegalArgumentException.class) - public void rejectsNullParameters() throws Exception { - - new ParameterBinder(new JpaParameters(valid), (Object[]) null); - } - - @Test(expected = IllegalArgumentException.class) - public void rejectsToLittleParameters() throws SecurityException, NoSuchMethodException { - - JpaParameters parameters = new JpaParameters(valid); - new ParameterBinder(parameters); - } - - @Test - public void returnsPageableNoneIfNoPageableWasProvided() throws SecurityException, NoSuchMethodException { - - Method method = SampleRepository.class.getMethod("validWithPageable", String.class, Pageable.class); - - JpaParameters parameters = new JpaParameters(method); - ParameterBinder binder = new ParameterBinder(parameters, new Object[] { "foo", null }); - - assertThat(binder.getPageable(), is(Pageable.unpaged())); - } - @Test public void bindWorksWithNullForSort() throws Exception { Method validWithSort = SampleRepository.class.getMethod("validWithSort", String.class, Sort.class); - new ParameterBinder(new JpaParameters(validWithSort), new Object[] { "foo", null }).bind(query); + Object[] values = { "foo", null }; + ParameterBinderFactory.createParameterBinder(new JpaParameters(validWithSort)).bind(query, values); verify(query).setParameter(eq(1), eq("foo")); } @@ -139,18 +108,21 @@ public class ParameterBinderUnitTests { Method validWithPageable = SampleRepository.class.getMethod("validWithPageable", String.class, Pageable.class); - new ParameterBinder(new JpaParameters(validWithPageable), new Object[] { "foo", null }).bind(query); + Object[] values = { "foo", null }; + ParameterBinderFactory.createParameterBinder(new JpaParameters(validWithPageable)).bind(query, values); verify(query).setParameter(eq(1), eq("foo")); } @Test public void usesIndexedParametersIfNoParamAnnotationPresent() throws Exception { - new ParameterBinder(new JpaParameters(useIndexedParameters), new Object[] { "foo" }).bind(query); + Object[] values = { "foo" }; + ParameterBinderFactory.createParameterBinder(new JpaParameters(useIndexedParameters)).bind(query, values); verify(query).setParameter(eq(1), anyObject()); } @Test + public void usesParameterNameIfAnnotated() throws Exception { when(query.setParameter(eq("username"), anyObject())).thenReturn(query); @@ -159,7 +131,8 @@ public class ParameterBinderUnitTests { when(parameter.getName()).thenReturn("username"); when(query.getParameters()).thenReturn(singleton(parameter)); - new ParameterBinder(new JpaParameters(valid), new Object[] { "foo" }).bind(query); + Object[] values = { "foo" }; + ParameterBinderFactory.createParameterBinder(new JpaParameters(valid)).bind(query, values); verify(query).setParameter(eq("username"), anyObject()); } @@ -171,20 +144,12 @@ public class ParameterBinderUnitTests { JpaParameters parameters = new JpaParameters(method); SampleEmbeddable embeddable = new SampleEmbeddable(); - new ParameterBinder(parameters, new Object[] { embeddable }).bind(query); + Object[] values = { embeddable }; + ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); verify(query).setParameter(1, embeddable); } - @Test - public void bindsSortForIndexedParameters() throws Exception { - - Sort sort = Sort.by("name"); - ParameterBinder binder = new ParameterBinder(new JpaParameters(indexedParametersWithSort), - new Object[] { "name", sort }); - assertThat(binder.getSort(), is(sort)); - } - @Test // DATAJPA-107 public void shouldSetTemporalQueryParameterToDate() throws Exception { @@ -192,7 +157,8 @@ public class ParameterBinderUnitTests { JpaParameters parameters = new JpaParameters(method); Date date = new Date(); - new ParameterBinder(parameters, new Object[] { date }).bind(query); + Object[] values = { date }; + ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq(date), eq(TemporalType.DATE)); } @@ -204,7 +170,8 @@ public class ParameterBinderUnitTests { JpaParameters parameters = new JpaParameters(method); Date date = new Date(); - new ParameterBinder(parameters, new Object[] { date }).bind(query); + Object[] values = { date }; + ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq(date), eq(TemporalType.TIMESTAMP)); } @@ -215,7 +182,7 @@ public class ParameterBinderUnitTests { Method method = SampleRepository.class.getMethod("invalidWithTemporalTypeParameter", String.class); JpaParameters parameters = new JpaParameters(method); - new ParameterBinder(parameters, new Object[] { "foo", "" }); + ParameterBinderFactory.createParameterBinder(parameters); } @Test // DATAJPA-461 @@ -224,7 +191,8 @@ public class ParameterBinderUnitTests { Method method = SampleRepository.class.getMethod("validWithVarArgs", Integer[].class); JpaParameters parameters = new JpaParameters(method); Integer[] ids = new Integer[] { 1, 2, 3 }; - new ParameterBinder(parameters, new Object[] { ids }).bind(query); + Object[] values = { ids }; + ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq(ids)); } @@ -235,7 +203,8 @@ public class ParameterBinderUnitTests { Method method = SampleRepository.class.getMethod("optionalParameter", Optional.class); JpaParameters parameters = new JpaParameters(method); - new ParameterBinder(parameters, new Object[] { Optional.of("Foo") }).bind(query); + Object[] values = { Optional.of("Foo") }; + ParameterBinderFactory.createParameterBinder(parameters).bind(query, values); verify(query).setParameter(eq(1), eq("Foo")); } diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 326050bd6..f00a249a5 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -228,11 +228,8 @@ public interface UserRepository // DATAJPA-132 List findByActiveFalse(); - /** - * Commented out until OpenJPA supports this. - */ - // @Query("select u.colleagues from User u where u = ?1") - // List findColleaguesFor(User user); + @Query("select u.colleagues from User u where u = ?1") + List findColleaguesFor(User user); // DATAJPA-188 List findByCreatedAtBefore(Date date); @@ -430,22 +427,30 @@ public interface UserRepository value = "select * from (select rownum() as RN, u.* from SD_User u) where RN between ?#{ #pageable.offset -1} and ?#{#pageable.offset + #pageable.pageSize}", countQuery = "select count(u.id) from SD_User u", nativeQuery = true) Page findUsersInNativeQueryWithPagination(Pageable pageable); - + // DATAJPA-1140 @Query("select u from User u where u.firstname =:#{#user.firstname} and u.lastname =:lastname") - List findUsersByUserFirstnameAsSpELExpressionAndLastnameAsString(@Param("user") User user, @Param("lastname") String lastname); - + List findUsersByUserFirstnameAsSpELExpressionAndLastnameAsString(@Param("user") User user, + @Param("lastname") String lastname); + // DATAJPA-1140 @Query("select u from User u where u.firstname =:firstname and u.lastname =:#{#user.lastname}") - List findUsersByFirstnameAsStringAndUserLastnameAsSpELExpression(@Param("firstname") String firstname, @Param("user") User user); - + List findUsersByFirstnameAsStringAndUserLastnameAsSpELExpression(@Param("firstname") String firstname, + @Param("user") User user); + // DATAJPA-1140 @Query("select u from User u where u.firstname =:#{#user.firstname} and u.lastname =:#{#lastname}") - List findUsersByUserFirstnameAsSpELExpressionAndLastnameAsFakeSpELExpression(@Param("user") User user, @Param("lastname") String lastname); - + List findUsersByUserFirstnameAsSpELExpressionAndLastnameAsFakeSpELExpression(@Param("user") User user, + @Param("lastname") String lastname); + // DATAJPA-1140 @Query("select u from User u where u.firstname =:#{#firstname} and u.lastname =:#{#user.lastname}") - List findUsersByFirstnameAsFakeSpELExpressionAndUserLastnameAsSpELExpression(@Param("firstname") String firstname, @Param("user") User user); + List findUsersByFirstnameAsFakeSpELExpressionAndUserLastnameAsSpELExpression( + @Param("firstname") String firstname, @Param("user") User user); + + // DATAJPA-1140 + @Query("select u from User u where u.firstname =:firstname") + List findUsersByFirstnamePaginated(Pageable page, @Param("firstname") String firstname); // DATAJPA-629 @Query("select u from #{#entityName} u where u.firstname = ?#{[0]} and u.lastname = ?#{[1]}")