Skip to content

Commit a04706a

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 7d6d221 commit a04706a

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
@@ -732,6 +732,24 @@ void findSliceByCustomQueryWithCountShouldWork(@Autowired PersonRepository repos
732732
assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME);
733733
assertThat(slice.hasNext()).isFalse();
734734
}
735+
736+
@Test // GH-2239
737+
void findPageByCustomQueryWithCountShouldWork(@Autowired PersonRepository repository) {
738+
739+
Page<PersonWithAllConstructor> slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, PageRequest.of(0, 1, Sort.by("n.name").descending()));
740+
assertThat(slice.getSize()).isEqualTo(1);
741+
assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON2_NAME);
742+
assertThat(slice.hasNext()).isTrue();
743+
assertThat(slice.getTotalElements()).isEqualTo(2);
744+
assertThat(slice.getTotalPages()).isEqualTo(2);
745+
746+
slice = repository.findPageByCustomQueryWithCount(TEST_PERSON1_NAME, TEST_PERSON2_NAME, slice.nextPageable());
747+
assertThat(slice.getSize()).isEqualTo(1);
748+
assertThat(slice.get()).hasSize(1).extracting("name").containsExactly(TEST_PERSON1_NAME);
749+
assertThat(slice.hasNext()).isFalse();
750+
assertThat(slice.getTotalElements()).isEqualTo(2);
751+
assertThat(slice.getTotalPages()).isEqualTo(2);
752+
}
735753
}
736754

737755
@Nested

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

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

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

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

0 commit comments

Comments
 (0)