From 48affe6cc5f63055ae60a30d3bf8c944d337ca60 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 7 Oct 2024 11:07:40 +0200 Subject: [PATCH 1/5] Preparing to work on the issue --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 252650fca3..59f29c4bd7 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.springframework.data spring-data-neo4j - 7.4.0-SNAPSHOT + 7.4.0-GH-2954-SNAPSHOT Spring Data Neo4j Next generation Object-Graph-Mapping for Spring Data. @@ -110,7 +110,7 @@ ${skipTests} - 3.4.0-SNAPSHOT + 3.4.0-GH-3049-SNAPSHOT 2.2.0 From 9deb678e5888905ffcc02a4fee53fd7be679695b Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 8 Oct 2024 16:57:58 +0200 Subject: [PATCH 2/5] WIP --- .../query/Neo4jQueryLookupStrategy.java | 12 ++-- .../repository/query/Neo4jQuerySupport.java | 4 +- .../repository/query/Neo4jSpelSupport.java | 16 +++--- .../query/StringBasedNeo4jQuery.java | 56 +++++++++---------- .../support/Neo4jRepositoryFactory.java | 10 +--- .../query/Neo4jSpelSupportTest.java | 14 ++--- .../repository/query/RepositoryQueryTest.java | 46 ++++++++------- 7 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java index 73d740240f..469703f1d4 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQueryLookupStrategy.java @@ -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}. @@ -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; } @@ -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); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java index 385e37a459..4d1d74d898 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java @@ -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; @@ -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; @@ -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; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java index 837f6277e4..61426179ea 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java @@ -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; @@ -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"); @@ -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); @@ -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; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index 906722fa3b..c4039f87ff 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -36,10 +36,9 @@ import org.springframework.data.neo4j.repository.query.Neo4jSpelSupport.LiteralReplacement; import org.springframework.data.projection.ProjectionFactory; 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.ValueExpressionDelegate; +import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -61,12 +60,6 @@ */ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { - /** - * Used for extracting SpEL expressions inside Cypher query templates. - */ - static final SpelQueryContext SPEL_QUERY_CONTEXT = SpelQueryContext.of(StringBasedNeo4jQuery::parameterNameSource, - StringBasedNeo4jQuery::replacementSource); - private final static String COMMENT_OR_WHITESPACE_GROUP = "(?:\\s|/\\\\*.*?\\\\*/|//.*?$)"; static final Pattern SKIP_AND_LIMIT_WITH_PLACEHOLDER_PATTERN = Pattern .compile("" @@ -81,12 +74,16 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { * 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.QueryExpressionEvaluator parsedQuery; /** * An optional evaluator for a count query if such a query is present. */ - private final Optional spelEvaluatorForCountQuery; + private final Optional parsedCountQuery; + + private final ValueExpressionDelegate delegate; + + private final ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter queryRewriter; /** * Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The @@ -94,12 +91,12 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { * * @param neo4jOperations the 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 StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, ProjectionFactory factory) { Query queryAnnotation = queryMethod.getQueryAnnotation() @@ -134,7 +131,7 @@ static StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappin cypherTemplate, queryMethod.getRepositoryName(), queryMethod.getName())); } - return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod, + return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, cypherTemplate, Neo4jQueryType.fromDefinition(queryAnnotation), factory); } @@ -143,35 +140,36 @@ static StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappin * * @param neo4jOperations the 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 StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, String cypherTemplate, ProjectionFactory factory) { Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template"); - return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, evaluationContextProvider, queryMethod, + return new StringBasedNeo4jQuery(neo4jOperations, mappingContext, delegate, queryMethod, cypherTemplate, Neo4jQueryType.DEFAULT, factory); } private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, - QueryMethodEvaluationContextProvider evaluationContextProvider, Neo4jQueryMethod queryMethod, + ValueExpressionDelegate delegate, Neo4jQueryMethod queryMethod, String cypherTemplate, Neo4jQueryType queryType, ProjectionFactory factory) { super(neo4jOperations, mappingContext, queryMethod, queryType, factory); - Parameters methodParameters = queryMethod.getParameters(); + this.delegate = delegate; cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER); - this.spelEvaluator = new SpelEvaluator( - evaluationContextProvider, methodParameters, SPEL_QUERY_CONTEXT.parse(cypherTemplate)); - this.spelEvaluatorForCountQuery = queryMethod.getQueryAnnotation() + this.queryRewriter = ValueExpressionQueryRewriter.of(delegate, + StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource); + this.parsedQuery = queryRewriter.parse(cypherTemplate, queryMethod.getParameters()); + this.parsedCountQuery = queryMethod.getQueryAnnotation() .map(Query::countQuery) - .map(q -> Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(q, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER)) - .map(countQuery -> new SpelEvaluator(evaluationContextProvider, methodParameters, SPEL_QUERY_CONTEXT.parse(countQuery))); + .map(q -> Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(q, mappingContext, queryMethod.getEntityInformation(), delegate)) + .map(string -> queryRewriter.parse(string, queryMethod.getParameters())); } @Override @@ -184,7 +182,7 @@ protected PreparedQuery prepareQuery(Class returnedType Map boundParameters = bindParameters(parameterAccessor, true, limitModifier); QueryContext queryContext = new QueryContext( queryMethod.getRepositoryName() + "." + queryMethod.getName(), - spelEvaluator.getQueryString(), + parsedQuery.getQueryString(), boundParameters ); @@ -204,7 +202,7 @@ Map bindParameters(Neo4jParameterAccessor parameterAccessor, boo Map resolvedParameters = new HashMap<>(); // Values from the parameter accessor can only get converted after evaluation - for (Entry evaluatedParam : spelEvaluator.evaluate(parameterAccessor.getValues()).entrySet()) { + for (Entry evaluatedParam : parsedQuery.evaluate(parameterAccessor.getValues()).entrySet()) { Object value = evaluatedParam.getValue(); if (!(evaluatedParam.getValue() instanceof LiteralReplacement)) { Neo4jQuerySupport.logParameterIfNull(evaluatedParam.getKey(), value); @@ -237,7 +235,7 @@ Map bindParameters(Neo4jParameterAccessor parameterAccessor, boo @Override protected Optional> getCountQuery(Neo4jParameterAccessor parameterAccessor) { - return spelEvaluatorForCountQuery.map(SpelEvaluator::getQueryString) + return parsedCountQuery.map(ValueExpressionQueryRewriter.QueryExpressionEvaluator::getQueryString) .map(countQuery -> { Map boundParameters = bindParameters(parameterAccessor, false, UnaryOperator.identity()); QueryContext queryContext = new QueryContext( @@ -258,7 +256,7 @@ protected Optional> getCountQuery(Neo4jParameterAccessor par * @param originalSpelExpression Not used for configuring parameter names atm. * @return A new parameter name for the given index. */ - private static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { + static String parameterNameSource(int index, @SuppressWarnings("unused") String originalSpelExpression) { return "__SpEL__" + index; } @@ -268,7 +266,7 @@ private static String parameterNameSource(int index, @SuppressWarnings("unused") * @param parameterName name of the parameter * @return The name of the parameter in its native Cypher form. */ - private static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { + static String replacementSource(@SuppressWarnings("unused") String originalPrefix, String parameterName) { return "$" + parameterName; } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 1efd3d1fe8..8bea468ad9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -40,6 +40,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Factory to create {@link Neo4jRepository} instances. @@ -135,15 +136,10 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { .getIfAvailable(Configuration::defaultConfig); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) - */ @Override protected Optional getQueryLookupStrategy(Key key, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, mappingContext, evaluationContextProvider, cypherDSLConfiguration)); + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); } @Override diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java index a2f20cc611..10d2a93a72 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java @@ -39,13 +39,14 @@ import org.junit.jupiter.params.provider.CsvSource; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.repository.query.Neo4jSpelSupport.LiteralReplacement; import org.springframework.data.repository.core.EntityMetadata; -import org.springframework.data.repository.query.SpelQueryContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.util.ReflectionUtils; /** @@ -188,11 +189,9 @@ void shouldUnquoteParameterExpressionsCorrectly(String quoted, String expected) @Test void moreThan10SpelEntriesShouldWork() { - SpelQueryContext spelQueryContext = StringBasedNeo4jQuery.SPEL_QUERY_CONTEXT; - StringBuilder template = new StringBuilder("MATCH (user:User) WHERE "); String query; - SpelQueryContext.SpelExtractor spelExtractor; + ValueExpressionQueryRewriter.ParsedQuery spelExtractor; class R implements LiteralReplacement { private final String value; @@ -218,7 +217,8 @@ public Target getTarget() { parameters.put("__SpEL__" + i, new R("'x" + i + "'")); } template.delete(template.length() - 4, template.length()); - spelExtractor = spelQueryContext.parse(template.toString()); + spelExtractor = ValueExpressionQueryRewriter.of(ValueExpressionDelegate.create(), + StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource).parse(template.toString()); query = spelExtractor.getQueryString(); Neo4jQuerySupport.QueryContext qc = new Neo4jQuerySupport.QueryContext("n/a", query, parameters); assertThat(qc.query).isEqualTo( @@ -240,7 +240,7 @@ void shouldReplaceStaticLabels() { String query = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery( "MATCH (n:#{#staticLabels}) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n", - new Neo4jMappingContext(), (EntityMetadata) () -> BikeNode.class, new SpelExpressionParser()); + new Neo4jMappingContext(), (EntityMetadata) () -> BikeNode.class, ValueExpressionParser.create()); assertThat(query).isEqualTo("MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n"); diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java index d790fa266c..108bb86766 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java @@ -57,6 +57,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.PreparedQuery; @@ -71,12 +72,12 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.Param; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.util.ReflectionUtils; /** @@ -200,7 +201,7 @@ void shouldSelectPartTreeNeo4jQuery() { final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, Configuration.defaultConfig()); + ValueExpressionDelegate.create(), Configuration.defaultConfig()); RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("findById", Object.class), TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, namedQueries); @@ -211,7 +212,7 @@ void shouldSelectPartTreeNeo4jQuery() { void shouldSelectStringBasedNeo4jQuery() { final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(neo4jOperations, - neo4jMappingContext, QueryMethodEvaluationContextProvider.DEFAULT, Configuration.defaultConfig()); + neo4jMappingContext, ValueExpressionDelegate.create(), Configuration.defaultConfig()); RepositoryQuery query = lookupStrategy.resolveQuery(queryMethod("annotatedQueryWithValidTemplate"), TEST_REPOSITORY_METADATA, PROJECTION_FACTORY, namedQueries); @@ -226,7 +227,7 @@ void shouldSelectStringBasedNeo4jQueryForNamedQuery() { when(namedQueries.getQuery(namedQueryName)).thenReturn("MATCH (n) RETURN n"); final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(neo4jOperations, - neo4jMappingContext, QueryMethodEvaluationContextProvider.DEFAULT, Configuration.defaultConfig()); + neo4jMappingContext, ValueExpressionDelegate.create(), Configuration.defaultConfig()); RepositoryQuery query = lookupStrategy .resolveQuery(queryMethod("findAllByANamedQuery"), TEST_REPOSITORY_METADATA, @@ -241,12 +242,13 @@ class StringBasedNeo4jQueryTest { @Test void spelQueryContextShouldBeConfiguredCorrectly() { - - SpelQueryContext spelQueryContext = StringBasedNeo4jQuery.SPEL_QUERY_CONTEXT; + ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter spelQueryContext = ValueExpressionQueryRewriter.of( + ValueExpressionDelegate.create(), StringBasedNeo4jQuery::parameterNameSource, + StringBasedNeo4jQuery::replacementSource); String template; String query; - SpelQueryContext.SpelExtractor spelExtractor; + ValueExpressionQueryRewriter.ParsedQuery spelExtractor; template = "MATCH (user:User) WHERE user.name = :#{#searchUser.name} and user.middleName = ?#{#searchUser.middleName} RETURN user"; @@ -272,7 +274,7 @@ void shouldDetectInvalidAnnotation() { assertThatExceptionOfType(MappingException.class) .isThrownBy(() -> StringBasedNeo4jQuery .create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory)) + ValueExpressionDelegate.create(), method, projectionFactory)) .withMessage("Expected @Query annotation to have a value, but it did not"); } @@ -283,7 +285,7 @@ void shouldDetectMissingCountQuery() { assertThatExceptionOfType(MappingException.class) .isThrownBy(() -> StringBasedNeo4jQuery .create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory)) + ValueExpressionDelegate.create(), method, projectionFactory)) .withMessage("Expected paging query method to have a count query"); } @@ -292,7 +294,7 @@ void shouldAllowMissingCountOnSlicedQuery(LogbackCapture logbackCapture) { Neo4jQueryMethod method = neo4jQueryMethod("missingCountQueryOnSlice", Pageable.class); StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory); + ValueExpressionDelegate.create(), method, projectionFactory); assertThat(logbackCapture.getFormattedMessages()) .anyMatch(s -> s.matches( "(?s)You provided a string based query returning a slice for '.*\\.missingCountQueryOnSlice'\\. You might want to consider adding a count query if more slices than you expect are returned\\.")); @@ -303,7 +305,7 @@ void shouldDetectMissingPlaceHoldersOnPagedQuery(LogbackCapture logbackCapture) Neo4jQueryMethod method = neo4jQueryMethod("missingPlaceHoldersOnPage", Pageable.class); StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory); + ValueExpressionDelegate.create(), method, projectionFactory); assertThat(logbackCapture.getFormattedMessages()) .anyMatch(s -> s.matches( "(?s)The custom query.*MATCH \\(n:Page\\) return n.*for '.*\\.missingPlaceHoldersOnPage' is supposed to work with a page or slicing query but does not have the required parameter placeholders `\\$skip` and `\\$limit`\\..*")); @@ -314,7 +316,7 @@ void shouldDetectMissingPlaceHoldersOnSlicedQuery(LogbackCapture logbackCapture) Neo4jQueryMethod method = neo4jQueryMethod("missingPlaceHoldersOnSlice", Pageable.class); StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory); + ValueExpressionDelegate.create(), method, projectionFactory); assertThat(logbackCapture.getFormattedMessages()) .anyMatch(s -> s.matches( "(?s)The custom query.*MATCH \\(n:Slice\\) return n.*is supposed to work with a page or slicing query but does not have the required parameter placeholders `\\$skip` and `\\$limit`\\..*")); @@ -326,7 +328,7 @@ void shouldWarnWhenUsingSortedAndCustomQuery(LogbackCapture logbackCapture) { Neo4jQueryMethod method = neo4jQueryMethod("findAllExtendedEntitiesWithCustomQuery", Sort.class); AbstractNeo4jQuery query = StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory); + ValueExpressionDelegate.create(), method, projectionFactory); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), @@ -353,7 +355,7 @@ void shouldWarnWhenUsingSortedPageable(LogbackCapture logbackCapture) { Neo4jQueryMethod method = neo4jQueryMethod("noWarningsPerSe", Pageable.class); AbstractNeo4jQuery query = StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory); + ValueExpressionDelegate.create(), method, projectionFactory); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), new Object[] { PageRequest.of(1, 1, Sort.by("name").ascending()) }); @@ -383,9 +385,11 @@ void orderBySpelShouldWork(LogbackCapture logbackCapture) { new Neo4jEvaluationContextExtension()); context.refresh(); + ValueExpressionDelegate delegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(context), ValueExpressionParser.create()); + Neo4jQueryMethod method = neo4jQueryMethod("orderBySpel", Pageable.class); StringBasedNeo4jQuery query = StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - new ExtensionAwareQueryMethodEvaluationContextProvider(context.getBeanFactory()), method, projectionFactory); + delegate, method, projectionFactory); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), @@ -415,9 +419,11 @@ void literalReplacementsShouldWork() { new Neo4jEvaluationContextExtension()); context.refresh(); + ValueExpressionDelegate delegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(context), ValueExpressionParser.create()); + Neo4jQueryMethod method = neo4jQueryMethod("makeStaticThingsDynamic", String.class, String.class, String.class, String.class, Sort.class); StringBasedNeo4jQuery query = StringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - new ExtensionAwareQueryMethodEvaluationContextProvider(context.getBeanFactory()), method, projectionFactory); + delegate, method, projectionFactory); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), @@ -442,7 +448,7 @@ void shouldBindParameters() { String.class); StringBasedNeo4jQuery repositoryQuery = spy(StringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory)); + neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)); // skip conversion doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); @@ -461,7 +467,7 @@ void shouldResolveNamedParameters() { org.neo4j.driver.types.Point.class, String.class, String.class); StringBasedNeo4jQuery repositoryQuery = spy(StringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, QueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory)); + neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)); // skip conversion doAnswer(invocation -> invocation.getArgument(0)).when(repositoryQuery).convertParameter(any()); From d31e50b1e1bdf7c33c6dd33609ca22370b28494e Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 9 Oct 2024 10:43:47 +0200 Subject: [PATCH 3/5] Added tests, added reactive support --- .../ReactiveNeo4jQueryLookupStrategy.java | 12 +++--- .../query/ReactiveStringBasedNeo4jQuery.java | 37 +++++++++---------- .../query/StringBasedNeo4jQuery.java | 3 -- .../ReactiveNeo4jRepositoryFactory.java | 13 ++----- .../ReactiveCustomTypesIT.java | 15 ++++++++ .../integration/imperative/RepositoryIT.java | 11 ++++++ .../repositories/PersonRepository.java | 3 ++ .../query/ReactiveRepositoryQueryTest.java | 25 ++++++++----- 8 files changed, 71 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java index ef8e7b4e9c..9ba53e8bb9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveNeo4jQueryLookupStrategy.java @@ -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}. @@ -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; } @@ -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); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java index 4d499ed945..8316ee0948 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -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; @@ -65,11 +64,9 @@ 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 @@ -77,12 +74,12 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { * * @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() @@ -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); } @@ -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 @@ -134,7 +131,7 @@ protected PreparedQuery prepareQuery(Class returnedType Map boundParameters = bindParameters(parameterAccessor); QueryContext queryContext = new QueryContext( queryMethod.getRepositoryName() + "." + queryMethod.getName(), - spelEvaluator.getQueryString(), + parsedQuery.getQueryString(), boundParameters ); @@ -153,7 +150,7 @@ Map bindParameters(Neo4jParameterAccessor parameterAccessor) { Map resolvedParameters = new HashMap<>(); // Values from the parameter accessor can only get converted after evaluation - for (Map.Entry evaluatedParam : spelEvaluator.evaluate(parameterAccessor.getValues()).entrySet()) { + for (Map.Entry evaluatedParam : parsedQuery.evaluate(parameterAccessor.getValues()).entrySet()) { Object value = evaluatedParam.getValue(); if (!(evaluatedParam.getValue() instanceof Neo4jSpelSupport.LiteralReplacement)) { Neo4jQuerySupport.logParameterIfNull(evaluatedParam.getKey(), value); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index c4039f87ff..0d7fa92d77 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -81,8 +81,6 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { */ private final Optional parsedCountQuery; - private final ValueExpressionDelegate delegate; - private final ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter queryRewriter; /** @@ -161,7 +159,6 @@ private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingConte super(neo4jOperations, mappingContext, queryMethod, queryType, factory); - this.delegate = delegate; cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER); this.queryRewriter = ValueExpressionQueryRewriter.of(delegate, StringBasedNeo4jQuery::parameterNameSource, StringBasedNeo4jQuery::replacementSource); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index 54ff4db0ad..7c774bbdfd 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -40,7 +40,7 @@ import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Factory to create {@link ReactiveNeo4jRepository} instances. @@ -127,16 +127,11 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleReactiveNeo4jRepository.class; } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) - */ - @Override - protected Optional getQueryLookupStrategy(Key key, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + @Override protected Optional getQueryLookupStrategy(Key key, + ValueExpressionDelegate valueExpressionDelegate) { return Optional - .of(new ReactiveNeo4jQueryLookupStrategy(neo4jOperations, mappingContext, evaluationContextProvider, cypherDSLConfiguration)); + .of(new ReactiveNeo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); } @Override diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java index c0d4c672cf..fa92ade14a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_reactive/ReactiveCustomTypesIT.java @@ -23,6 +23,7 @@ import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.ReactiveTransactionManager; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -66,6 +67,7 @@ */ @Neo4jIntegrationTest @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) +@TestPropertySource(properties = {"foo=XYZ"}) public class ReactiveCustomTypesIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; @@ -117,6 +119,15 @@ void findByConvertedCustomTypeWithSpELPropertyAccessQuery( .expectNextCount(1).verifyComplete(); } + @Test + void findByConvertedCustomTypeWithPropertyPlaceholderAccessQuery( + @Autowired EntityWithCustomTypePropertyRepository repository) { + + StepVerifier + .create(repository.findByCustomTypeCustomPropertyPlaceholderAccessQuery()) + .expectNextCount(1).verifyComplete(); + } + @Test void findByConvertedCustomTypeWithSpELObjectQuery(@Autowired EntityWithCustomTypePropertyRepository repository) { StepVerifier.create(repository.findByCustomTypeSpELObjectQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) @@ -182,6 +193,10 @@ Mono findByCustomTypeCustomQuery( Mono findByCustomTypeCustomSpELPropertyAccessQuery( @Param("customType") ThingWithCustomTypes.CustomType customType); + + @Query("MATCH (c:CustomTypes) WHERE c.customType = :${foo} return c") + Mono findByCustomTypeCustomPropertyPlaceholderAccessQuery(); + @Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType} return c") Mono findByCustomTypeSpELObjectQuery( @Param("customType") ThingWithCustomTypes.CustomType customType); diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index e0a555a208..c61117fdf3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -169,6 +169,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.data.support.WindowIterator; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -212,6 +213,7 @@ static PersonWithAllConstructor personExample(String sameValue) { PersonWithAllConstructor person2; @Nested + @TestPropertySource(properties = "foo=Test") class Find extends IntegrationTestBase { @Override @@ -546,6 +548,15 @@ void loadOptionalPersonWithAllConstructorWithSpelParameters(@Autowired PersonRep assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); } + @Test + void loadOptionalPersonWithAllConstructorWithPropertyPlacholder(@Autowired PersonRepository repository) { + + Optional person = repository + .getOptionalPersonViaPropertyPlaceholder(); + assertThat(person).isPresent(); + assertThat(person.get().getName()).isEqualTo(TEST_PERSON1_NAME); + } + @Test void loadOptionalPersonWithAllConstructorWithSpelParametersAndDynamicSort(@Autowired PersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java index 66f59fad41..f446e8af47 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java @@ -99,6 +99,9 @@ public CustomAggregation(Streamable delegate) { Optional getOptionalPersonViaQuery(@Param("part1") String part1, @Param("part2") String part2); + @Query("MATCH (n:PersonWithAllConstructor{name::${foo}}) return n") + Optional getOptionalPersonViaPropertyPlaceholder(); + @Query("MATCH (n:PersonWithAllConstructor{name::#{#part1 + #part2}}) return n :#{orderBy(#sort)}") Optional getOptionalPersonViaQueryWithSort(@Param("part1") String part1, @Param("part2") String part2, Sort sort); diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java index b1edd730d5..467e6e0c83 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/ReactiveRepositoryQueryTest.java @@ -53,9 +53,9 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.Param; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.util.ReflectionUtils; @@ -124,7 +124,7 @@ void shouldDetectInvalidAnnotation() { Neo4jQueryMethod method = reactiveNeo4jQueryMethod("annotatedQueryWithoutTemplate"); assertThatExceptionOfType(MappingException.class) .isThrownBy(() -> ReactiveStringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, ReactiveQueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory)) + .create(neo4jOperations, neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)) .withMessage("Expected @Query annotation to have a value, but it did not"); } @@ -135,7 +135,7 @@ void shouldWarnWhenUsingSortedAndCustomQuery(LogbackCapture logbackCapture) { ReactiveStringBasedNeo4jQuery query = ReactiveStringBasedNeo4jQuery .create(neo4jOperations, neo4jMappingContext, - ReactiveQueryMethodEvaluationContextProvider.DEFAULT, method, projectionFactory); + ValueExpressionDelegate.create(), method, projectionFactory); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), @@ -167,11 +167,10 @@ void orderBySpelShouldWork(LogbackCapture logbackCapture) { context.refresh(); Neo4jQueryMethod method = reactiveNeo4jQueryMethod("orderBySpel", Pageable.class); + ValueExpressionDelegate delegate = getValueExpressionDelegate(context); ReactiveStringBasedNeo4jQuery query = ReactiveStringBasedNeo4jQuery - .create(neo4jOperations, neo4jMappingContext, - new ReactiveExtensionAwareQueryMethodEvaluationContextProvider( - context.getBeanFactory()), method, projectionFactory); + .create(neo4jOperations, neo4jMappingContext, delegate, method, projectionFactory); Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor( (Neo4jQueryMethod.Neo4jParameters) method.getParameters(), @@ -205,7 +204,7 @@ void literalReplacementsShouldWork() { String.class, String.class, Sort.class); ReactiveStringBasedNeo4jQuery query = ReactiveStringBasedNeo4jQuery.create(neo4jOperations, neo4jMappingContext, - new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(context.getBeanFactory()), + getValueExpressionDelegate(context), method, projectionFactory); String s = Mono.fromSupplier(() -> { @@ -236,7 +235,7 @@ void shouldBindParameters() { ReactiveStringBasedNeo4jQuery repositoryQuery = spy( ReactiveStringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, ReactiveQueryMethodEvaluationContextProvider.DEFAULT, + neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)); // skip conversion @@ -258,7 +257,7 @@ void shouldResolveNamedParameters() { ReactiveStringBasedNeo4jQuery repositoryQuery = spy( ReactiveStringBasedNeo4jQuery.create(neo4jOperations, - neo4jMappingContext, ReactiveQueryMethodEvaluationContextProvider.DEFAULT, + neo4jMappingContext, ValueExpressionDelegate.create(), method, projectionFactory)); // skip conversion @@ -277,6 +276,12 @@ void shouldResolveNamedParameters() { } } + private static ValueExpressionDelegate getValueExpressionDelegate(ConfigurableApplicationContext context) { + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(context.getEnvironment(), context.getBeanFactory()); + ValueExpressionDelegate delegate = new ValueExpressionDelegate(accessor, ValueExpressionDelegate.create()); + return delegate; + } + private static Method queryMethod(String name, Class... parameters) { return ReflectionUtils.findMethod(TestRepository.class, name, parameters); From 549cbd6508547a7ef4d8ab3bb0b5506a2bd3676d Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 9 Oct 2024 10:52:46 +0200 Subject: [PATCH 4/5] Added docs --- .../ROOT/pages/appendix/custom-queries.adoc | 20 ++++++++++++++++++- .../domain_events/ARepository.java | 5 +++++ .../domain_events/DomainEventsTest.java | 5 +++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/antora/modules/ROOT/pages/appendix/custom-queries.adoc b/src/main/antora/modules/ROOT/pages/appendix/custom-queries.adoc index b1842e7772..55171bc4c0 100644 --- a/src/main/antora/modules/ROOT/pages/appendix/custom-queries.adoc +++ b/src/main/antora/modules/ROOT/pages/appendix/custom-queries.adoc @@ -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. @@ -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`. \ No newline at end of file diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java index ff2944236f..71497bc3b8 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java @@ -36,6 +36,11 @@ public interface ARepository extends Neo4jRepository { Optional findByCustomQuery(String name); // end::standard-parameter[] + // tag::property-placeholder[] + @Query("MATCH (a:AnAggregateRoot) WHERE a.name = :${foo} RETURN a") + Optional findByCustomQueryWithPropertyPlaceholder(); + // end::property-placeholder[] + // tag::spel[] @Query("MATCH (a:AnAggregateRoot) WHERE a.name = :#{#pt1 + #pt2} RETURN a") Optional findByCustomQueryWithSpEL(String pt1, String pt2); diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java index 80e47483f7..46a75f1550 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/DomainEventsTest.java @@ -22,11 +22,13 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; /** * Basic tests for domain events (and documentation). */ @Disabled +@TestPropertySource("foo=The Root") // tag::domain-events[] public class DomainEventsTest { @@ -57,6 +59,9 @@ void customQueryShouldWork() { optionalAggregate = aRepository.findByCustomQueryWithSpEL("The ", "Root"); assertThat(optionalAggregate).isPresent(); + + optionalAggregate = aRepository.findByCustomQueryWithPropertyPlaceholder(); + assertThat(optionalAggregate).isPresent(); } // tag::domain-events[] From 9e80388d24009b140cc534f56168025b4bb9fea1 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 10 Oct 2024 15:49:59 +0200 Subject: [PATCH 5/5] Uses 3.4.0-SNAPSHOT now that the code is in main --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 59f29c4bd7..e6e4e32ad9 100644 --- a/pom.xml +++ b/pom.xml @@ -110,7 +110,7 @@ ${skipTests} - 3.4.0-GH-3049-SNAPSHOT + 3.4.0-SNAPSHOT 2.2.0