Skip to content

Add support for SpEL in @Query #2826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
====
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]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
Expand Down Expand Up @@ -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);
}
----

====
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
*
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 3.2
*/
abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery {
Expand Down Expand Up @@ -112,7 +113,7 @@ private Object execute(ElasticsearchParametersParameterAccessor parameterAccesso
* @param accessor must not be {@literal null}.
* @return
*/
protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor);
protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor);

private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
Converter<Object, Object> resultProcessing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* @author Christoph Strobl
* @since 3.2
*/
class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
public class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
implements ElasticsearchParameterAccessor {

private final Object[] values;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
*/
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.
// 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
protected final Method method;
protected final Class<?> unwrappedReturnType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
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.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;

/**
Expand All @@ -30,16 +31,21 @@
* @author Mark Paluch
* @author Taylor Ono
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {

private final String queryString;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;

public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString) {
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations);
Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");

this.queryString = queryString;
this.evaluationContextProvider = evaluationContextProvider;
}

@Override
Expand All @@ -58,12 +64,14 @@ protected boolean isExistsQuery() {
}

protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {

String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
.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());
return query;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@
package org.springframework.data.elasticsearch.repository.query;

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.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;

/**
* @author Christoph Strobl
* @author Taylor Ono
* @author Haibo Liu
* @since 3.2
*/
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {

private final String query;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;

public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
Expand All @@ -43,14 +47,17 @@ public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQuery

super(queryMethod, operations);
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}

@Override
protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(
getElasticsearchOperations().getElasticsearchConverter().getConversionService()).replacePlaceholders(this.query,
parameterAccessor);
return new StringQuery(queryString);
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(getElasticsearchOperations().getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.query, parameterAccessor);

QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider);
return new StringQuery(evaluator.evaluate());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod query
}

@Override
protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) {
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery();

if (tree.isLimiting()) {
Expand Down
Loading