DATACASS-788 - Add support for reactive SpEL context extensions.

We now support context extensions that contribute contextual details using a reactive programming model for evaluation of SpEL expressions in @Query methods.
This commit is contained in:
Mark Paluch
2020-07-27 10:48:24 +02:00
parent 94f26e95a1
commit e9801aaf9a
20 changed files with 590 additions and 342 deletions

View File

@@ -38,7 +38,6 @@ import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.util.Assert;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.cql.Statement;
/**
* Base class for reactive {@link RepositoryQuery} implementations for Cassandra.
@@ -81,18 +80,10 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository
*/
@Override
public Object execute(Object[] parameters) {
return getQueryMethod().hasReactiveWrapperParameter() ? executeDeferred(parameters) : executeNow(parameters);
return Flux.defer(() -> executeLater(parameters));
}
@SuppressWarnings("unchecked")
private Object executeDeferred(Object[] parameters) {
return getQueryMethod().isCollectionQuery() ? Flux.defer(() -> (Publisher<Object>) execute(parameters))
: Mono.defer(() -> (Mono<Object>) execute(parameters));
}
private Object executeNow(Object[] parameters) {
private Publisher<Object> executeLater(Object[] parameters) {
ReactiveCassandraParameterAccessor parameterAccessor = new ReactiveCassandraParameterAccessor(getQueryMethod(),
parameters);
@@ -100,7 +91,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository
CassandraParameterAccessor convertingParameterAccessor = new ConvertingParameterAccessor(
getRequiredConverter(getReactiveCassandraOperations()), parameterAccessor);
Statement<?> statement = createQuery(convertingParameterAccessor);
Mono<SimpleStatement> statement = createQuery(convertingParameterAccessor);
ResultProcessor resultProcessor = getQueryMethod().getResultProcessor()
.withDynamicProjection(convertingParameterAccessor);
@@ -110,7 +101,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository
Class<?> resultType = resolveResultType(resultProcessor);
return queryExecution.execute(statement, resultType);
return statement.flatMapMany(it -> queryExecution.execute(it, resultType));
}
private Class<?> resolveResultType(ResultProcessor resultProcessor) {
@@ -126,7 +117,7 @@ public abstract class AbstractReactiveCassandraQuery extends CassandraRepository
*
* @param accessor must not be {@literal null}.
*/
protected abstract SimpleStatement createQuery(CassandraParameterAccessor accessor);
protected abstract Mono<SimpleStatement> createQuery(CassandraParameterAccessor accessor);
protected ReactiveCassandraOperations getReactiveCassandraOperations() {
return this.operations;

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2020 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
*
* https://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.cassandra.repository.query;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Value object capturing the binding context to provide {@link #getBindingValues() binding values} for queries.
*
* @author Mark Paluch
* @since 1.5
*/
class BindingContext {
private final CassandraParameters parameters;
private final ParameterAccessor parameterAccessor;
private final List<ParameterBinding> bindings;
private final SpELExpressionEvaluator evaluator;
/**
* Create new {@link BindingContext}.
*/
public BindingContext(CassandraParameters parameters, ParameterAccessor parameterAccessor,
List<ParameterBinding> bindings, SpELExpressionEvaluator evaluator) {
this.parameters = parameters;
this.parameterAccessor = parameterAccessor;
this.bindings = bindings;
this.evaluator = evaluator;
}
/**
* @return {@literal true} when list of bindings is not empty.
*/
private boolean hasBindings() {
return !bindings.isEmpty();
}
/**
* Bind values provided by {@link CassandraParameterAccessor} to placeholders in {@link BindingContext} while
* considering potential conversions and parameter types.
*
* @return {@literal null} if given {@code raw} value is empty.
*/
public List<Object> getBindingValues() {
if (!hasBindings()) {
return Collections.emptyList();
}
List<Object> parameters = new ArrayList<>(bindings.size());
for (ParameterBinding binding : bindings) {
Object parameterValueForBinding = getParameterValueForBinding(binding);
parameters.add(parameterValueForBinding);
}
return parameters;
}
/**
* Return the value to be used for the given {@link ParameterBinding}.
*
* @param binding must not be {@literal null}.
* @return the value used for the given {@link ParameterBinding}.
*/
@Nullable
private Object getParameterValueForBinding(ParameterBinding binding) {
if (binding.isExpression()) {
return evaluator.evaluate(binding.getRequiredExpression());
}
return binding.isNamed()
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName()))
: parameterAccessor.getBindableValue(binding.getParameterIndex());
}
private int getParameterIndex(CassandraParameters parameters, String parameterName) {
return parameters.stream() //
.filter(cassandraParameter -> cassandraParameter //
.getName().filter(s -> s.equals(parameterName)) //
.isPresent()) //
.mapToInt(Parameter::getIndex) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(
String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName)));
}
/**
* A generic parameter binding with name or position information.
*
* @author Mark Paluch
*/
static class ParameterBinding {
private final int parameterIndex;
private final @Nullable String expression;
private final @Nullable String parameterName;
private ParameterBinding(int parameterIndex, @Nullable String expression, @Nullable String parameterName) {
this.parameterIndex = parameterIndex;
this.expression = expression;
this.parameterName = parameterName;
}
static ParameterBinding expression(String expression, boolean quoted) {
return new ParameterBinding(-1, expression, null);
}
static ParameterBinding indexed(int parameterIndex) {
return new ParameterBinding(parameterIndex, null, null);
}
static ParameterBinding named(String name) {
return new ParameterBinding(-1, null, name);
}
boolean isNamed() {
return (parameterName != null);
}
int getParameterIndex() {
return parameterIndex;
}
String getParameter() {
return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
}
String getRequiredExpression() {
Assert.state(expression != null, "ParameterBinding is not an expression");
return expression;
}
boolean isExpression() {
return (this.expression != null);
}
String getRequiredParameterName() {
Assert.state(parameterName != null, "ParameterBinding is not named");
return parameterName;
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2020 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
*
* https://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.cassandra.repository.query;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
/**
* Simple {@link SpELExpressionEvaluator} implementation using {@link ExpressionParser} and {@link EvaluationContext}.
*
* @author Mark Paluch
* @since 3.1
*/
class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator {
private final ExpressionParser parser;
private final EvaluationContext context;
DefaultSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) {
this.parser = parser;
this.context = context;
}
/**
* Return a {@link SpELExpressionEvaluator} that does not support expression evaluation.
*
* @return a {@link SpELExpressionEvaluator} that does not support expression evaluation.
*/
public static SpELExpressionEvaluator unsupported() {
return NoOpExpressionEvaluator.INSTANCE;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.SpELExpressionEvaluator#evaluate(java.lang.String)
*/
@Override
@SuppressWarnings("unchecked")
public <T> T evaluate(String expression) {
return (T) parser.parseExpression(expression).getValue(context, Object.class);
}
/**
* {@link SpELExpressionEvaluator} that does not support SpEL evaluation.
*
* @author Mark Paluch
*/
enum NoOpExpressionEvaluator implements SpELExpressionEvaluator {
INSTANCE;
@Override
public <T> T evaluate(String expression) {
throw new UnsupportedOperationException("Expression evaluation not supported");
}
}
}

View File

@@ -1,250 +0,0 @@
/*
* Copyright 2016-2020 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
*
* https://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.cassandra.repository.query;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
* {@link String}.
*
* @author Mark Paluch
* @since 1.5
*/
class ExpressionEvaluatingParameterBinder {
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Creates new {@link ExpressionEvaluatingParameterBinder}
*
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(expressionParser, "ExpressionParser must not be null");
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null");
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* Bind values provided by {@link CassandraParameterAccessor} to placeholders in {@link BindingContext} while
* considering potential conversions and parameter types.
*
* @param parameterAccessor must not be {@literal null}.
* @param bindingContext must not be {@literal null}.
* @return {@literal null} if given {@code raw} value is empty.
*/
public List<Object> bind(CassandraParameterAccessor parameterAccessor, BindingContext bindingContext) {
if (!bindingContext.hasBindings()) {
return Collections.emptyList();
}
List<Object> parameters = new ArrayList<>(bindingContext.getBindings().size());
bindingContext.getBindings() //
.stream() //
.map(binding -> getParameterValueForBinding(parameterAccessor, bindingContext.getParameters(), binding)) //
.forEach(parameters::add);
return parameters;
}
/**
* Returns the value to be used for the given {@link ParameterBinding}.
*
* @param parameterAccessor must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @param binding must not be {@literal null}.
* @return the value used for the given {@link ParameterBinding}.
*/
@Nullable
private Object getParameterValueForBinding(CassandraParameterAccessor parameterAccessor,
CassandraParameters parameters, ParameterBinding binding) {
if (binding.isExpression()) {
return evaluateExpression(binding.getExpression(), parameters, parameterAccessor.getValues());
}
return binding.isNamed()
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getParameterName()))
: parameterAccessor.getBindableValue(binding.getParameterIndex());
}
private int getParameterIndex(CassandraParameters parameters, String parameterName) {
return parameters.stream() //
.filter(cassandraParameter -> cassandraParameter //
.getName().filter(s -> s.equals(parameterName)) //
.isPresent()) //
.mapToInt(Parameter::getIndex) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(
String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName)));
}
/**
* Evaluates the given {@code expressionString}.
*
* @param expressionString must not be {@literal null} or empty.
* @param parameters must not be {@literal null}.
* @param parameterValues must not be {@literal null}.
* @return the value of the {@code expressionString} evaluation.
*/
@Nullable
private Object evaluateExpression(String expressionString, CassandraParameters parameters, Object[] parameterValues) {
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
Expression expression = expressionParser.parseExpression(expressionString);
return expression.getValue(evaluationContext, Object.class);
}
/**
* @author Mark Paluch
* @since 1.5
*/
static class BindingContext {
final CassandraQueryMethod queryMethod;
final List<ParameterBinding> bindings;
/**
* Creates new {@link BindingContext}.
*
* @param queryMethod {@link CassandraQueryMethod} on which the parameters are evaluated.
* @param bindings {@link List} of {@link ParameterBinding} containing name or position (index) information
* pertaining to the parameter in the referenced {@code queryMethod}.
*/
public BindingContext(CassandraQueryMethod queryMethod, List<ParameterBinding> bindings) {
this.queryMethod = queryMethod;
this.bindings = bindings;
}
/**
* @return {@literal true} when list of bindings is not empty.
*/
boolean hasBindings() {
return !CollectionUtils.isEmpty(bindings);
}
/**
* Get unmodifiable list of {@link ParameterBinding}s.
*
* @return never {@literal null}.
*/
List<ParameterBinding> getBindings() {
return Collections.unmodifiableList(bindings);
}
/**
* Get the associated {@link CassandraParameters}.
*
* @return the {@link CassandraParameters} associated with the {@link CassandraQueryMethod}.
*/
CassandraParameters getParameters() {
return queryMethod.getParameters();
}
/**
* Get the {@link CassandraQueryMethod}.
*
* @return the {@link CassandraQueryMethod} used in the expression evaluation context.
*/
CassandraQueryMethod getQueryMethod() {
return queryMethod;
}
}
/**
* A generic parameter binding with name or position information.
*
* @author Mark Paluch
*/
static class ParameterBinding {
private final boolean quoted;
private final int parameterIndex;
private final @Nullable String expression;
private final @Nullable String parameterName;
private ParameterBinding(int parameterIndex, boolean quoted, @Nullable String expression,
@Nullable String parameterName) {
this.parameterIndex = parameterIndex;
this.quoted = quoted;
this.expression = expression;
this.parameterName = parameterName;
}
static ParameterBinding expression(String expression, boolean quoted) {
return new ParameterBinding(-1, quoted, expression, null);
}
static ParameterBinding indexed(int parameterIndex) {
return new ParameterBinding(parameterIndex, false, null, null);
}
static ParameterBinding named(String name) {
return new ParameterBinding(-1, false, null, name);
}
boolean isNamed() {
return (parameterName != null);
}
int getParameterIndex() {
return parameterIndex;
}
String getParameter() {
return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
}
@Nullable
String getExpression() {
return expression;
}
boolean isExpression() {
return (this.expression != null);
}
@Nullable
String getParameterName() {
return parameterName;
}
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.repository.Query.Idempotency;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
@@ -229,11 +230,12 @@ class QueryStatementCreator {
* @param parameterAccessor must not be {@literal null}.
* @return the {@link Statement}.
*/
SimpleStatement select(StringBasedQuery stringBasedQuery, CassandraParameterAccessor parameterAccessor) {
SimpleStatement select(StringBasedQuery stringBasedQuery, CassandraParameterAccessor parameterAccessor,
SpELExpressionEvaluator evaluator) {
try {
SimpleStatement boundQuery = stringBasedQuery.bindQuery(parameterAccessor, this.queryMethod);
SimpleStatement boundQuery = stringBasedQuery.bindQuery(parameterAccessor, evaluator);
Optional<QueryOptions> queryOptions = Optional.ofNullable(parameterAccessor.getQueryOptions());

View File

@@ -49,7 +49,7 @@ import com.datastax.oss.driver.api.core.cql.Statement;
@FunctionalInterface
interface ReactiveCassandraQueryExecution {
Object execute(Statement<?> statement, Class<?> type);
Publisher<? extends Object> execute(Statement<?> statement, Class<?> type);
/**
* {@link ReactiveCassandraQueryExecution} for a {@link org.springframework.data.domain.Slice}.
@@ -72,7 +72,7 @@ interface ReactiveCassandraQueryExecution {
* @see org.springframework.data.cassandra.repository.query.CassandraQueryExecution#execute(java.lang.String, java.lang.Class)
*/
@Override
public Object execute(Statement<?> statement, Class<?> type) {
public Publisher<? extends Object> execute(Statement<?> statement, Class<?> type) {
CassandraPageRequest.validatePageable(pageable);
@@ -114,7 +114,7 @@ interface ReactiveCassandraQueryExecution {
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class)
*/
@Override
public Object execute(Statement<?> statement, Class<?> type) {
public Publisher<? extends Object> execute(Statement<?> statement, Class<?> type) {
return operations.select(statement, type);
}
}
@@ -139,7 +139,7 @@ interface ReactiveCassandraQueryExecution {
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class)
*/
@Override
public Object execute(Statement<?> statement, Class<?> type) {
public Publisher<? extends Object> execute(Statement<?> statement, Class<?> type) {
return operations.select(statement, type).buffer(2).map(objects -> {
@@ -175,7 +175,7 @@ interface ReactiveCassandraQueryExecution {
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(com.datastax.oss.driver.api.core.cql.Statement, java.lang.Class)
*/
@Override
public Object execute(Statement<?> statement, Class<?> type) {
public Publisher<? extends Object> execute(Statement<?> statement, Class<?> type) {
Mono<List<Row>> rows = this.operations.getReactiveCqlOperations().queryForRows(statement).buffer(2).next();
@@ -223,8 +223,8 @@ interface ReactiveCassandraQueryExecution {
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class)
*/
@Override
public Object execute(Statement<?> statement, Class<?> type) {
return converter.convert(delegate.execute(statement, type));
public Publisher<? extends Object> execute(Statement<?> statement, Class<?> type) {
return (Publisher) converter.convert(delegate.execute(statement, type));
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.cassandra.repository.query;
import reactor.core.publisher.Mono;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.core.StatementFactory;
import org.springframework.data.cassandra.core.convert.UpdateMapper;
@@ -94,22 +96,25 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue
* @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean)
*/
@Override
protected SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) {
protected Mono<SimpleStatement> createQuery(CassandraParameterAccessor parameterAccessor) {
if (isCountQuery()) {
return getQueryStatementCreator().count(getStatementFactory(), getTree(), parameterAccessor);
}
return Mono.fromSupplier(() -> {
if (isExistsQuery()) {
return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor);
}
if (isCountQuery()) {
return getQueryStatementCreator().count(getStatementFactory(), getTree(), parameterAccessor);
}
if (getTree().isDelete()) {
return getQueryStatementCreator().delete(getStatementFactory(), getTree(), parameterAccessor);
}
if (isExistsQuery()) {
return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor);
}
return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor,
getQueryMethod().getResultProcessor());
if (getTree().isDelete()) {
return getQueryStatementCreator().delete(getStatementFactory(), getTree(), parameterAccessor);
}
return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor,
getQueryMethod().getResultProcessor());
});
}
/* (non-Javadoc)

View File

@@ -15,9 +15,15 @@
*/
package org.springframework.data.cassandra.repository.query;
import reactor.core.publisher.Mono;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.repository.Query;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -45,6 +51,9 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra
private final boolean isExistsQuery;
private final ExpressionParser expressionParser;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Create a new {@link ReactiveStringBasedCassandraQuery} for the given {@link CassandraQueryMethod},
* {@link ReactiveCassandraOperations}, {@link SpelExpressionParser}, and
@@ -59,8 +68,8 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations
*/
public ReactiveStringBasedCassandraQuery(ReactiveCassandraQueryMethod queryMethod,
ReactiveCassandraOperations operations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveCassandraOperations operations, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider);
}
@@ -79,15 +88,17 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations
*/
public ReactiveStringBasedCassandraQuery(String query, ReactiveCassandraQueryMethod method,
ReactiveCassandraOperations operations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveCassandraOperations operations, ExpressionParser expressionParser,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, operations);
Assert.hasText(query, "Query must not be empty");
this.stringBasedQuery = new StringBasedQuery(query,
new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider));
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.stringBasedQuery = new StringBasedQuery(query, method.getParameters(), expressionParser);
if (method.hasAnnotatedQuery()) {
@@ -113,8 +124,14 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra
* @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor)
*/
@Override
public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) {
return getQueryStatementCreator().select(getStringBasedQuery(), parameterAccessor);
public Mono<SimpleStatement> createQuery(CassandraParameterAccessor parameterAccessor) {
StringBasedQuery query = getStringBasedQuery();
Mono<SpELExpressionEvaluator> spelEvaluator = getSpelEvaluatorFor(query.getExpressionDependencies(),
parameterAccessor);
return spelEvaluator.map(it -> getQueryStatementCreator().select(query, parameterAccessor, it));
}
/* (non-Javadoc)
@@ -148,4 +165,22 @@ public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandra
protected boolean isModifyingQuery() {
return false;
}
/**
* Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} suitable to evaluate expressions
* backed by the given dependencies.
*
* @param dependencies must not be {@literal null}.
* @param accessor must not be {@literal null}.
* @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready.
*/
private Mono<SpELExpressionEvaluator> getSpelEvaluatorFor(ExpressionDependencies dependencies,
CassandraParameterAccessor accessor) {
return evaluationContextProvider
.getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), dependencies)
.map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator(expressionParser,
evaluationContext))
.defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported());
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.cassandra.repository.query;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.repository.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
@@ -44,6 +46,9 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
private final boolean isExistsQuery;
private final ExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Create a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod},
* {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
@@ -57,7 +62,7 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
* @see org.springframework.data.cassandra.core.CassandraOperations
*/
public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations,
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider);
}
@@ -76,12 +81,15 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
* @see org.springframework.data.cassandra.core.CassandraOperations
*/
public StringBasedCassandraQuery(String query, CassandraQueryMethod method, CassandraOperations operations,
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, operations);
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.stringBasedQuery = new StringBasedQuery(query,
new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider));
method.getParameters(), expressionParser);
if (method.hasAnnotatedQuery()) {
@@ -108,7 +116,14 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
*/
@Override
public SimpleStatement createQuery(CassandraParameterAccessor parameterAccessor) {
return getQueryStatementCreator().select(getStringBasedQuery(), parameterAccessor);
StringBasedQuery query = getStringBasedQuery();
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(
getQueryMethod().getParameters(), parameterAccessor.getValues(), query.getExpressionDependencies());
return getQueryStatementCreator().select(query, parameterAccessor,
new DefaultSpELExpressionEvaluator(expressionParser, evaluationContext));
}
/* (non-Javadoc)

View File

@@ -21,8 +21,10 @@ import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding;
import org.springframework.data.cassandra.repository.query.BindingContext.ParameterBinding;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.ExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -39,54 +41,76 @@ class StringBasedQuery {
private final String query;
private final ExpressionEvaluatingParameterBinder parameterBinder;
private final CassandraParameters parameters;
private final ExpressionParser expressionParser;
private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();
private final ExpressionDependencies expressionDependencies;
/**
* Create a new {@link StringBasedQuery} given {@code query}, {@link ExpressionEvaluatingParameterBinder} and
* {@link CodecRegistry}.
* Create a new {@link StringBasedQuery} given {@code query}, {@link CassandraParameters} and
* {@link ExpressionParser}.
*
* @param query must not be empty.
* @param parameterBinder must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
*/
StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder) {
Assert.hasText(query, "Query must not be empty");
Assert.notNull(parameterBinder, "ExpressionEvaluatingParameterBinder must not be null");
this.parameterBinder = parameterBinder;
StringBasedQuery(String query, CassandraParameters parameters, ExpressionParser expressionParser) {
this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings);
this.parameters = parameters;
this.expressionParser = expressionParser;
this.expressionDependencies = createExpressionDependencies();
}
private ExpressionEvaluatingParameterBinder getParameterBinder() {
return this.parameterBinder;
private ExpressionDependencies createExpressionDependencies() {
if (queryParameterBindings.isEmpty()) {
return ExpressionDependencies.none();
}
List<ExpressionDependencies> dependencies = new ArrayList<>();
for (ParameterBinding binding : queryParameterBindings) {
if (binding.isExpression()) {
dependencies
.add(ExpressionDependencies.discover(expressionParser.parseExpression(binding.getRequiredExpression())));
}
}
return ExpressionDependencies.merged(dependencies);
}
/* (non-Javadoc) */
protected String getQuery() {
return this.query;
/**
* Obtain {@link ExpressionDependencies} from the parsed query.
*
* @return the {@link ExpressionDependencies} from the parsed query.
*/
public ExpressionDependencies getExpressionDependencies() {
return expressionDependencies;
}
/**
* Bind the query to actual parameters using {@link CassandraParameterAccessor},
*
* @param parameterAccessor must not be {@literal null}.
* @param queryMethod must not be {@literal null}.
* @param evaluator must not be {@literal null}.
* @return the bound String query containing formatted parameters.
*/
SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, CassandraQueryMethod queryMethod) {
public SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, SpELExpressionEvaluator evaluator) {
Assert.notNull(parameterAccessor, "CassandraParameterAccessor must not be null");
Assert.notNull(queryMethod, "CassandraQueryMethod must not be null");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");
List<Object> arguments = getParameterBinder().bind(parameterAccessor,
new BindingContext(queryMethod, this.queryParameterBindings));
BindingContext bindingContext = new BindingContext(this.parameters, parameterAccessor, this.queryParameterBindings,
evaluator);
return ParameterBinder.INSTANCE.bind(getQuery(), arguments);
List<Object> arguments = bindingContext.getBindingValues();
return ParameterBinder.INSTANCE.bind(this.query, arguments);
}
/**
@@ -218,14 +242,15 @@ class StringBasedQuery {
result.append(ARGUMENT_PLACEHOLDER);
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding
bindings.add(
BindingContext.ParameterBinding
.expression(input.substring(exprStart + 3, currentPosition - 1), true));
} else {
if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
bindings
.add(ExpressionEvaluatingParameterBinder.ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
.add(BindingContext.ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
} else {
bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding.named(matcher.group(1)));
bindings.add(BindingContext.ParameterBinding.named(matcher.group(1)));
}
currentPosition = matcher.end();

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020 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
*
* https://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.cassandra.repository.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
/**
* Caching variant of {@link ExpressionParser}. This implementation does not support
* {@link #parseExpression(String, ParserContext) parsing with ParseContext}.
*
* @author Mark Paluch
* @since 3.1
*/
class CachingExpressionParser implements ExpressionParser {
private final ExpressionParser delegate;
private final Map<String, Expression> cache = new ConcurrentHashMap<>();
CachingExpressionParser(ExpressionParser delegate) {
this.delegate = delegate;
}
/*
* (non-Javadoc)
* @see org.springframework.expression.ExpressionParser#parseExpression(java.lang.String)
*/
@Override
public Expression parseExpression(String expressionString) throws ParseException {
return cache.computeIfAbsent(expressionString, delegate::parseExpression);
}
/*
* (non-Javadoc)
* @see org.springframework.expression.ExpressionParser#parseExpression(java.lang.String, org.springframework.expression.ParserContext)
*/
@Override
public Expression parseExpression(String expressionString, ParserContext context) throws ParseException {
throw new UnsupportedOperationException("Parsing using ParserContext is not supported");
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -112,7 +113,7 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport {
return Optional.of(new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
}
private class CassandraQueryLookupStrategy implements QueryLookupStrategy {
private static class CassandraQueryLookupStrategy implements QueryLookupStrategy {
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
@@ -120,6 +121,8 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport {
private final CassandraOperations operations;
private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER);
CassandraQueryLookupStrategy(CassandraOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext) {
@@ -141,13 +144,14 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport {
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, expressionParser,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedCassandraQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
return new StringBasedCassandraQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
} else {
return new PartTreeCassandraQuery(queryMethod, operations);
}
}
}
}

View File

@@ -34,7 +34,9 @@ import org.springframework.data.repository.core.support.ReactiveRepositoryFactor
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -64,6 +66,8 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor
this.operations = cassandraOperations;
this.mappingContext = cassandraOperations.getConverter().getMappingContext();
setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
}
/* (non-Javadoc)
@@ -91,7 +95,8 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
return Optional.of(new CassandraQueryLookupStrategy(operations,
(ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, mappingContext));
}
/* (non-Javadoc)
@@ -113,12 +118,16 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor
*/
private static class CassandraQueryLookupStrategy implements QueryLookupStrategy {
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
private final ReactiveCassandraOperations operations;
private final MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> mappingContext;
private final ExpressionParser expressionParser = new CachingExpressionParser(EXPRESSION_PARSER);
CassandraQueryLookupStrategy(ReactiveCassandraOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider,
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends CassandraPersistentEntity<?>, ? extends CassandraPersistentProperty> mappingContext) {
this.evaluationContextProvider = evaluationContextProvider;
@@ -141,10 +150,10 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, expressionParser,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveStringBasedCassandraQuery(queryMethod, operations, EXPRESSION_PARSER,
return new ReactiveStringBasedCassandraQuery(queryMethod, operations, expressionParser,
evaluationContextProvider);
} else {
return new ReactivePartTreeCassandraQuery(queryMethod, operations);

View File

@@ -15,11 +15,16 @@
*/
package org.springframework.data.cassandra.repository.support;
import java.util.Optional;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -80,6 +85,16 @@ public class ReactiveCassandraRepositoryFactoryBean<T extends Repository<S, ID>,
return getFactoryInstance(operations);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createDefaultQueryMethodEvaluationContextProvider(ListableBeanFactory)
*/
@Override
protected Optional<QueryMethodEvaluationContextProvider> createDefaultQueryMethodEvaluationContextProvider(
ListableBeanFactory beanFactory) {
return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory));
}
/**
* Creates and initializes a {@link RepositoryFactorySupport} instance.
*

View File

@@ -111,7 +111,6 @@ public class ReactiveCassandraRepositoryIntegrationTests extends AbstractSpringD
factory.setRepositoryBaseClass(SimpleReactiveCassandraRepository.class);
factory.setBeanClassLoader(classLoader);
factory.setBeanFactory(beanFactory);
factory.setEvaluationContextProvider(QueryMethodEvaluationContextProvider.DEFAULT);
repository = factory.getRepository(UserRepository.class);
groupRepostitory = factory.getRepository(GroupRepository.class);

View File

@@ -21,7 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding;
import org.springframework.data.cassandra.repository.query.BindingContext.ParameterBinding;
import org.springframework.data.cassandra.repository.query.StringBasedQuery.ParameterBindingParser;
/**

View File

@@ -176,7 +176,8 @@ public class ReactivePartTreeCassandraQueryUnitTests {
CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(),
args);
return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor));
return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor))
.block();
}
private ReactivePartTreeCassandraQuery createQueryForMethod(Class<?> repositoryInterface, String methodName,

View File

@@ -18,7 +18,10 @@ package org.springframework.data.cassandra.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
@@ -40,8 +43,11 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import com.datastax.oss.driver.api.core.DefaultConsistencyLevel;
@@ -79,13 +85,13 @@ public class ReactiveStringBasedCassandraQueryUnitTests {
}
@Test // DATACASS-335
public void bindsSimplePropertyCorrectly() throws Exception {
public void bindsSimplePropertyCorrectly() {
ReactiveStringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class);
CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(
cassandraQuery.getQueryMethod(), "White");
SimpleStatement actual = cassandraQuery.createQuery(accessor);
SimpleStatement actual = cassandraQuery.createQuery(accessor).block();
assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;");
assertThat(actual.getPositionalValues().get(0)).isEqualTo("White");
@@ -102,7 +108,7 @@ public class ReactiveStringBasedCassandraQueryUnitTests {
CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor(
cassandraQuery.getQueryMethod(), queryOptions, "White");
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor);
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor).block();
assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;");
assertThat(actual.getPositionalValues().get(0)).isEqualTo("White");
@@ -117,13 +123,27 @@ public class ReactiveStringBasedCassandraQueryUnitTests {
CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor(
cassandraQuery.getQueryMethod(), "Matthews");
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor);
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor).block();
assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;");
assertThat(actual.getPositionalValues().get(0)).isEqualTo("Matthews");
assertThat(actual.getConsistencyLevel()).isEqualTo(DefaultConsistencyLevel.LOCAL_ONE);
}
@Test // DATACASS-788
public void shouldUseSpelExtension() {
ReactiveStringBasedCassandraQuery cassandraQuery = getQueryMethod("findBySpel");
CassandraParametersParameterAccessor parameterAccessor = new CassandraParametersParameterAccessor(
cassandraQuery.getQueryMethod());
SimpleStatement actual = cassandraQuery.createQuery(parameterAccessor).block();
assertThat(actual.getQuery()).isEqualTo("SELECT * FROM person WHERE lastname=?;");
assertThat(actual.getPositionalValues().get(0)).isEqualTo("Walter");
}
private ReactiveStringBasedCassandraQuery getQueryMethod(String name, Class<?>... args) {
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
@@ -131,8 +151,11 @@ public class ReactiveStringBasedCassandraQueryUnitTests {
ReactiveCassandraQueryMethod queryMethod = new ReactiveCassandraQueryMethod(method, metadata, factory,
converter.getMappingContext());
ReactiveExtensionAwareQueryMethodEvaluationContextProvider provider = new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(
Arrays.asList(MyReactiveExtension.INSTANCE, MyDefunctExtension.INSTANCE));
return new ReactiveStringBasedCassandraQuery(queryMethod, operations, PARSER,
ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
provider);
}
@SuppressWarnings("unused")
@@ -145,5 +168,74 @@ public class ReactiveStringBasedCassandraQueryUnitTests {
@Query("SELECT * FROM person WHERE lastname=?0;")
Person findByLastname(QueryOptions queryOptions, String lastname);
@Query("SELECT * FROM person WHERE lastname=:#{getName()};")
Person findBySpel();
}
public static class MyReactiveExtensionObject implements EvaluationContextExtension {
public String getName() {
return "Walter";
}
@Override
public String getExtensionId() {
return "ext-1";
}
@Nullable
@Override
public MyReactiveExtensionObject getRootObject() {
return this;
}
}
public static class DefunctExtensionObject implements EvaluationContextExtension {
public String getPrincipal() {
throw new IllegalStateException();
}
@Override
public String getExtensionId() {
return "ext-1";
}
@Nullable
@Override
public DefunctExtensionObject getRootObject() {
throw new IllegalStateException();
}
}
enum MyReactiveExtension implements ReactiveEvaluationContextExtension {
INSTANCE;
@Override
public Mono<MyReactiveExtensionObject> getExtension() {
return Mono.just(new MyReactiveExtensionObject());
}
@Override
public String getExtensionId() {
return "ext-1";
}
}
enum MyDefunctExtension implements ReactiveEvaluationContextExtension {
INSTANCE;
@Override
public Mono<DefunctExtensionObject> getExtension() {
return Mono.error(new IllegalStateException());
}
@Override
public String getExtensionId() {
return "ext-2";
}
}
}

View File

@@ -107,7 +107,6 @@ public class SimpleReactiveCassandraRepositoryIntegrationTests extends AbstractE
factory.setRepositoryBaseClass(SimpleReactiveCassandraRepository.class);
factory.setBeanClassLoader(classLoader);
factory.setBeanFactory(beanFactory);
factory.setEvaluationContextProvider(ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
repository = factory.getRepository(UserRepostitory.class);

View File

@@ -7,6 +7,7 @@ This chapter summarizes changes and new features for each release.
== What's new in Spring Data for Apache Cassandra 3.1
* <<cassandra.auditing,Reactive auditing>> enabled through `@EnableReactiveCassandraAuditing`. `@EnableCassandraAuditing` no longer registers `ReactiveAuditingEntityCallback`.
* Reactive SpEL support in `@Query` query methods.
[[new-features.3-0-0]]
== What's new in Spring Data for Apache Cassandra 3.0