Skip to content

Commit 359e248

Browse files
feat: Add support for Value Expressions for Repository Query methods (#2956)
1 parent 640429b commit 359e248

File tree

18 files changed

+177
-126
lines changed

18 files changed

+177
-126
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
<groupId>org.springframework.data</groupId>
2626
<artifactId>spring-data-neo4j</artifactId>
27-
<version>7.4.0-SNAPSHOT</version>
27+
<version>7.4.0-GH-2954-SNAPSHOT</version>
2828

2929
<name>Spring Data Neo4j</name>
3030
<description>Next generation Object-Graph-Mapping for Spring Data.</description>

src/main/antora/modules/ROOT/pages/appendix/custom-queries.adoc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,11 @@ If an entity has a relationship with the same type to different types of others
367367
If you need such a mapping and also have the need to work with those custom parameters, you have to unroll it accordingly.
368368
One way to do this are correlated subqueries (Neo4j 4.1+ required).
369369

370+
[[custom-queries.expressions]]
371+
== Value Expressions in custom queries
372+
370373
[[custom-queries.spel]]
371-
== Spring Expression Language in custom queries
374+
=== Spring Expression Language in custom queries
372375

373376
{springdocsurl}/core/expressions.html[Spring Expression Language (SpEL)] can be used in custom queries inside `:#{}`.
374377
The colon here refers to a parameter and such an expression should be used where parameters make sense.
@@ -502,3 +505,18 @@ MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.id = $nameOrId OR n.name = $nameO
502505

503506
Notice how we used standard parameter for the `nameOrId`: In most cases there is no need to complicate things here by
504507
adding a SpEL expression.
508+
509+
510+
[[custom-queries.propertyplaceholder]]
511+
=== Property Placeholder resolution in custom queries
512+
513+
Spring's property placeholders can be used in custom queries inside `${}`.
514+
515+
[source,java,indent=0]
516+
[[custom-queries-with-property-placeholder-example]]
517+
.ARepository.java
518+
----
519+
include::example$documentation/repositories/domain_events/ARepository.java[tags=property-placeholder]
520+
----
521+
522+
In the example above, if the property `foo` would be set to `bar` then the `${foo}` block would be resolved to `bar`.

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
import org.springframework.data.repository.core.NamedQueries;
2727
import org.springframework.data.repository.core.RepositoryMetadata;
2828
import org.springframework.data.repository.query.QueryLookupStrategy;
29-
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3029
import org.springframework.data.repository.query.RepositoryQuery;
30+
import org.springframework.data.repository.query.ValueExpressionDelegate;
3131

3232
/**
3333
* Lookup strategy for queries. This is the internal api of the {@code query package}.
@@ -41,14 +41,14 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy {
4141

4242
private final Neo4jMappingContext mappingContext;
4343
private final Neo4jOperations neo4jOperations;
44-
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
44+
private final ValueExpressionDelegate delegate;
4545
private final Configuration configuration;
4646

4747
public Neo4jQueryLookupStrategy(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext,
48-
QueryMethodEvaluationContextProvider evaluationContextProvider, Configuration configuration) {
48+
ValueExpressionDelegate delegate, Configuration configuration) {
4949
this.neo4jOperations = neo4jOperations;
5050
this.mappingContext = mappingContext;
51-
this.evaluationContextProvider = evaluationContextProvider;
51+
this.delegate = delegate;
5252
this.configuration = configuration;
5353
}
5454

@@ -63,10 +63,10 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
6363
String namedQueryName = queryMethod.getNamedQueryName();
6464

6565
if (namedQueries.hasQuery(namedQueryName)) {
66-
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
66+
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, queryMethod,
6767
namedQueries.getQuery(namedQueryName), factory);
6868
} else if (queryMethod.hasQueryAnnotation()) {
69-
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
69+
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, queryMethod,
7070
factory);
7171
} else if (queryMethod.isCypherBasedProjection()) {
7272
return CypherdslBasedQuery.create(neo4jOperations, mappingContext, queryMethod, factory, Renderer.getRenderer(configuration)::render);

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.data.domain.ScrollPosition;
4545
import org.springframework.data.domain.ScrollPosition.Direction;
4646
import org.springframework.data.domain.Window;
47+
import org.springframework.data.expression.ValueExpressionParser;
4748
import org.springframework.data.geo.Box;
4849
import org.springframework.data.geo.Circle;
4950
import org.springframework.data.geo.Distance;
@@ -60,7 +61,6 @@
6061
import org.springframework.data.repository.query.ResultProcessor;
6162
import org.springframework.data.repository.query.ReturnedType;
6263
import org.springframework.data.util.TypeInformation;
63-
import org.springframework.expression.spel.standard.SpelExpressionParser;
6464
import org.springframework.lang.Nullable;
6565
import org.springframework.util.Assert;
6666

@@ -74,7 +74,7 @@
7474
*/
7575
abstract class Neo4jQuerySupport {
7676

77-
protected static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser();
77+
protected static final ValueExpressionParser SPEL_EXPRESSION_PARSER = ValueExpressionParser.create();
7878

7979
protected final Neo4jMappingContext mappingContext;
8080
protected final Neo4jQueryMethod queryMethod;

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@
2929
import org.neo4j.cypherdsl.support.schema_name.SchemaNames;
3030
import org.springframework.data.domain.Pageable;
3131
import org.springframework.data.domain.Sort;
32+
import org.springframework.data.expression.ValueEvaluationContext;
33+
import org.springframework.data.expression.ValueExpression;
34+
import org.springframework.data.expression.ValueExpressionParser;
3235
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
3336
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
3437
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
3538
import org.springframework.data.repository.core.EntityMetadata;
36-
import org.springframework.expression.Expression;
37-
import org.springframework.expression.ParserContext;
38-
import org.springframework.expression.spel.standard.SpelExpressionParser;
3939
import org.springframework.expression.spel.support.StandardEvaluationContext;
4040
import org.springframework.lang.Nullable;
4141
import org.springframework.util.Assert;
@@ -233,7 +233,7 @@ public Target getTarget() {
233233
* @return A query in which some SpEL expression have been replaced with the result of evaluating the expression
234234
*/
235235
public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMappingContext mappingContext, EntityMetadata<?> metadata,
236-
SpelExpressionParser parser) {
236+
ValueExpressionParser parser) {
237237

238238
Assert.notNull(query, "query must not be null");
239239
Assert.notNull(metadata, "metadata must not be null");
@@ -243,10 +243,10 @@ public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMap
243243
return query;
244244
}
245245

246-
StandardEvaluationContext evalContext = new StandardEvaluationContext();
246+
ValueEvaluationContext evalContext = ValueEvaluationContext.of(null, new StandardEvaluationContext());
247247
Neo4jPersistentEntity<?> requiredPersistentEntity = mappingContext
248248
.getRequiredPersistentEntity(metadata.getJavaType());
249-
evalContext.setVariable(ENTITY_NAME, requiredPersistentEntity.getStaticLabels()
249+
evalContext.getEvaluationContext().setVariable(ENTITY_NAME, requiredPersistentEntity.getStaticLabels()
250250
.stream()
251251
.map(l -> {
252252
Matcher matcher = LABEL_AND_TYPE_QUOTATION.matcher(l);
@@ -256,9 +256,9 @@ public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMap
256256

257257
query = potentiallyQuoteExpressionsParameter(query);
258258

259-
Expression expr = parser.parseExpression(query, ParserContext.TEMPLATE_EXPRESSION);
259+
ValueExpression expr = parser.parse(query);
260260

261-
String result = expr.getValue(evalContext, String.class);
261+
String result = (String) expr.evaluate(evalContext);
262262

263263
if (result == null) {
264264
return query;

src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
import org.springframework.data.repository.core.NamedQueries;
2727
import org.springframework.data.repository.core.RepositoryMetadata;
2828
import org.springframework.data.repository.query.QueryLookupStrategy;
29-
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3029
import org.springframework.data.repository.query.RepositoryQuery;
30+
import org.springframework.data.repository.query.ValueExpressionDelegate;
3131

3232
/**
3333
* Lookup strategy for queries. This is the internal api of the {@code query package}.
@@ -41,14 +41,14 @@ public final class ReactiveNeo4jQueryLookupStrategy implements QueryLookupStrate
4141

4242
private final ReactiveNeo4jOperations neo4jOperations;
4343
private final Neo4jMappingContext mappingContext;
44-
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
44+
private final ValueExpressionDelegate delegate;
4545
private final Configuration configuration;
4646

4747
public ReactiveNeo4jQueryLookupStrategy(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext,
48-
QueryMethodEvaluationContextProvider evaluationContextProvider, Configuration configuration) {
48+
ValueExpressionDelegate delegate, Configuration configuration) {
4949
this.neo4jOperations = neo4jOperations;
5050
this.mappingContext = mappingContext;
51-
this.evaluationContextProvider = evaluationContextProvider;
51+
this.delegate = delegate;
5252
this.configuration = configuration;
5353
}
5454

@@ -63,10 +63,10 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
6363
String namedQueryName = queryMethod.getNamedQueryName();
6464

6565
if (namedQueries.hasQuery(namedQueryName)) {
66-
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider,
66+
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate,
6767
queryMethod, namedQueries.getQuery(namedQueryName), projectionFactory);
6868
} else if (queryMethod.hasQueryAnnotation()) {
69-
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider,
69+
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate,
7070
queryMethod, projectionFactory);
7171
} else if (queryMethod.isCypherBasedProjection()) {
7272
return ReactiveCypherdslBasedQuery.create(neo4jOperations, mappingContext, queryMethod, projectionFactory, Renderer.getRenderer(configuration)::render);

src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@
3333
import org.springframework.data.projection.ProjectionFactory;
3434
import org.springframework.data.repository.query.Parameter;
3535
import org.springframework.data.repository.query.Parameters;
36-
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3736
import org.springframework.data.repository.query.RepositoryQuery;
38-
import org.springframework.data.repository.query.SpelEvaluator;
3937
import org.springframework.data.repository.query.SpelQueryContext;
40-
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
38+
import org.springframework.data.repository.query.ValueExpressionDelegate;
39+
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
4140
import org.springframework.lang.Nullable;
4241
import org.springframework.util.Assert;
4342
import org.springframework.util.StringUtils;
@@ -65,24 +64,22 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery {
6564
static final SpelQueryContext SPEL_QUERY_CONTEXT = SpelQueryContext
6665
.of(ReactiveStringBasedNeo4jQuery::parameterNameSource, ReactiveStringBasedNeo4jQuery::replacementSource);
6766

68-
/**
69-
* Used to evaluate the expression found while parsing the cypher template of this query against the actual parameters
70-
* with the help of the formal parameters during the building of the {@link PreparedQuery}.
71-
*/
72-
private final SpelEvaluator spelEvaluator;
67+
private final ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter queryRewriter;
68+
69+
private final ValueExpressionQueryRewriter.QueryExpressionEvaluator parsedQuery;
7370

7471
/**
7572
* Create a {@link ReactiveStringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The
7673
* annotation is expected to have a value.
7774
*
7875
* @param neo4jOperations reactive Neo4j operations
7976
* @param mappingContext a Neo4jMappingContext instance
80-
* @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance
77+
* @param delegate a ValueExpressionDelegate instance
8178
* @param queryMethod the query method
8279
* @return A new instance of a String based Neo4j query.
8380
*/
8481
static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperations,
85-
Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider,
82+
Neo4jMappingContext mappingContext, ValueExpressionDelegate delegate,
8683
Neo4jQueryMethod queryMethod, ProjectionFactory factory) {
8784

8885
Query queryAnnotation = queryMethod.getQueryAnnotation()
@@ -91,7 +88,7 @@ static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperati
9188
String cypherTemplate = Optional.ofNullable(queryAnnotation.value()).filter(StringUtils::hasText)
9289
.orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not"));
9390

94-
return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
91+
return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod,
9592
cypherTemplate, Neo4jQueryType.fromDefinition(queryAnnotation), factory);
9693
}
9794

@@ -100,30 +97,30 @@ static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperati
10097
*
10198
* @param neo4jOperations reactive Neo4j operations
10299
* @param mappingContext a Neo4jMappingContext instance
103-
* @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance
100+
* @param delegate a ValueExpressionDelegate instance
104101
* @param queryMethod the query method
105102
* @param cypherTemplate The template to use.
106103
* @return A new instance of a String based Neo4j query.
107104
*/
108105
static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperations,
109-
Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider,
106+
Neo4jMappingContext mappingContext, ValueExpressionDelegate delegate,
110107
Neo4jQueryMethod queryMethod, String cypherTemplate, ProjectionFactory factory) {
111108

112109
Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template");
113110

114-
return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
111+
return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod,
115112
cypherTemplate, Neo4jQueryType.DEFAULT, factory);
116113
}
117114

118115
private ReactiveStringBasedNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext,
119-
QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod,
116+
ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod,
120117
String cypherTemplate, Neo4jQueryType queryType, ProjectionFactory factory) {
121118

122119
super(neo4jOperations, mappingContext, queryMethod, queryType, factory);
123120

124-
cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER);
125-
SpelExtractor spelExtractor = SPEL_QUERY_CONTEXT.parse(cypherTemplate);
126-
this.spelEvaluator = new SpelEvaluator(evaluationContextProvider, queryMethod.getParameters(), spelExtractor);
121+
this.queryRewriter = ValueExpressionQueryRewriter.of(delegate,
122+
StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource);
123+
this.parsedQuery = queryRewriter.parse(cypherTemplate, queryMethod.getParameters());
127124
}
128125

129126
@Override
@@ -134,7 +131,7 @@ protected <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType
134131
Map<String, Object> boundParameters = bindParameters(parameterAccessor);
135132
QueryContext queryContext = new QueryContext(
136133
queryMethod.getRepositoryName() + "." + queryMethod.getName(),
137-
spelEvaluator.getQueryString(),
134+
parsedQuery.getQueryString(),
138135
boundParameters
139136
);
140137

@@ -153,7 +150,7 @@ Map<String, Object> bindParameters(Neo4jParameterAccessor parameterAccessor) {
153150
Map<String, Object> resolvedParameters = new HashMap<>();
154151

155152
// Values from the parameter accessor can only get converted after evaluation
156-
for (Map.Entry<String, Object> evaluatedParam : spelEvaluator.evaluate(parameterAccessor.getValues()).entrySet()) {
153+
for (Map.Entry<String, Object> evaluatedParam : parsedQuery.evaluate(parameterAccessor.getValues()).entrySet()) {
157154
Object value = evaluatedParam.getValue();
158155
if (!(evaluatedParam.getValue() instanceof Neo4jSpelSupport.LiteralReplacement)) {
159156
Neo4jQuerySupport.logParameterIfNull(evaluatedParam.getKey(), value);

0 commit comments

Comments
 (0)