Skip to content

Add support for Value Expressions for Repository Query methods #2956

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 5 commits into from
Oct 10, 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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>7.4.0-SNAPSHOT</version>
<version>7.4.0-GH-2954-SNAPSHOT</version>

<name>Spring Data Neo4j</name>
<description>Next generation Object-Graph-Mapping for Spring Data.</description>
Expand Down
20 changes: 19 additions & 1 deletion src/main/antora/modules/ROOT/pages/appendix/custom-queries.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,11 @@ If an entity has a relationship with the same type to different types of others
If you need such a mapping and also have the need to work with those custom parameters, you have to unroll it accordingly.
One way to do this are correlated subqueries (Neo4j 4.1+ required).

[[custom-queries.expressions]]
== Value Expressions in custom queries

[[custom-queries.spel]]
== Spring Expression Language in custom queries
=== Spring Expression Language in custom queries

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

Notice how we used standard parameter for the `nameOrId`: In most cases there is no need to complicate things here by
adding a SpEL expression.


[[custom-queries.propertyplaceholder]]
=== Property Placeholder resolution in custom queries

Spring's property placeholders can be used in custom queries inside `${}`.

[source,java,indent=0]
[[custom-queries-with-property-placeholder-example]]
.ARepository.java
----
include::example$documentation/repositories/domain_events/ARepository.java[tags=property-placeholder]
----

In the example above, if the property `foo` would be set to `bar` then the `${foo}` block would be resolved to `bar`.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;

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

private final Neo4jMappingContext mappingContext;
private final Neo4jOperations neo4jOperations;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ValueExpressionDelegate delegate;
private final Configuration configuration;

public Neo4jQueryLookupStrategy(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext,
QueryMethodEvaluationContextProvider evaluationContextProvider, Configuration configuration) {
ValueExpressionDelegate delegate, Configuration configuration) {
this.neo4jOperations = neo4jOperations;
this.mappingContext = mappingContext;
this.evaluationContextProvider = evaluationContextProvider;
this.delegate = delegate;
this.configuration = configuration;
}

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

if (namedQueries.hasQuery(namedQueryName)) {
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, queryMethod,
namedQueries.getQuery(namedQueryName), factory);
} else if (queryMethod.hasQueryAnnotation()) {
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
return StringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate, queryMethod,
factory);
} else if (queryMethod.isCypherBasedProjection()) {
return CypherdslBasedQuery.create(neo4jOperations, mappingContext, queryMethod, factory, Renderer.getRenderer(configuration)::render);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.ScrollPosition.Direction;
import org.springframework.data.domain.Window;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
Expand All @@ -60,7 +61,6 @@
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

Expand All @@ -74,7 +74,7 @@
*/
abstract class Neo4jQuerySupport {

protected static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser();
protected static final ValueExpressionParser SPEL_EXPRESSION_PARSER = ValueExpressionParser.create();

protected final Neo4jMappingContext mappingContext;
protected final Neo4jQueryMethod queryMethod;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@
import org.neo4j.cypherdsl.support.schema_name.SchemaNames;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.repository.core.EntityMetadata;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -233,7 +233,7 @@ public Target getTarget() {
* @return A query in which some SpEL expression have been replaced with the result of evaluating the expression
*/
public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMappingContext mappingContext, EntityMetadata<?> metadata,
SpelExpressionParser parser) {
ValueExpressionParser parser) {

Assert.notNull(query, "query must not be null");
Assert.notNull(metadata, "metadata must not be null");
Expand All @@ -243,10 +243,10 @@ public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMap
return query;
}

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

query = potentiallyQuoteExpressionsParameter(query);

Expression expr = parser.parseExpression(query, ParserContext.TEMPLATE_EXPRESSION);
ValueExpression expr = parser.parse(query);

String result = expr.getValue(evalContext, String.class);
String result = (String) expr.evaluate(evalContext);

if (result == null) {
return query;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;

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

private final ReactiveNeo4jOperations neo4jOperations;
private final Neo4jMappingContext mappingContext;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ValueExpressionDelegate delegate;
private final Configuration configuration;

public ReactiveNeo4jQueryLookupStrategy(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext,
QueryMethodEvaluationContextProvider evaluationContextProvider, Configuration configuration) {
ValueExpressionDelegate delegate, Configuration configuration) {
this.neo4jOperations = neo4jOperations;
this.mappingContext = mappingContext;
this.evaluationContextProvider = evaluationContextProvider;
this.delegate = delegate;
this.configuration = configuration;
}

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

if (namedQueries.hasQuery(namedQueryName)) {
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider,
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate,
queryMethod, namedQueries.getQuery(namedQueryName), projectionFactory);
} else if (queryMethod.hasQueryAnnotation()) {
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, evaluationContextProvider,
return ReactiveStringBasedNeo4jQuery.create(neo4jOperations, mappingContext, delegate,
queryMethod, projectionFactory);
} else if (queryMethod.isCypherBasedProjection()) {
return ReactiveCypherdslBasedQuery.create(neo4jOperations, mappingContext, queryMethod, projectionFactory, Renderer.getRenderer(configuration)::render);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.SpelEvaluator;
import org.springframework.data.repository.query.SpelQueryContext;
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -65,24 +64,22 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery {
static final SpelQueryContext SPEL_QUERY_CONTEXT = SpelQueryContext
.of(ReactiveStringBasedNeo4jQuery::parameterNameSource, ReactiveStringBasedNeo4jQuery::replacementSource);

/**
* Used to evaluate the expression found while parsing the cypher template of this query against the actual parameters
* with the help of the formal parameters during the building of the {@link PreparedQuery}.
*/
private final SpelEvaluator spelEvaluator;
private final ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter queryRewriter;

private final ValueExpressionQueryRewriter.QueryExpressionEvaluator parsedQuery;

/**
* Create a {@link ReactiveStringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The
* annotation is expected to have a value.
*
* @param neo4jOperations reactive Neo4j operations
* @param mappingContext a Neo4jMappingContext instance
* @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance
* @param delegate a ValueExpressionDelegate instance
* @param queryMethod the query method
* @return A new instance of a String based Neo4j query.
*/
static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperations,
Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider,
Neo4jMappingContext mappingContext, ValueExpressionDelegate delegate,
Neo4jQueryMethod queryMethod, ProjectionFactory factory) {

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

return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod,
cypherTemplate, Neo4jQueryType.fromDefinition(queryAnnotation), factory);
}

Expand All @@ -100,30 +97,30 @@ static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperati
*
* @param neo4jOperations reactive Neo4j operations
* @param mappingContext a Neo4jMappingContext instance
* @param evaluationContextProvider a QueryMethodEvaluationContextProvider instance
* @param delegate a ValueExpressionDelegate instance
* @param queryMethod the query method
* @param cypherTemplate The template to use.
* @return A new instance of a String based Neo4j query.
*/
static ReactiveStringBasedNeo4jQuery create(ReactiveNeo4jOperations neo4jOperations,
Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider,
Neo4jMappingContext mappingContext, ValueExpressionDelegate delegate,
Neo4jQueryMethod queryMethod, String cypherTemplate, ProjectionFactory factory) {

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

return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod,
return new ReactiveStringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod,
cypherTemplate, Neo4jQueryType.DEFAULT, factory);
}

private ReactiveStringBasedNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext,
QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod,
ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod,
String cypherTemplate, Neo4jQueryType queryType, ProjectionFactory factory) {

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

cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER);
SpelExtractor spelExtractor = SPEL_QUERY_CONTEXT.parse(cypherTemplate);
this.spelEvaluator = new SpelEvaluator(evaluationContextProvider, queryMethod.getParameters(), spelExtractor);
this.queryRewriter = ValueExpressionQueryRewriter.of(delegate,
StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource);
this.parsedQuery = queryRewriter.parse(cypherTemplate, queryMethod.getParameters());
}

@Override
Expand All @@ -134,7 +131,7 @@ protected <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType
Map<String, Object> boundParameters = bindParameters(parameterAccessor);
QueryContext queryContext = new QueryContext(
queryMethod.getRepositoryName() + "." + queryMethod.getName(),
spelEvaluator.getQueryString(),
parsedQuery.getQueryString(),
boundParameters
);

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

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