Add support for SpEL in @Query.

Original Pull Request #2826
Closes #2083
This commit is contained in:
puppylpg 2024-01-20 02:19:03 +08:00 committed by GitHub
parent c6041fb659
commit e1a2412651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1645 additions and 143 deletions

View File

@ -316,7 +316,7 @@ Repository methods can be defined to have the following return types for returni
.Declare query on the method using the `@Query` annotation. .Declare query on the method using the `@Query` annotation.
==== ====
The arguments passed to the method can be inserted into placeholders in the query string. the placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on. The arguments passed to the method can be inserted into placeholders in the query string. The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
[source,java] [source,java]
---- ----
interface BookRepository extends ElasticsearchRepository<Book, String> { interface BookRepository extends ElasticsearchRepository<Book, String> {
@ -361,3 +361,202 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
} }
---- ----
==== ====
[[elasticsearch.query-methods.at-query.spel]]
=== Using SpEL Expressions
.Declare query on the method using the `@Query` annotation with SpEL expression.
====
https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"term":{
"name": "#{#name}"
}
}
]
}
}
""")
Page<Book> findByName(String name, Pageable pageable);
}
----
If for example the function is called with the parameter _John_, it would produce the following query body:
[source,json]
----
{
"bool":{
"must":[
{
"term":{
"name": "John"
}
}
]
}
}
----
====
.accessing parameter property.
====
Supposing that we have the following class as query parameter type:
[source,java]
----
public record QueryParameter(String value) {
}
----
It's easy to access the parameter by `#` symbol, then reference the property `value` with a simple `.`:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"term":{
"name": "#{#parameter.value}"
}
}
]
}
}
""")
Page<Book> findByName(QueryParameter parameter, Pageable pageable);
}
----
We can pass `new QueryParameter("John")` as the parameter now, and it will produce the same query string as above.
====
.accessing bean property.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"term":{
"name": "#{@queryParameter.value}"
}
}
]
}
}
""")
Page<Book> findByName(Pageable pageable);
}
----
====
.SpEL and `Collection` param.
====
`Collection` parameter is also supported and is as easy to use as normal `String`, such as the following `terms` query:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"terms":{
"name": #{#names}
}
}
]
}
}
""")
Page<Book> findByName(Collection<String> names, Pageable pageable);
}
----
NOTE: collection values should not be quoted when declaring the elasticsearch json query.
A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:
[source,json]
----
{
"bool":{
"must":[
{
"terms":{
"name": ["name1", "name2"]
}
}
]
}
}
----
====
.access property in the `Collection` param.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/collection-projection.html[SpEL Collection Projection] is convenient to use when values in the `Collection` parameter is not plain `String`:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"terms":{
"name": #{#parameters.![value]}
}
}
]
}
}
""")
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
}
----
This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
====
.alter parameter name by using `@Param`
====
When accessing the parameter by SpEL, it's also useful to alter the parameter name to another one by `@Param` annotation in Sping Data:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"terms":{
"name": #{#another.![value]}
}
}
]
}
}
""")
Page<Book> findByName(@Param("another") Collection<QueryParameter> parameters, Pageable pageable);
}
----
====

View File

@ -43,6 +43,7 @@ import org.springframework.util.Assert;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
* @since 3.2 * @since 3.2
*/ */
abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery { abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery {
@ -112,7 +113,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
* @param accessor must not be {@literal null}. * @param accessor must not be {@literal null}.
* @return * @return
*/ */
protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor); protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor);
private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor, private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
Converter<Object, Object> resultProcessing) { Converter<Object, Object> resultProcessing) {

View File

@ -21,7 +21,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor;
* @author Christoph Strobl * @author Christoph Strobl
* @since 3.2 * @since 3.2
*/ */
class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor public class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
implements ElasticsearchParameterAccessor { implements ElasticsearchParameterAccessor {
private final Object[] values; private final Object[] values;

View File

@ -69,9 +69,9 @@ import org.springframework.util.ClassUtils;
*/ */
public class ElasticsearchQueryMethod extends QueryMethod { public class ElasticsearchQueryMethod extends QueryMethod {
// the following 2 variables exits in the base class, but are private. We need them for // the following 2 variables exist in the base class, but are private. We need them for
// correct handling of return types (SearchHits), so we have our own values here. // correct handling of return types (SearchHits), so we have our own values here.
// Alas this means that we have to copy code that initializes these variables and in the // This means that we have to copy code that initializes these variables and in the
// base class uses them in order to use our variables // base class uses them in order to use our variables
protected final Method method; protected final Method method;
protected final Class<?> unwrappedReturnType; protected final Class<?> unwrappedReturnType;

View File

@ -17,9 +17,10 @@ package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -30,16 +31,21 @@ import org.springframework.util.Assert;
* @author Mark Paluch * @author Mark Paluch
* @author Taylor Ono * @author Taylor Ono
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
*/ */
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery { public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString; private final String queryString;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString) { String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations); super(queryMethod, elasticsearchOperations);
Assert.notNull(queryString, "Query cannot be empty"); Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
this.queryString = queryString; this.queryString = queryString;
this.evaluationContextProvider = evaluationContextProvider;
} }
@Override @Override
@ -58,12 +64,14 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue
} }
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService()) String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.queryString, parameterAccessor); .replacePlaceholders(this.queryString, parameterAccessor);
var query = new StringQuery(queryString); QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider);
var query = new StringQuery(evaluator.evaluate());
query.addSort(parameterAccessor.getSort()); query.addSort(parameterAccessor.getSort());
return query; return query;
} }
} }

View File

@ -16,19 +16,23 @@
package org.springframework.data.elasticsearch.repository.query; package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
* @author Taylor Ono * @author Taylor Ono
* @author Haibo Liu
* @since 3.2 * @since 3.2
*/ */
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery { public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private final String query; private final String query;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod, public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser, ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
@ -43,14 +47,17 @@ public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsea
super(queryMethod, operations); super(queryMethod, operations);
this.query = query; this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
} }
@Override @Override
protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil( String queryString = new StringQueryUtil(getElasticsearchOperations().getElasticsearchConverter().getConversionService())
getElasticsearchOperations().getElasticsearchConverter().getConversionService()).replacePlaceholders(this.query, .replacePlaceholders(this.query, parameterAccessor);
parameterAccessor);
return new StringQuery(queryString); QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider);
return new StringQuery(evaluator.evaluate());
} }
@Override @Override

View File

@ -40,7 +40,7 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
} }
@Override @Override
protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery(); CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery();
if (tree.isLimiting()) { if (tree.isLimiting()) {

View File

@ -34,6 +34,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -54,6 +56,7 @@ import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;
* @author Sascha Woo * @author Sascha Woo
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Ezequiel Antúnez Camacho * @author Ezequiel Antúnez Camacho
* @author Haibo Liu
*/ */
public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
@ -96,11 +99,17 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
@Override @Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key, protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) { QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new ElasticsearchQueryLookupStrategy()); return Optional.of(new ElasticsearchQueryLookupStrategy(evaluationContextProvider));
} }
private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy { private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
ElasticsearchQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
@ -115,9 +124,11 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
if (namedQueries.hasQuery(namedQueryName)) { if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName); String namedQuery = namedQueries.getQuery(namedQueryName);
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery); return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) { } else if (queryMethod.hasAnnotatedQuery()) {
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery()); return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
evaluationContextProvider);
} }
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations); return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations);
} }

View File

@ -0,0 +1,92 @@
/*
* Copyright 2024 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.elasticsearch.repository.support.spel;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.lang.Nullable;
import java.util.*;
/**
* Convert a collection into string for value part of the elasticsearch query.
* <p>
* If the value is type {@link String}, it should be wrapped with square brackets, with each element quoted therefore
* escaped(by {@link ElasticsearchStringValueToStringConverter}) if quotations exist in the original element.
* <p>
* eg: The value part of an elasticsearch terms query should looks like {@code ["hello \"Stranger\"","Another string"]}
* for query
* <pre>
* {@code
* {
* "bool":{
* "must":{
* "terms":{
* "name": ["hello \"Stranger\"", "Another string"]
* }
* }
* }
* }
* }
* </pre>
*
* @since 5.3
* @author Haibo Liu
*/
public class ElasticsearchCollectionValueToStringConverter implements GenericConverter {
private static final String DELIMITER = ",";
private final ConversionService conversionService;
public ElasticsearchCollectionValueToStringConverter(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Collection.class, String.class));
}
@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return "[]";
}
Collection<?> sourceCollection = (Collection<?>) source;
if (sourceCollection.isEmpty()) {
return "[]";
}
StringJoiner sb = new StringJoiner(DELIMITER, "[", "]");
for (Object sourceElement : sourceCollection) {
// ignore the null value in collection
if (Objects.isNull(sourceElement)) {
continue;
}
Object targetElement = this.conversionService.convert(
sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType);
if (sourceElement instanceof String) {
sb.add("\"" + targetElement + "\"");
} else {
sb.add(String.valueOf(targetElement));
}
}
return sb.toString();
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2024 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.elasticsearch.repository.support.spel;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.lang.Nullable;
import java.util.Collections;
import java.util.Set;
import java.util.regex.Matcher;
/**
* Values in elasticsearch query may contain quotations and should be escaped when converting.
* Note that the converter should only be used in this situation, rather than common string to string conversions.
*
* @since 5.3
* @author Haibo Liu
*/
public class ElasticsearchStringValueToStringConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, String.class));
}
@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
return escape(source);
}
private String escape(@Nullable Object source) {
// escape the quotes in the string, because the string should already be quoted manually
return String.valueOf(source).replaceAll("\"", Matcher.quoteReplacement("\\\""));
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2024 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.elasticsearch.repository.support.spel;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.util.Lazy;
/**
* To supply a {@link ConversionService} with custom converters to handle SpEL values in elasticsearch query.
*
* @since 5.3
* @author Haibo Liu
*/
public class ElasticsearchValueSpELConversionService {
public static final Lazy<ConversionService> CONVERSION_SERVICE_LAZY = Lazy.of(
ElasticsearchValueSpELConversionService::buildSpELConversionService);
private static ConversionService buildSpELConversionService() {
// register elasticsearch custom type converter for conversion service
ConversionService conversionService = new DefaultConversionService();
ConverterRegistry converterRegistry = (ConverterRegistry) conversionService;
converterRegistry.addConverter(new ElasticsearchCollectionValueToStringConverter(conversionService));
converterRegistry.addConverter(new ElasticsearchStringValueToStringConverter());
return conversionService;
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright 2024 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.elasticsearch.repository.support.spel;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.common.CompositeStringExpression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* To evaluate the SpEL expressions of the query string.
*
* @author Haibo Liu
* @since 5.3
*/
public class QueryStringSpELEvaluator {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private static final Map<String, Expression> QUERY_EXPRESSIONS = new ConcurrentHashMap<>();
private final String queryString;
private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final QueryMethod queryMethod;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final TypeConverter elasticsearchSpELTypeConverter;
public QueryStringSpELEvaluator(String queryString, ElasticsearchParametersParameterAccessor parameterAccessor,
QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.queryString = queryString;
this.parameterAccessor = parameterAccessor;
this.queryMethod = queryMethod;
this.evaluationContextProvider = evaluationContextProvider;
this.elasticsearchSpELTypeConverter = new StandardTypeConverter(ElasticsearchValueSpELConversionService.CONVERSION_SERVICE_LAZY);
}
/**
* Evaluate the SpEL parts of the query string.
*
* @return a plain string with values evaluated
*/
public String evaluate() {
Expression expr = getQueryExpression(queryString);
if (expr != null) {
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getParameters(),
parameterAccessor.getValues());
if (context instanceof StandardEvaluationContext standardEvaluationContext) {
standardEvaluationContext.setTypeConverter(elasticsearchSpELTypeConverter);
}
String parsed = parseExpressions(expr, context);
Assert.notNull(parsed, "Query parsed by SpEL should not be null");
return parsed;
}
return queryString;
}
/**
* {@link Expression#getValue(EvaluationContext, Class)} is not used because the value part in SpEL should be converted
* by {@link ElasticsearchStringValueToStringConverter} or
* {@link ElasticsearchCollectionValueToStringConverter} to
* escape the quotations, but other literal parts in SpEL expression should not be processed with these converters.
* So we just get the string value from {@link LiteralExpression} directly rather than
* {@link LiteralExpression#getValue(EvaluationContext, Class)}.
*/
private String parseExpressions(Expression rootExpr, EvaluationContext context) {
StringBuilder parsed = new StringBuilder();
if (rootExpr instanceof LiteralExpression literalExpression) {
// get the string literal directly
parsed.append(literalExpression.getExpressionString());
} else if (rootExpr instanceof SpelExpression spelExpression) {
// evaluate the value
String value = spelExpression.getValue(context, String.class);
if (value == null) {
throw new ConversionException(String.format(
"Parameter value can't be null for SpEL expression '%s' in method '%s' when querying elasticsearch",
spelExpression.getExpressionString(), queryMethod.getName()));
}
parsed.append(value);
} else if (rootExpr instanceof CompositeStringExpression compositeStringExpression) {
// parse one by one for composite expression
Expression[] expressions = compositeStringExpression.getExpressions();
for (Expression exp : expressions) {
parsed.append(parseExpressions(exp, context));
}
} else {
// no more
parsed.append(rootExpr.getValue(context, String.class));
}
return parsed.toString();
}
@Nullable
private Expression getQueryExpression(String queryString) {
return QUERY_EXPRESSIONS.computeIfAbsent(queryString, f -> {
Expression expr = PARSER.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION);
return expr instanceof LiteralExpression ? null : expr;
});
}
}

View File

@ -323,9 +323,9 @@ public abstract class SearchTemplateIntegrationTests {
@Document(indexName = "#{@indexNameProvider.indexName()}-student") @Document(indexName = "#{@indexNameProvider.indexName()}-student")
record Student( // record Student( //
@Nullable @Id String id, // @Nullable @Id String id, //
@Field(type = FieldType.Text) String firstName, // @Field(type = FieldType.Text) String firstName, //
@Field(type = FieldType.Text) String lastName // @Field(type = FieldType.Text) String lastName //
) { ) {
} }
} }

View File

@ -25,6 +25,7 @@ import org.springframework.test.context.ContextConfiguration;
/** /**
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4 * @since 4.4
*/ */
@ContextConfiguration(classes = { CustomMethodRepositoryELCIntegrationTests.Config.class }) @ContextConfiguration(classes = { CustomMethodRepositoryELCIntegrationTests.Config.class })
@ -40,5 +41,13 @@ public class CustomMethodRepositoryELCIntegrationTests extends CustomMethodRepos
IndexNameProvider indexNameProvider() { IndexNameProvider indexNameProvider() {
return new IndexNameProvider("custom-method-repository"); return new IndexNameProvider("custom-method-repository");
} }
/**
* a normal bean referenced by SpEL in query
*/
@Bean
QueryParameter queryParameter() {
return new QueryParameter("abc");
}
} }
} }

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.repositories.custommethod; package org.springframework.data.elasticsearch.repositories.custommethod;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.data.elasticsearch.annotations.FieldType.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import static org.springframework.data.elasticsearch.utils.IdGenerator.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
@ -52,6 +53,7 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.SearchPage; import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.core.geo.GeoBox; import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
@ -63,6 +65,7 @@ import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance; import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point; import org.springframework.data.geo.Point;
import org.springframework.data.repository.query.Param;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -1522,6 +1525,135 @@ public abstract class CustomMethodRepositoryIntegrationTests {
assertThat(searchHits.getTotalHits()).isEqualTo(20); assertThat(searchHits.getTotalHits()).isEqualTo(20);
} }
@Test
void shouldReturnSearchHitsForStringQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
// when
SearchHits<SampleEntity> searchHits = repository.queryByStringSpEL("abc");
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test
void shouldRaiseExceptionForNullStringQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByStringSpEL(null));
assertThat(thrown.getMessage())
.isEqualTo("Parameter value can't be null for SpEL expression '#type' in method 'queryByStringSpEL'" +
" when querying elasticsearch");
}
@Test
void shouldReturnSearchHitsForParameterPropertyQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
QueryParameter param = new QueryParameter("abc");
// when
SearchHits<SampleEntity> searchHits = repository.queryByParameterPropertySpEL(param);
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test
void shouldReturnSearchHitsForBeanQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
// when
SearchHits<SampleEntity> searchHits = repository.queryByBeanPropertySpEL();
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test
void shouldReturnSearchHitsForCollectionQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
// when
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(List.of("abc"));
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test
void shouldRaiseExceptionForNullCollectionQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByCollectionSpEL(null));
assertThat(thrown.getMessage())
.isEqualTo("Parameter value can't be null for SpEL expression '#types' in method 'queryByCollectionSpEL'" +
" when querying elasticsearch");
}
@Test
void shouldNotReturnSearchHitsForEmptyCollectionQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
// when
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(List.of());
assertThat(searchHits.getTotalHits()).isEqualTo(0);
}
@Test
void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
List<String> params = new ArrayList<>();
params.add(null);
// when
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(params);
assertThat(searchHits.getTotalHits()).isEqualTo(0);
}
@Test
void shouldIgnoreNullValuesInCollectionQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
// when
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(Arrays.asList("abc", null));
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
QueryParameter param = new QueryParameter("abc");
// when
SearchHits<SampleEntity> searchHits = repository.queryByParameterPropertyCollectionSpEL(List.of(param));
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() {
List<SampleEntity> entities = createSampleEntities("abc", 20);
repository.saveAll(entities);
QueryParameter param = new QueryParameter("abc");
// when
SearchHits<SampleEntity> searchHits = repository.queryByParameterPropertyCollectionSpELWithParamAnnotation(
List.of(param));
assertThat(searchHits.getTotalHits()).isEqualTo(20);
}
@Test // DATAES-372 @Test // DATAES-372
void shouldReturnHighlightsOnAnnotatedMethod() { void shouldReturnHighlightsOnAnnotatedMethod() {
List<SampleEntity> entities = createSampleEntities("abc", 2); List<SampleEntity> entities = createSampleEntities("abc", 2);
@ -1940,6 +2072,105 @@ public abstract class CustomMethodRepositoryIntegrationTests {
@Highlight(fields = { @HighlightField(name = "type") }) @Highlight(fields = { @HighlightField(name = "type") })
SearchHits<SampleEntity> queryByString(String type); SearchHits<SampleEntity> queryByString(String type);
/**
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
* will not accept a null parameter as query value.
*/
@Query("""
{
"bool":{
"must":[
{
"term":{
"type": "#{#type}"
}
}
]
}
}
""")
SearchHits<SampleEntity> queryByStringSpEL(@Nullable String type);
@Query("""
{
"bool":{
"must":[
{
"term":{
"type": "#{#parameter.value}"
}
}
]
}
}
""")
SearchHits<SampleEntity> queryByParameterPropertySpEL(QueryParameter parameter);
@Query("""
{
"bool":{
"must":[
{
"term":{
"type": "#{@queryParameter.value}"
}
}
]
}
}
""")
SearchHits<SampleEntity> queryByBeanPropertySpEL();
/**
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
* will not accept a null parameter as query value.
*/
@Query("""
{
"bool":{
"must":[
{
"terms":{
"type": #{#types}
}
}
]
}
}
""")
SearchHits<SampleEntity> queryByCollectionSpEL(@Nullable Collection<String> types);
@Query("""
{
"bool":{
"must":[
{
"terms":{
"type": #{#parameters.![value]}
}
}
]
}
}
""")
SearchHits<SampleEntity> queryByParameterPropertyCollectionSpEL(Collection<QueryParameter> parameters);
@Query("""
{
"bool":{
"must":[
{
"terms":{
"type": #{#e.![value]}
}
}
]
}
}
""")
SearchHits<SampleEntity> queryByParameterPropertyCollectionSpELWithParamAnnotation(
@Param("e") Collection<QueryParameter> parameters);
@Query(""" @Query("""
{ {
"bool":{ "bool":{

View File

@ -0,0 +1,25 @@
/*
* Copyright 2024 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.elasticsearch.repositories.custommethod;
/**
* Used as a parameter referenced by SpEL in query method tests.
*
* @param value content
* @author Haibo Liu
*/
public record QueryParameter(String value) {
}

View File

@ -32,6 +32,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.Field;
@ -42,15 +44,18 @@ import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Niklas Herder * @author Niklas Herder
* @author Haibo Liu
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase { public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
@ -83,6 +88,205 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
.isEqualTo("name:(zero, eleven, one, two, three, four, five, six, seven, eight, nine, ten, eleven, zero, one)"); .isEqualTo("name:(zero, eleven, one, two, three, four, five, six, seven, eight, nine, ten, eleven, zero, one)");
} }
@Test
public void shouldReplaceParametersSpEL() throws Exception {
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", "Luke");
String expected = """
{
"bool":{
"must":{
"term":{
"name": "Luke"
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceParametersSpELWithQuotes() throws Exception {
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL",
"hello \"world\"");
String expected = """
{
"bool":{
"must":{
"term":{
"name": "hello \\"world\\""
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldUseParameterPropertySpEL() throws Exception {
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByParameterPropertySpEL",
new QueryParameter("Luke"));
String expected = """
{
"bool":{
"must":{
"term":{
"name": "Luke"
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceCollectionSpEL() throws Exception {
final List<String> anotherString = List.of("hello \"Stranger\"", "Another string");
List<String> params = new ArrayList<>(anotherString);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": ["hello \\"Stranger\\"", "Another string"]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceNonStringCollectionSpEL() throws Exception {
final List<Integer> ages = List.of(1, 2, 3);
List<Integer> params = new ArrayList<>(ages);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAgesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"age": [1, 2, 3]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceEmptyCollectionSpEL() throws Exception {
final List<String> anotherString = List.of();
List<String> params = new ArrayList<>(anotherString);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": []
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldBeEmptyWithNullValuesInCollectionSpEL() throws Exception {
final List<String> anotherString = List.of();
List<String> params = new ArrayList<>(anotherString);
// add a null value
params.add(null);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": []
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldIgnoreNullValuesInCollectionSpEL() throws Exception {
final List<String> anotherString = List.of("abc");
List<String> params = new ArrayList<>(anotherString);
// add a null value
params.add(null);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": ["abc"]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceCollectionParametersSpEL() throws Exception {
final List<QueryParameter> anotherString = List.of(new QueryParameter("hello \"Stranger\""),
new QueryParameter("Another string"));
List<QueryParameter> params = new ArrayList<>(anotherString);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesParameterSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": ["hello \\"Stranger\\"", "Another string"]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test // #1790 @Test // #1790
@DisplayName("should escape Strings in query parameters") @DisplayName("should escape Strings in query parameters")
void shouldEscapeStringsInQueryParameters() throws Exception { void shouldEscapeStringsInQueryParameters() throws Exception {
@ -166,7 +370,8 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
} }
private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) { private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery()); return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery(),
QueryMethodEvaluationContextProvider.DEFAULT);
} }
private ElasticsearchQueryMethod getQueryMethod(String name, Class<?>... parameters) throws NoSuchMethodException { private ElasticsearchQueryMethod getQueryMethod(String name, Class<?>... parameters) throws NoSuchMethodException {
@ -187,9 +392,74 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }") @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }")
Person findByName(String name); Person findByName(String name);
@Query("""
{
"bool":{
"must":{
"term":{
"name": "#{#name}"
}
}
}
}
""")
Person findByNameSpEL(String name);
@Query("""
{
"bool":{
"must":{
"term":{
"name": "#{#param.value}"
}
}
}
}
""")
Person findByParameterPropertySpEL(QueryParameter param);
@Query("{ 'bool' : { 'must' : { 'terms' : { 'name' : ?0 } } } }") @Query("{ 'bool' : { 'must' : { 'terms' : { 'name' : ?0 } } } }")
Person findByNameIn(ArrayList<String> names); Person findByNameIn(ArrayList<String> names);
@Query("""
{
"bool":{
"must":{
"terms":{
"name": #{#names}
}
}
}
}
""")
Person findByNamesSpEL(ArrayList<String> names);
@Query("""
{
"bool":{
"must":{
"terms":{
"age": #{#ages}
}
}
}
}
""")
Person findByAgesSpEL(ArrayList<Integer> ages);
@Query("""
{
"bool":{
"must":{
"terms":{
"name": #{#names.![value]}
}
}
}
}
""")
Person findByNamesParameterSpEL(ArrayList<QueryParameter> names);
@Query(value = "name:(?0, ?11, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?0, ?1)") @Query(value = "name:(?0, ?11, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?0, ?1)")
Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5, Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5,
String arg6, String arg7, String arg8, String arg9, String arg10, String arg11); String arg6, String arg7, String arg8, String arg9, String arg10, String arg11);

View File

@ -22,6 +22,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@ -29,12 +30,13 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.Field;
@ -45,6 +47,7 @@ import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@ -55,6 +58,7 @@ import org.springframework.lang.Nullable;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase { public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
@ -71,10 +75,7 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
@Test // DATAES-519 @Test // DATAES-519
public void bindsSimplePropertyCorrectly() throws Exception { public void bindsSimplePropertyCorrectly() throws Exception {
ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByName", String.class); org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByName", "Luke");
StubParameterAccessor accessor = new StubParameterAccessor("Luke");
org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor);
StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }"); StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }");
assertThat(query).isInstanceOf(StringQuery.class); assertThat(query).isInstanceOf(StringQuery.class);
@ -82,20 +83,214 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
} }
@Test // DATAES-519 @Test // DATAES-519
@Disabled("TODO: fix spel query integration")
public void bindsExpressionPropertyCorrectly() throws Exception { public void bindsExpressionPropertyCorrectly() throws Exception {
ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByNameWithExpression", org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameWithExpression", "Luke");
String.class);
StubParameterAccessor accessor = new StubParameterAccessor("Luke");
org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor);
StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }"); StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }");
assertThat(query).isInstanceOf(StringQuery.class); assertThat(query).isInstanceOf(StringQuery.class);
assertThat(((StringQuery) query).getSource()).isEqualTo(reference.getSource()); assertThat(((StringQuery) query).getSource()).isEqualTo(reference.getSource());
} }
@Test
public void shouldReplaceParametersSpEL() throws Exception {
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", "Luke");
String expected = """
{
"bool":{
"must":{
"term":{
"name": "Luke"
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceParametersSpELWithQuotes() throws Exception {
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL",
"hello \"world\"");
String expected = """
{
"bool":{
"must":{
"term":{
"name": "hello \\"world\\""
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldUseParameterPropertySpEL() throws Exception {
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByParameterPropertySpEL",
new QueryParameter("Luke"));
String expected = """
{
"bool":{
"must":{
"term":{
"name": "Luke"
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceCollectionSpEL() throws Exception {
final List<String> anotherString = List.of("hello \"Stranger\"", "Another string");
List<String> params = new ArrayList<>(anotherString);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": ["hello \\"Stranger\\"", "Another string"]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceNonStringCollectionSpEL() throws Exception {
final List<Integer> ages = List.of(1, 2, 3);
List<Integer> params = new ArrayList<>(ages);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAgesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"age": [1, 2, 3]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceEmptyCollectionSpEL() throws Exception {
final List<String> anotherString = List.of();
List<String> params = new ArrayList<>(anotherString);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": []
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldBeEmptyWithNullValuesInCollectionSpEL() throws Exception {
final List<String> anotherString = List.of();
List<String> params = new ArrayList<>(anotherString);
// add a null value
params.add(null);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": []
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldIgnoreNullValuesInCollectionSpEL() throws Exception {
final List<String> anotherString = List.of("abc");
List<String> params = new ArrayList<>(anotherString);
// add a null value
params.add(null);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": ["abc"]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test
public void shouldReplaceCollectionParametersSpEL() throws Exception {
final List<QueryParameter> anotherString = List.of(new QueryParameter("hello \"Stranger\""),
new QueryParameter("Another string"));
List<QueryParameter> params = new ArrayList<>(anotherString);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesParameterSpEL", params);
String expected = """
{
"bool":{
"must":{
"terms":{
"name": ["hello \\"Stranger\\"", "Another string"]
}
}
}
}
""";
assertThat(query).isInstanceOf(StringQuery.class);
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
}
@Test // DATAES-552 @Test // DATAES-552
public void shouldReplaceLotsOfParametersCorrectly() throws Exception { public void shouldReplaceLotsOfParametersCorrectly() throws Exception {
@ -205,7 +400,59 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }") @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }")
Mono<Person> findByName(String name); Mono<Person> findByName(String name);
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?#{[0]}' } } } }") @Query("""
{
"bool":{
"must":{
"term":{
"name": "#{#name}"
}
}
}
}
""")
Mono<Person> findByNameSpEL(String name);
@Query("""
{
"bool":{
"must":{
"terms":{
"name": #{#names}
}
}
}
}
""")
Flux<Person> findByNamesSpEL(List<String> names);
@Query("""
{
"bool":{
"must":{
"term":{
"name": "#{#param.value}"
}
}
}
}
""")
Flux<Person> findByParameterPropertySpEL(QueryParameter param);
@Query("""
{
"bool":{
"must":{
"terms":{
"name": #{#names.![value]}
}
}
}
}
""")
Flux<Person> findByNamesParameterSpEL(List<QueryParameter> names);
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '#{[0]}' } } } }")
Flux<Person> findByNameWithExpression(String param0); Flux<Person> findByNameWithExpression(String param0);
@Query(value = "name:(?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)") @Query(value = "name:(?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)")
@ -228,6 +475,18 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
@Query("{ 'bool' : { 'must' : { 'terms' : { 'ages' : ?0 } } } }") @Query("{ 'bool' : { 'must' : { 'terms' : { 'ages' : ?0 } } } }")
Flux<Person> findByAges(List<Integer> ages); Flux<Person> findByAges(List<Integer> ages);
@Query("""
{
"bool":{
"must":{
"terms":{
"age": #{#ages}
}
}
}
}
""")
Flux<Person> findByAgesSpEL(List<Integer> ages);
} }
/** /**

View File

@ -1,108 +0,0 @@
/*
* Copyright 2019-2024 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.elasticsearch.repository.query;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.ParameterAccessor;
/**
* Simple {@link ParameterAccessor} that returns the given parameters unfiltered.
*
* @author Christoph Strobl
* @author Peter-Josef Meisch
*/
class StubParameterAccessor implements ElasticsearchParameterAccessor {
private final Object[] values;
StubParameterAccessor(Object... values) {
this.values = values;
}
@Override
public ScrollPosition getScrollPosition() {
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ParameterAccessor#getPageable()
*/
@Override
public Pageable getPageable() {
return Pageable.unpaged();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ParameterAccessor#getBindableValue(int)
*/
@Override
public Object getBindableValue(int index) {
return values[index];
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ParameterAccessor#hasBindableNullValue()
*/
@Override
public boolean hasBindableNullValue() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ParameterAccessor#getSort()
*/
@Override
public Sort getSort() {
return Sort.unsorted();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ParameterAccessor#iterator()
*/
@Override
public Iterator<Object> iterator() {
return Arrays.asList(values).iterator();
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.repository.query.ElasticsearchParameterAccessor#getValues()
*/
@Override
public Object[] getValues() {
return this.values;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ParameterAccessor#findDynamicProjection()
*/
@Override
public Class<?> findDynamicProjection() {
return null;
}
}

View File

@ -19,12 +19,14 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
/** /**
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4 * @since 4.4
*/ */
@ContextConfiguration(classes = { SimpleReactiveElasticsearchRepositoryELCIntegrationTests.Config.class }) @ContextConfiguration(classes = { SimpleReactiveElasticsearchRepositoryELCIntegrationTests.Config.class })
@ -39,6 +41,14 @@ public class SimpleReactiveElasticsearchRepositoryELCIntegrationTests
IndexNameProvider indexNameProvider() { IndexNameProvider indexNameProvider() {
return new IndexNameProvider("simple-reactive-repository"); return new IndexNameProvider("simple-reactive-repository");
} }
/**
* a normal bean referenced by SpEL in query
*/
@Bean
QueryParameter queryParameter() {
return new QueryParameter("message");
}
} }
} }

View File

@ -16,10 +16,14 @@
package org.springframework.data.elasticsearch.repository.support; package org.springframework.data.elasticsearch.repository.support;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
import static org.springframework.data.elasticsearch.core.query.Query.*; import static org.springframework.data.elasticsearch.core.query.Query.*;
import static org.springframework.data.elasticsearch.utils.IdGenerator.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
import org.springframework.data.repository.query.Param;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
@ -64,6 +68,7 @@ import org.springframework.lang.Nullable;
* @author Christoph Strobl * @author Christoph Strobl
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Jens Schauder * @author Jens Schauder
* @author Haibo Liu
*/ */
@SpringIntegrationTest @SpringIntegrationTest
abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@ -234,6 +239,167 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.verifyComplete(); .verifyComplete();
} }
@Test
void shouldReturnSearchHitsForStringQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
repository.queryByStringSpEL("message")
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldRaiseExceptionForNullStringQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByStringSpEL(null));
assertThat(thrown.getMessage())
.isEqualTo("Parameter value can't be null for SpEL expression '#message' in method 'queryByStringSpEL'" +
" when querying elasticsearch");
}
@Test
void shouldReturnSearchHitsForParameterPropertyQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
QueryParameter param = new QueryParameter("message");
repository.queryByParameterPropertySpEL(param)
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldReturnSearchHitsForBeanQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
repository.queryByBeanPropertySpEL()
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldReturnSearchHitsForCollectionQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
repository.queryByCollectionSpEL(List.of("message"))
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldRaiseExceptionForNullCollectionQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByCollectionSpEL(null));
assertThat(thrown.getMessage())
.isEqualTo("Parameter value can't be null for SpEL expression '#messages' in method 'queryByCollectionSpEL'" +
" when querying elasticsearch");
}
@Test
void shouldNotReturnSearchHitsForEmptyCollectionQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
repository.queryByCollectionSpEL(List.of())
.as(StepVerifier::create) //
.expectNextCount(0) //
.verifyComplete();
}
@Test
void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
List<String> params = new ArrayList<>();
params.add(null);
repository.queryByCollectionSpEL(params)
.as(StepVerifier::create) //
.expectNextCount(0) //
.verifyComplete();
}
@Test
void shouldIgnoreNullValuesInCollectionQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
repository.queryByCollectionSpEL(Arrays.asList("message", null))
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
QueryParameter param = new QueryParameter("message");
repository.queryByParameterPropertyCollectionSpEL(List.of(param))
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() {
bulkIndex(new SampleEntity("id-one", "message"), //
new SampleEntity("id-two", "message"), //
new SampleEntity("id-three", "message")) //
.block();
QueryParameter param = new QueryParameter("message");
repository.queryByParameterPropertyCollectionSpELWithParamAnnotation(List.of(param))
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
.expectNextCount(2) //
.verifyComplete();
}
@Test // DATAES-372 @Test // DATAES-372
void shouldReturnHighlightsOnAnnotatedMethod() { void shouldReturnHighlightsOnAnnotatedMethod() {
@ -787,6 +953,105 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }") @Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }")
Flux<SampleEntity> findAllViaAnnotatedQueryByMessageLikePaged(String message, Pageable pageable); Flux<SampleEntity> findAllViaAnnotatedQueryByMessageLikePaged(String message, Pageable pageable);
/**
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
* will not accept a null parameter as query value.
*/
@Query("""
{
"bool":{
"must":[
{
"term":{
"message": "#{#message}"
}
}
]
}
}
""")
Flux<SearchHit<SampleEntity>> queryByStringSpEL(@Nullable String message);
@Query("""
{
"bool":{
"must":[
{
"term":{
"message": "#{#parameter.value}"
}
}
]
}
}
""")
Flux<SearchHit<SampleEntity>> queryByParameterPropertySpEL(QueryParameter parameter);
@Query("""
{
"bool":{
"must":[
{
"term":{
"message": "#{@queryParameter.value}"
}
}
]
}
}
""")
Flux<SearchHit<SampleEntity>> queryByBeanPropertySpEL();
/**
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
* will not accept a null parameter as query value.
*/
@Query("""
{
"bool":{
"must":[
{
"terms":{
"message": #{#messages}
}
}
]
}
}
""")
Flux<SearchHit<SampleEntity>> queryByCollectionSpEL(@Nullable Collection<String> messages);
@Query("""
{
"bool":{
"must":[
{
"terms":{
"message": #{#parameters.![value]}
}
}
]
}
}
""")
Flux<SearchHit<SampleEntity>> queryByParameterPropertyCollectionSpEL(Collection<QueryParameter> parameters);
@Query("""
{
"bool":{
"must":[
{
"terms":{
"message": #{#e.![value]}
}
}
]
}
}
""")
Flux<SearchHit<SampleEntity>> queryByParameterPropertyCollectionSpELWithParamAnnotation(
@Param("e") Collection<QueryParameter> parameters);
Mono<SampleEntity> findFirstByMessageLike(String message); Mono<SampleEntity> findFirstByMessageLike(String message);
Mono<Long> countAllByMessage(String message); Mono<Long> countAllByMessage(String message);

View File

@ -16,7 +16,7 @@
package org.springframework.data.elasticsearch.utils; package org.springframework.data.elasticsearch.utils;
/** /**
* Class providing an index name with a prefix and a index number * Class providing an index name with a prefix and an index number
* *
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
*/ */