Skip to content

Commit 5cfde4d

Browse files
GH-2239 - Resolve or remove literal replacements in custom count queries.
When executing a custom count query, the bindable parameters must be treated the exact same way like with the actual query: Literal replacement must be applied to the query or dropped. This fixes #2239.
1 parent ae8db45 commit 5cfde4d

File tree

3 files changed

+49
-9
lines changed

3 files changed

+49
-9
lines changed

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

+27-9
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.springframework.data.repository.query.RepositoryQuery;
3838
import org.springframework.data.repository.query.SpelEvaluator;
3939
import org.springframework.data.repository.query.SpelQueryContext;
40-
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
4140
import org.springframework.lang.Nullable;
4241
import org.springframework.util.Assert;
4342
import org.springframework.util.StringUtils;
@@ -81,6 +80,11 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery {
8180
*/
8281
private final SpelEvaluator spelEvaluator;
8382

83+
/**
84+
* An optional evaluator for a count query if such a query is present.
85+
*/
86+
private final Optional<SpelEvaluator> spelEvaluatorForCountQuery;
87+
8488
/**
8589
* Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The
8690
* annotation is expected to have a value.
@@ -156,8 +160,12 @@ private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingConte
156160

157161
super(neo4jOperations, mappingContext, queryMethod, queryType);
158162

159-
SpelExtractor spelExtractor = SPEL_QUERY_CONTEXT.parse(cypherTemplate);
160-
this.spelEvaluator = new SpelEvaluator(evaluationContextProvider, queryMethod.getParameters(), spelExtractor);
163+
Parameters<?, ?> methodParameters = queryMethod.getParameters();
164+
this.spelEvaluator = new SpelEvaluator(
165+
evaluationContextProvider, methodParameters, SPEL_QUERY_CONTEXT.parse(cypherTemplate));
166+
this.spelEvaluatorForCountQuery = queryMethod.getQueryAnnotation()
167+
.map(Query::countQuery)
168+
.map(countQuery -> new SpelEvaluator(evaluationContextProvider, methodParameters, SPEL_QUERY_CONTEXT.parse(countQuery)));
161169
}
162170

163171
@Override
@@ -193,7 +201,6 @@ Map<String, Object> bindParameters(Neo4jParameterAccessor parameterAccessor, boo
193201
// Values from the parameter accessor can only get converted after evaluation
194202
for (Entry<String, Object> evaluatedParam : spelEvaluator.evaluate(parameterAccessor.getValues()).entrySet()) {
195203
Object value;
196-
197204
if (evaluatedParam.getValue() instanceof LiteralReplacement) {
198205
value = evaluatedParam.getValue();
199206
} else {
@@ -224,11 +231,22 @@ Map<String, Object> bindParameters(Neo4jParameterAccessor parameterAccessor, boo
224231

225232
@Override
226233
protected Optional<PreparedQuery<Long>> getCountQuery(Neo4jParameterAccessor parameterAccessor) {
227-
228-
return queryMethod.getQueryAnnotation().map(queryAnnotation ->
229-
PreparedQuery.queryFor(Long.class)
230-
.withCypherQuery(queryAnnotation.countQuery())
231-
.withParameters(bindParameters(parameterAccessor, false, UnaryOperator.identity())).build());
234+
return spelEvaluatorForCountQuery.map(SpelEvaluator::getQueryString)
235+
.map(countQuery -> {
236+
Map<String, Object> boundParameters = bindParameters(parameterAccessor, false, UnaryOperator.identity());
237+
QueryContext queryContext = new QueryContext(
238+
queryMethod.getRepositoryName() + "." + queryMethod.getName(),
239+
countQuery,
240+
boundParameters
241+
);
242+
243+
replaceLiteralsIn(queryContext);
244+
245+
return PreparedQuery.queryFor(Long.class)
246+
.withCypherQuery(queryContext.query)
247+
.withParameters(boundParameters)
248+
.build();
249+
});
232250
}
233251

234252
/**

src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java

+18
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,24 @@ void filtersOnSameEntitiesButDifferentRelationsShouldWork(@Autowired FlightRepos
758758
assertThat(flights).hasSize(1)
759759
.first().extracting(Flight::getName).isEqualTo("FL 001");
760760
}
761+
762+
@Test // GH-2239
763+
void findPageByCustomQueryWithCountShouldWork(@Autowired PersonRepository repository) {
764+
765+
Page<PersonWithAllConstructor> slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.by("n.name").descending()));
766+
assertThat(slice.getSize()).isEqualTo(1);
767+
assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME);
768+
assertThat(slice.hasNext()).isTrue();
769+
assertThat(slice.getTotalElements()).isEqualTo(2);
770+
assertThat(slice.getTotalPages()).isEqualTo(2);
771+
772+
slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, slice.nextPageable());
773+
assertThat(slice.getSize()).isEqualTo(1);
774+
assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME);
775+
assertThat(slice.hasNext()).isFalse();
776+
assertThat(slice.getTotalElements()).isEqualTo(2);
777+
assertThat(slice.getTotalPages()).isEqualTo(2);
778+
}
761779
}
762780

763781
@Nested

src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java

+4
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ Optional<PersonWithAllConstructor> getOptionalPersonViaNamedQuery(@Param("part1"
120120
countQuery = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN count(n)")
121121
Slice<PersonWithAllConstructor> findSliceByCustomQueryWithCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable);
122122

123+
@Query(value = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN n :#{orderBy(#pageable)} SKIP $skip LIMIT $limit",
124+
countQuery = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN count(n)")
125+
Page<PersonWithAllConstructor> findPageByCustomQueryWithCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable);
126+
123127
Long countAllByNameOrName(String aName, String anotherName);
124128

125129
Optional<PersonWithAllConstructor> findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName);

0 commit comments

Comments
 (0)