DATAJPA-564 - Support for SpEL based parameter expressions in String based queries.

We now support the discovery and dynamic registration of SpEL expression parameters in String based queries. Introduced an ExpressionEvaluationContextProvider that provides access to
a potentially shared SpEL EvaluationContext that is defined in the application context. This allow shared spring beans to be used within query expressions.
The SpEL expressions are evaluated in org.springframework.data.jpa.repository.query.SpelExpressionStringQueryParameterBinder.potentiallyBindSyntheticParameters(T) by using a hierarchal EvaluationContext with the RootObject set to the current method arguments.
We enhanced the parsing of ParameterBindings in ParameterBindingParser to support "synthetic" Parameters like SpEL expressions that should be evaluated at query time.

This feature works with Hibernate, EclipseLink as well as OpenJPA.

We currently support those variants:
Indexed parameter:
@Query("select c from Customer c where c.firstname = ?1 and c.attribute1 like ?#{[0] + ' ' + [1]})
To determine the index for the expression parameter we determine the max parameter index present and use that as an offset to generate appropriate parameter indices.

Named parameter:
@Query("select c from Customer c where c.firstname = :firstname and c.attribute1 like :#{[0] + ' ' + [1]})
whereby we generate a name like __$synthetic$__0 for SpEL parameter expression.
This commit is contained in:
Thomas Darimont
2014-06-25 01:49:18 +02:00
committed by Oliver Gierke
parent 6260e8209c
commit c4f245b11e
18 changed files with 568 additions and 67 deletions

View File

@@ -131,4 +131,6 @@ public @interface EnableJpaRepositories {
* repositories infrastructure.
*/
boolean considerNestedRepositories() default false;
String expressionEvaluationContextProviderRef() default "expressionEvaluationContextProvider";
}

View File

@@ -27,6 +27,7 @@ import javax.persistence.metamodel.Metamodel;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.AbstractFactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.FieldRetrievingFactoryBean;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@@ -37,10 +38,12 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostPro
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.StandardExpressionEvaluationContextProvider;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* JPA specific configuration extension parsing custom attributes from the XML namespace and
@@ -56,6 +59,8 @@ import org.springframework.util.Assert;
*/
public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport {
private static final String DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER = "expressionEvaluationContextProvider";
public static final String JPA_MAPPING_CONTEXT_BEAN_NAME = "jpaMapppingContext";
private static final Class<?> PAB_POST_PROCESSOR = PersistenceAnnotationBeanPostProcessor.class;
@@ -96,6 +101,11 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
}
builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME);
String expressionEvaluationContextProviderRef = source.getAttribute("expressionEvaluationContextProviderRef");
if (StringUtils.hasText(expressionEvaluationContextProviderRef)) {
builder.addPropertyReference("expressionEvaluationContextProvider", expressionEvaluationContextProviderRef);
}
}
/**
@@ -158,6 +168,14 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
registerWithSourceAndGeneratedBeanName(registry, new RootBeanDefinition(PAB_POST_PROCESSOR), source);
}
if (!registry.containsBeanDefinition(DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER)) {
registry.registerBeanDefinition(
DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER,
BeanDefinitionBuilder.rootBeanDefinition(FieldRetrievingFactoryBean.class)
.addPropertyValue("targetClass", StandardExpressionEvaluationContextProvider.class)
.addPropertyValue("targetField", "INSTANCE").getBeanDefinition());
}
}
/**

View File

@@ -19,6 +19,7 @@ import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.util.Assert;
@@ -33,6 +34,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
private final StringQuery query;
private final StringQuery countQuery;
private final ExpressionEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -41,13 +43,17 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString) {
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
ExpressionEvaluationContextProvider evaluationContextProvider) {
super(method, em);
Assert.hasText(queryString, "Query string must not be null or empty!");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!");
this.evaluationContextProvider = evaluationContextProvider;
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation());
this.countQuery = new StringQuery(method.getCountQuery() != null ? method.getCountQuery()
: QueryUtils.createCountQueryFor(this.query.getQueryString(), method.getCountQueryProjection()));
@@ -74,7 +80,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
*/
@Override
protected ParameterBinder createBinder(Object[] values) {
return new StringQueryParameterBinder(getQueryMethod().getParameters(), values, query);
return new SpelExpressionStringQueryParameterBinder(getQueryMethod().getParameters(), values, query,
evaluationContextProvider);
}
/**

View File

@@ -20,6 +20,7 @@ import javax.persistence.EntityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -40,12 +41,14 @@ enum JpaQueryFactory {
*
* @param queryMethod must not be {@literal null}.
* @param em must not be {@literal null}.
* @param evaluationContextProvider
* @return the {@link RepositoryQuery} derived from the annotation or {@code null} if no annotation found.
*/
AbstractJpaQuery fromQueryAnnotation(JpaQueryMethod queryMethod, EntityManager em) {
AbstractJpaQuery fromQueryAnnotation(JpaQueryMethod queryMethod, EntityManager em,
ExpressionEvaluationContextProvider evaluationContextProvider) {
LOG.debug("Looking up query for method {}", queryMethod.getName());
return fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery());
return fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), evaluationContextProvider);
}
/**
@@ -54,16 +57,18 @@ enum JpaQueryFactory {
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null} or empty.
* @param evaluationContextProvider
* @return
*/
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString) {
AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString,
ExpressionEvaluationContextProvider evaluationContextProvider) {
if (queryString == null) {
return null;
}
return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString) : //
new SimpleJpaQuery(method, em, queryString);
return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString, evaluationContextProvider) : //
new SimpleJpaQuery(method, em, queryString, evaluationContextProvider);
}
/**

View File

@@ -19,6 +19,7 @@ import java.lang.reflect.Method;
import javax.persistence.EntityManager;
import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -104,15 +105,19 @@ public final class JpaQueryLookupStrategy {
*/
private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {
public DeclaredQueryLookupStrategy(EntityManager em, QueryExtractor extractor) {
private final ExpressionEvaluationContextProvider evaluationContextProvider;
public DeclaredQueryLookupStrategy(EntityManager em, QueryExtractor extractor,
ExpressionEvaluationContextProvider evaluationContextProvider) {
super(em, extractor);
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em);
RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em, this.evaluationContextProvider);
if (null != query) {
return query;
@@ -126,7 +131,8 @@ public final class JpaQueryLookupStrategy {
String name = method.getNamedQueryName();
if (namedQueries.hasQuery(name)) {
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name));
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
this.evaluationContextProvider);
}
query = NamedQuery.lookupFrom(method, em);
@@ -149,21 +155,22 @@ public final class JpaQueryLookupStrategy {
*/
private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLookupStrategy {
private final DeclaredQueryLookupStrategy strategy;
private final DeclaredQueryLookupStrategy lookupStrategy;
private final CreateQueryLookupStrategy createStrategy;
public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor) {
public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor,
CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy) {
super(em, extractor);
this.strategy = new DeclaredQueryLookupStrategy(em, extractor);
this.createStrategy = new CreateQueryLookupStrategy(em, extractor);
this.createStrategy = createStrategy;
this.lookupStrategy = lookupStrategy;
}
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
try {
return strategy.resolveQuery(method, em, namedQueries);
return lookupStrategy.resolveQuery(method, em, namedQueries);
} catch (IllegalStateException e) {
return createStrategy.resolveQuery(method, em, namedQueries);
}
@@ -175,21 +182,20 @@ public final class JpaQueryLookupStrategy {
*
* @param em
* @param key
* @param evaluationContextProvider
* @return
*/
public static QueryLookupStrategy create(EntityManager em, Key key, QueryExtractor extractor) {
public static QueryLookupStrategy create(EntityManager em, Key key, QueryExtractor extractor,
ExpressionEvaluationContextProvider evaluationContextProvider) {
if (key == null) {
return new CreateIfNotFoundQueryLookupStrategy(em, extractor);
}
switch (key) {
switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
case CREATE:
return new CreateQueryLookupStrategy(em, extractor);
case USE_DECLARED_QUERY:
return new DeclaredQueryLookupStrategy(em, extractor);
return new DeclaredQueryLookupStrategy(em, extractor, evaluationContextProvider);
case CREATE_IF_NOT_FOUND:
return new CreateIfNotFoundQueryLookupStrategy(em, extractor);
return new CreateIfNotFoundQueryLookupStrategy(em, extractor, new CreateQueryLookupStrategy(em, extractor),
new DeclaredQueryLookupStrategy(em, extractor, evaluationContextProvider));
default:
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -36,10 +37,12 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null} or empty.
* @param evaluationContextProvider
*/
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString) {
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
ExpressionEvaluationContextProvider evaluationContextProvider) {
super(method, em, queryString);
super(method, em, queryString, evaluationContextProvider);
Parameters<?, ?> parameters = method.getParameters();
boolean hasPagingOrSortingParameter = parameters.hasPageableParameter() || parameters.hasSortParameter();

View File

@@ -161,4 +161,13 @@ public class ParameterBinder {
return result;
}
/**
* Returns the values to bind.
*
* @return
*/
Object[] getValues() {
return values;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
/**
@@ -36,8 +37,9 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
*/
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em) {
this(method, em, method.getAnnotatedQuery());
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em,
ExpressionEvaluationContextProvider evaluationContextProvider) {
this(method, em, method.getAnnotatedQuery(), evaluationContextProvider);
}
/**
@@ -47,9 +49,10 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null} or empty.
*/
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString) {
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
ExpressionEvaluationContextProvider evaluationContextProvider) {
super(method, em, queryString);
super(method, em, queryString, evaluationContextProvider);
validateQuery(getQuery().getQueryString(), String.format("Validation failed for query for method %s!", method));

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import java.util.List;
import javax.persistence.Query;
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeComparator;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
/**
* A {@link StringQueryParameterBinder} that is able to bind synthetic query parameters.
*
* @author Thomas Darimont
*/
class SpelExpressionStringQueryParameterBinder extends StringQueryParameterBinder {
private final StringQuery query;
private final ExpressionEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link SpelExpressionStringQueryParameterBinder}.
*
* @param parameters must not be {@literal null}
* @param values must not be {@literal null}
* @param query must not be {@literal null}
* @param evaluationContextProvider must not be {@literal null}
*/
public SpelExpressionStringQueryParameterBinder(JpaParameters parameters, Object[] values, StringQuery query,
ExpressionEvaluationContextProvider evaluationContextProvider) {
super(parameters, values, query);
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!");
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}
/* (non-Javadoc)
* @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query)
*/
@Override
public <T extends Query> T bind(T jpaQuery) {
return potentiallyBindExpressionParameters(super.bind(jpaQuery));
}
/**
* @param jpaQuery must not be {@literal null}
* @return
*/
private <T extends Query> T potentiallyBindExpressionParameters(T jpaQuery) {
for (ParameterBinding binding : query.getParameterBindings()) {
if (binding.isExpression()) {
Expression expr = new SpelExpressionParser().parseExpression(binding.getExpression());
EvaluationContext delegatee = evaluationContextProvider.getEvaluationContext();
StandardEvaluationContext evalContext = new DelegatingStandardEvaluationContext(getValues(), delegatee);
Object actualValue = expr.getValue(evalContext, String.class);
if (binding.getName() != null) {
jpaQuery.setParameter(binding.getName(), binding.prepare(actualValue));
} else {
jpaQuery.setParameter(binding.getPosition(), binding.prepare(actualValue));
}
}
}
return jpaQuery;
}
/**
* A {@link StandardEvaluationContext} that delegates to the given {@link EvaluationContext}. Variables are first
* looked-up locally and if not the lookup is performed against the delegatee.
*
* @author Thomas Darimont
*/
static class DelegatingStandardEvaluationContext extends StandardEvaluationContext {
private final EvaluationContext delegatee;
/**
* Creates a new {@link DelegatingStandardEvaluationContext}.
*
* @param values must not be {@literal null}
* @param delegatee must not be {@literal null}
*/
public DelegatingStandardEvaluationContext(Object[] values, EvaluationContext delegatee) {
super(values);
Assert.notNull(delegatee, "EvaluationContext delegatee must not be null!");
this.delegatee = delegatee;
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getConstructorResolvers()
*/
@Override
public List<ConstructorResolver> getConstructorResolvers() {
return delegatee.getConstructorResolvers();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getMethodResolvers()
*/
@Override
public List<MethodResolver> getMethodResolvers() {
return delegatee.getMethodResolvers();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getPropertyAccessors()
*/
@Override
public List<PropertyAccessor> getPropertyAccessors() {
return delegatee.getPropertyAccessors();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getTypeLocator()
*/
@Override
public TypeLocator getTypeLocator() {
return delegatee.getTypeLocator();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getTypeConverter()
*/
@Override
public TypeConverter getTypeConverter() {
return delegatee.getTypeConverter();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getTypeComparator()
*/
@Override
public TypeComparator getTypeComparator() {
return delegatee.getTypeComparator();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getOperatorOverloader()
*/
@Override
public OperatorOverloader getOperatorOverloader() {
return delegatee.getOperatorOverloader();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#getBeanResolver()
*/
@Override
public BeanResolver getBeanResolver() {
return delegatee.getBeanResolver();
}
/* (non-Javadoc)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#lookupVariable(java.lang.String)
*/
@Override
public Object lookupVariable(String name) {
Object result = super.lookupVariable(name);
if (result != null) {
return result;
}
return delegatee.lookupVariable(name);
}
}
}

View File

@@ -142,6 +142,8 @@ class StringQuery {
INSTANCE;
private static final String EXPRESSION_PREFIX = "__$synthetic$__";
private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile("\\?(\\d+)");
private static final Pattern PARAMETER_BINDING_PATTERN;
private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type! "
+ "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same binding.";
@@ -161,13 +163,15 @@ class StringQuery {
builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
builder.append(")?");
builder.append("(?: )?"); // some whitespace
builder.append("\\(?"); // optional braces around paramters
builder.append("\\(?"); // optional braces around parameters
builder.append("(");
builder.append("%?(\\?(\\d+))%?"); // position parameter
builder.append("%?(\\?(\\d+))%?"); // position parameter and parameter index
builder.append("|"); // or
builder.append("%?(:([\\p{L}\\w]+))%?"); // named parameter;
builder.append("%?(:([\\p{L}\\w]+))%?"); // named parameter and the parameter name
builder.append("|"); // or
builder.append("%?((:|\\?)#\\{([^}]+)\\})%?"); // expression parameter and expression
builder.append(")");
builder.append("\\)?"); // optional braces around paramters
builder.append("\\)?"); // optional braces around parameters
PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
}
@@ -182,8 +186,18 @@ class StringQuery {
private final String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
List<ParameterBinding> bindings) {
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(query);
String result = query;
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(query);
int greatestParameterIndex = determineGreatestParameterIndexIfPresent(query);
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
/*
* If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to
* not mix-up with the actual parameter indices.
*/
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
while (matcher.find()) {
@@ -191,30 +205,46 @@ class StringQuery {
String parameterName = parameterIndexString != null ? null : matcher.group(6);
Integer parameterIndex = parameterIndexString == null ? null : Integer.valueOf(parameterIndexString);
String typeSource = matcher.group(1);
String expression = null;
String replacement = null;
if (parameterName == null && parameterIndex == null) {
expressionParameterIndex++;
if (parametersShouldBeAccessedByIndex) {
parameterIndex = expressionParameterIndex;
replacement = "?" + parameterIndex;
} else {
parameterName = EXPRESSION_PREFIX + expressionParameterIndex;
replacement = ":" + parameterName;
}
expression = matcher.group(9);
}
switch (ParameterBindingType.of(typeSource)) {
case LIKE:
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
String replacement = matcher.group(3);
replacement = replacement != null ? replacement : matcher.group(3);
if (parameterIndex != null) {
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType), bindings);
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
} else {
checkAndRegister(new LikeParameterBinding(parameterName, likeType), bindings);
replacement = matcher.group(5);
checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
replacement = expression != null ? ":" + parameterName : matcher.group(5);
}
result = StringUtils.replace(result, matcher.group(2), replacement);
break;
case IN:
if (parameterIndex != null) {
checkAndRegister(new InParameterBinding(parameterIndex), bindings);
checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
} else {
checkAndRegister(new InParameterBinding(parameterName), bindings);
checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
}
result = query;
@@ -223,14 +253,32 @@ class StringQuery {
case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
default:
bindings.add(parameterIndex != null ? new ParameterBinding(parameterIndex) : new ParameterBinding(
parameterName));
bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
: new ParameterBinding(parameterName, null, expression));
}
if (replacement != null) {
result = StringUtils.replace(result, matcher.group(2), replacement);
}
}
return result;
}
private int determineGreatestParameterIndexIfPresent(String query) {
Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
int greatestParameterIndex = -1;
while (parameterIndexMatcher.find()) {
String parameterIndexString = parameterIndexMatcher.group(1);
greatestParameterIndex = Math.max(greatestParameterIndex, Integer.parseInt(parameterIndexString));
}
return greatestParameterIndex;
}
private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
for (ParameterBinding existing : bindings) {
@@ -304,6 +352,7 @@ class StringQuery {
static class ParameterBinding {
private final String name;
private final String expression;
private final Integer position;
/**
@@ -312,11 +361,7 @@ class StringQuery {
* @param name must not be {@literal null}.
*/
public ParameterBinding(String name) {
Assert.notNull(name, "Name must not be null!");
this.name = name;
this.position = null;
this(name, null, null);
}
/**
@@ -325,11 +370,30 @@ class StringQuery {
* @param position must not be {@literal null}.
*/
public ParameterBinding(Integer position) {
this(null, position, null);
}
Assert.notNull(position, "Position must not be null!");
/**
* Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
* information.
*
* @param name
* @param position
* @param expression
*/
ParameterBinding(String name, Integer position, String expression) {
this.name = null;
if (name == null) {
Assert.notNull(position, "Position must not be null!");
}
if (position == null) {
Assert.notNull(name, "Name must not be null!");
}
this.name = name;
this.position = position;
this.expression = expression;
}
/**
@@ -368,6 +432,13 @@ class StringQuery {
return position;
}
/**
* @return {@literal true} if this parameter binding is a synthetic SpEL expression.
*/
public boolean isExpression() {
return this.expression != null;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
@@ -379,6 +450,7 @@ class StringQuery {
result += nullSafeHashCode(this.name);
result += nullSafeHashCode(this.position);
result += nullSafeHashCode(this.expression);
return result;
}
@@ -396,7 +468,8 @@ class StringQuery {
ParameterBinding that = (ParameterBinding) obj;
return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position);
return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position)
&& nullSafeEquals(this.expression, that.expression);
}
/*
@@ -405,7 +478,8 @@ class StringQuery {
*/
@Override
public String toString() {
return String.format("ParameterBinding [name: %s, position: %d]", getName(), getPosition());
return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
getExpression());
}
/**
@@ -415,6 +489,10 @@ class StringQuery {
public Object prepare(Object valueToBind) {
return valueToBind;
}
public String getExpression() {
return expression;
}
}
/**
@@ -429,18 +507,20 @@ class StringQuery {
* Creates a new {@link InParameterBinding} for the parameter with the given name.
*
* @param name
* @param expression
*/
public InParameterBinding(String name) {
super(name);
public InParameterBinding(String name, String expression) {
super(name, null, expression);
}
/**
* Creates a new {@link InParameterBinding} for the parameter with the given position.
*
* @param position
* @param expression
*/
public InParameterBinding(int position) {
super(position);
public InParameterBinding(int position, String expression) {
super(null, position, expression);
}
/*
@@ -486,8 +566,20 @@ class StringQuery {
* @param type must not be {@literal null}.
*/
public LikeParameterBinding(String name, Type type) {
this(name, type, null);
}
super(name);
/**
* Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter
* binding input.
*
* @param name must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @param expression may be {@literal null}.
*/
public LikeParameterBinding(String name, Type type, String expression) {
super(name, null, expression);
Assert.hasText(name, "Name must not be null or empty!");
Assert.notNull(type, "Type must not be null!");
@@ -505,8 +597,19 @@ class StringQuery {
* @param type must not be {@literal null}.
*/
public LikeParameterBinding(int position, Type type) {
this(position, type, null);
}
super(position);
/**
* Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
*
* @param position
* @param type must not be {@literal null}.
* @param expression may be {@literal null}.
*/
public LikeParameterBinding(int position, Type type, String expression) {
super(null, position, expression);
Assert.isTrue(position > 0, "Position must be greater than zero!");
Assert.notNull(type, "Type must not be null!");

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import org.springframework.expression.EvaluationContext;
/**
* Provides a way to access a centrally defined potentially shared {@link EvaluationContext}.
*
* @author Thomas Darimont
*/
public interface ExpressionEvaluationContextProvider {
/**
* Returns the {@link EvaluationContext}.
*
* @return
*/
EvaluationContext getEvaluationContext();
}

View File

@@ -41,6 +41,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
private final EntityManager entityManager;
private final QueryExtractor extractor;
private final CrudMethodMetadataPostProcessor lockModePostProcessor;
private final ExpressionEvaluationContextProvider evaluationContextProvider;
/**
* Creates a new {@link JpaRepositoryFactory}.
@@ -48,12 +49,23 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
* @param entityManager must not be {@literal null}
*/
public JpaRepositoryFactory(EntityManager entityManager) {
this(entityManager, StandardExpressionEvaluationContextProvider.INSTANCE);
}
/**
* Creates a new {@link JpaRepositoryFactory}.
*
* @param entityManager must not be {@literal null}
* @param evaluationContextProvider must not be {@literal null}
*/
public JpaRepositoryFactory(EntityManager entityManager, ExpressionEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(entityManager);
this.entityManager = entityManager;
this.extractor = PersistenceProvider.fromEntityManager(entityManager);
this.lockModePostProcessor = CrudMethodMetadataPostProcessor.INSTANCE;
this.evaluationContextProvider = evaluationContextProvider;
addRepositoryProxyPostProcessor(lockModePostProcessor);
}
@@ -132,7 +144,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
@Override
protected QueryLookupStrategy getQueryLookupStrategy(Key key) {
return JpaQueryLookupStrategy.create(entityManager, key, extractor);
return JpaQueryLookupStrategy.create(entityManager, key, extractor, evaluationContextProvider);
}
/*

View File

@@ -39,6 +39,8 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends
private EntityManager entityManager;
private ExpressionEvaluationContextProvider expressionEvaluationContextProvider = StandardExpressionEvaluationContextProvider.INSTANCE;
/**
* The {@link EntityManager} to be used.
*
@@ -58,6 +60,14 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends
super.setMappingContext(mappingContext);
}
/**
* @param expressionEvaluationContextProvider the expressionEvaluationContextProvider to set
*/
public void setExpressionEvaluationContextProvider(
ExpressionEvaluationContextProvider expressionEvaluationContextProvider) {
this.expressionEvaluationContextProvider = expressionEvaluationContextProvider;
}
/*
* (non-Javadoc)
*
@@ -76,7 +86,7 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends
* @return
*/
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
return new JpaRepositoryFactory(entityManager);
return new JpaRepositoryFactory(entityManager, expressionEvaluationContextProvider);
}
/*

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Default implementation of {@link ExpressionEvaluationContextProvider} that always creates a new
* {@link EvaluationContext}.
*
* @author Thomas Darimont
*/
public enum StandardExpressionEvaluationContextProvider implements ExpressionEvaluationContextProvider {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider#getEvaluationContext()
*/
@Override
public StandardEvaluationContext getEvaluationContext() {
return new StandardEvaluationContext();
}
}

View File

@@ -1588,6 +1588,32 @@ public class UserRepositoryTests {
assertThat(result.isPresent(), is(true));
assertThat(result.get(), is(firstUser));
}
/**
* @see DATAJPA-XXX
*/
@Test
public void shouldFindUserByFirstnameAndLastnameWithSpelExpressionInStringBasedQuery() {
flushTestUsers();
List<User> users = repository.findByFirstnameAndLastnameWithSpelExpression("Oliver", "ierk");
assertThat(users, hasSize(1));
assertThat(users.get(0), is(firstUser));
}
/**
* @see DATAJPA-XXX
*/
@Test
public void shouldFindUserByLastnameWithSpelExpressionInStringBasedQuery() {
flushTestUsers();
List<User> users = repository.findByLastnameWithSpelExpression("ierk");
assertThat(users, hasSize(1));
assertThat(users.get(0), is(firstUser));
}
private Page<User> executeSpecWithSort(Sort sort) {

View File

@@ -36,6 +36,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.support.StandardExpressionEvaluationContextProvider;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -72,7 +73,8 @@ public class JpaQueryLookupStrategyUnitTests {
@Test
public void invalidAnnotatedQueryCausesException() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor);
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor,
StandardExpressionEvaluationContextProvider.INSTANCE);
Method method = UserRepository.class.getMethod("findByFoo", String.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
@@ -93,7 +95,8 @@ public class JpaQueryLookupStrategyUnitTests {
@Test
public void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor);
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor,
StandardExpressionEvaluationContextProvider.INSTANCE);
Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Pageable.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);

View File

@@ -44,6 +44,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.jpa.repository.support.DefaultJpaEntityMetadata;
import org.springframework.data.jpa.repository.support.JpaEntityMetadata;
import org.springframework.data.jpa.repository.support.StandardExpressionEvaluationContextProvider;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -95,7 +96,8 @@ public class SimpleJpaQueryUnitTests {
when(method.getEntityInformation()).thenReturn((JpaEntityMetadata) new DefaultJpaEntityMetadata<User>(User.class));
when(em.createQuery("foo", Long.class)).thenReturn(query);
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u");
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u",
StandardExpressionEvaluationContextProvider.INSTANCE);
assertThat(jpaQuery.createCountQuery(new Object[] {}), is(query));
}
@@ -111,7 +113,8 @@ public class SimpleJpaQueryUnitTests {
Method method = UserRepository.class.getMethod("findAllPaged", Pageable.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor);
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u");
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u",
StandardExpressionEvaluationContextProvider.INSTANCE);
jpaQuery.createCountQuery(new Object[] { new PageRequest(1, 10) });
verify(query, times(0)).setFirstResult(anyInt());
@@ -124,7 +127,8 @@ public class SimpleJpaQueryUnitTests {
Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor);
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em);
AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em,
StandardExpressionEvaluationContextProvider.INSTANCE);
assertThat(jpaQuery instanceof NativeJpaQuery, is(true));
@@ -205,7 +209,8 @@ public class SimpleJpaQueryUnitTests {
private RepositoryQuery createJpaQuery(Method method) {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor);
return JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em);
return JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em,
StandardExpressionEvaluationContextProvider.INSTANCE);
}
interface SampleRepository {

View File

@@ -447,7 +447,7 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
* @see DATAJPA-551
*/
Slice<User> findTop2UsersBy(Pageable page);
/**
* @see DATAJPA-506
*/
@@ -459,4 +459,16 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
*/
@Query("select u from User u where u.emailAddress = ?1")
Optional<User> findOptionalByEmailAddress(String emailAddress);
/**
* @see DATAJPA-XXX
*/
@Query("select u from User u where u.firstname = ?#{[0]} and u.firstname = ?1 and u.lastname like %?#{[1]}% and u.lastname like %?2%")
List<User> findByFirstnameAndLastnameWithSpelExpression(String firstname, String lastname);
/**
* @see DATAJPA-XXX
*/
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
}