Skip to content

Commit d094857

Browse files
committed
DATAGRAPH-1409 - Support for custom paged queries.
1 parent 3e6c5aa commit d094857

File tree

6 files changed

+110
-57
lines changed

6 files changed

+110
-57
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Collections;
1919
import java.util.List;
20+
import java.util.Optional;
2021
import java.util.function.BiFunction;
2122
import java.util.function.LongSupplier;
2223

@@ -88,8 +89,10 @@ public final Object execute(Object[] parameters) {
8889

8990
LongSupplier totalSupplier = () -> {
9091

91-
PreparedQuery<Long> countQuery = prepareQuery(Long.class, Collections.emptyList(), parameterAccessor,
92-
Neo4jQueryType.COUNT, null);
92+
PreparedQuery<Long> countQuery = getCountQuery(parameterAccessor)
93+
.orElse(
94+
prepareQuery(Long.class, Collections.emptyList(), parameterAccessor, Neo4jQueryType.COUNT, null)
95+
);
9396
return neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult();
9497
};
9598
if (queryMethod.isPageQuery()) {
@@ -107,4 +110,8 @@ protected abstract <T extends Object> PreparedQuery<T> prepareQuery(Class<T> ret
107110
List<String> includedProperties, Neo4jParameterAccessor parameterAccessor,
108111
@Nullable Neo4jQueryType queryType,
109112
@Nullable BiFunction<TypeSystem, Record, ?> mappingFunction);
113+
114+
protected Optional<PreparedQuery<Long>> getCountQuery(Neo4jParameterAccessor parameterAccessor) {
115+
return Optional.empty();
116+
}
110117
}

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

Lines changed: 0 additions & 38 deletions
This file was deleted.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
*/
4646
String value() default "";
4747

48+
/**
49+
* The Cypher statement for counting the total number of expected results. Only needed for methods returning pages or slices based on custom queries.
50+
*/
51+
String countQuery() default "";
52+
4853
/**
4954
* @return whether the query defined should be executed as count projection.
5055
*/

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323

2424
import org.neo4j.driver.Record;
2525
import org.neo4j.driver.types.TypeSystem;
26+
import org.springframework.data.domain.Pageable;
2627
import org.springframework.data.mapping.MappingException;
2728
import org.springframework.data.neo4j.core.Neo4jOperations;
2829
import org.springframework.data.neo4j.core.PreparedQuery;
2930
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
30-
import org.springframework.data.repository.query.Parameter;
3131
import org.springframework.data.repository.query.Parameters;
3232
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3333
import org.springframework.data.repository.query.RepositoryQuery;
@@ -90,6 +90,10 @@ static StringBasedNeo4jQuery create(Neo4jOperations neo4jOperations, Neo4jMappin
9090
Query queryAnnotation = queryMethod.getQueryAnnotation()
9191
.orElseThrow(() -> new MappingException("Expected @Query annotation on the query method!"));
9292

93+
if (queryAnnotation.countQuery().isEmpty() && (queryMethod.isSliceQuery() || queryMethod.isPageQuery())) {
94+
throw new MappingException("Expected paging query method to have a count query!");
95+
}
96+
9397
String cypherTemplate = Optional.ofNullable(queryAnnotation.value()).filter(StringUtils::hasText)
9498
.orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not."));
9599

@@ -128,12 +132,6 @@ private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingConte
128132
this.cypherQuery = spelExtractor.getQueryString();
129133
}
130134

131-
static String getQueryTemplate(Query queryAnnotation) {
132-
133-
return Optional.ofNullable(queryAnnotation.value()).filter(StringUtils::hasText)
134-
.orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not."));
135-
}
136-
137135
@Override
138136
protected <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType, List<String> includedProperties,
139137
Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType,
@@ -153,7 +151,7 @@ Map<String, Object> bindParameters(Neo4jParameterAccessor parameterAccessor) {
153151
resolvedParameters.put(evaluatedParam.getKey(), super.convertParameter(evaluatedParam.getValue()));
154152
}
155153

156-
formalParameters.stream().filter(Parameter::isBindable).forEach(parameter -> {
154+
formalParameters.getBindableParameters().forEach(parameter -> {
157155

158156
int parameterIndex = parameter.getIndex();
159157
Object parameterValue = super.convertParameter(parameterAccessor.getBindableValue(parameterIndex));
@@ -164,9 +162,24 @@ Map<String, Object> bindParameters(Neo4jParameterAccessor parameterAccessor) {
164162
resolvedParameters.put(Integer.toString(parameterIndex), parameterValue);
165163
});
166164

165+
if (formalParameters.hasPageableParameter()) {
166+
Pageable pageable = parameterAccessor.getPageable();
167+
resolvedParameters.put("limit", pageable.getPageSize());
168+
resolvedParameters.put("skip", pageable.getOffset());
169+
}
170+
167171
return resolvedParameters;
168172
}
169173

174+
@Override
175+
protected Optional<PreparedQuery<Long>> getCountQuery(Neo4jParameterAccessor parameterAccessor) {
176+
177+
return queryMethod.getQueryAnnotation().map(queryAnnotation ->
178+
PreparedQuery.queryFor(Long.class)
179+
.withCypherQuery(queryAnnotation.countQuery())
180+
.withParameters(bindParameters(parameterAccessor)).build());
181+
}
182+
170183
/**
171184
* @param index position of this parameter placeholder
172185
* @param originalSpelExpression Not used for configuring parameter names atm.

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
6565
import org.springframework.data.domain.Page;
6666
import org.springframework.data.domain.PageRequest;
67+
import org.springframework.data.domain.Pageable;
6768
import org.springframework.data.domain.Range;
6869
import org.springframework.data.domain.Range.Bound;
6970
import org.springframework.data.domain.Slice;
@@ -588,6 +589,57 @@ void customFindMapsDeepRelationships(@Autowired PetRepository repository) {
588589
Pet pet2 = loadedPet.getFriends().get(loadedPet.getFriends().indexOf(comparisonPet2));
589590
assertThat(pet2.getFriends()).containsExactly(comparisonPet3);
590591
}
592+
593+
@Test // DATAGRAPH-1409
594+
void findPageWithCustomQuery(@Autowired PetRepository repository) {
595+
596+
try (Session session = createSession()) {
597+
session.run("CREATE (luna:Pet{name:'Luna'})").consume();
598+
}
599+
Page<Pet> loadedPets = repository.pagedPets(PageRequest.of(0, 1));
600+
601+
assertThat(loadedPets.getNumberOfElements()).isEqualTo(1);
602+
assertThat(loadedPets.getTotalElements()).isEqualTo(1);
603+
604+
loadedPets = repository.pagedPets(PageRequest.of(1, 1));
605+
assertThat(loadedPets.getNumberOfElements()).isEqualTo(0);
606+
assertThat(loadedPets.getTotalElements()).isEqualTo(1);
607+
}
608+
609+
@Test // DATAGRAPH-1409
610+
void findPageWithCustomQueryAndParameters(@Autowired PetRepository repository) {
611+
612+
try (Session session = createSession()) {
613+
session.run("CREATE (luna:Pet{name:'Luna'})").consume();
614+
}
615+
Page<Pet> loadedPets = repository.pagedPetsWithParameter("Luna", PageRequest.of(0, 1));
616+
617+
assertThat(loadedPets.getNumberOfElements()).isEqualTo(1);
618+
assertThat(loadedPets.getTotalElements()).isEqualTo(1);
619+
620+
loadedPets = repository.pagedPetsWithParameter("Luna", PageRequest.of(1, 1));
621+
assertThat(loadedPets.getNumberOfElements()).isEqualTo(0);
622+
assertThat(loadedPets.getTotalElements()).isEqualTo(1);
623+
}
624+
625+
@Test // DATAGRAPH-1409
626+
void findSliceWithCustomQuery(@Autowired PetRepository repository) {
627+
628+
try (Session session = createSession()) {
629+
session.run("CREATE (luna:Pet{name:'Luna'})").consume();
630+
}
631+
Slice<Pet> loadedPets = repository.slicedPets(PageRequest.of(0, 1));
632+
633+
assertThat(loadedPets.getNumberOfElements()).isEqualTo(1);
634+
assertThat(loadedPets.isFirst()).isTrue();
635+
assertThat(loadedPets.isLast()).isTrue();
636+
637+
loadedPets = repository.slicedPets(PageRequest.of(1, 1));
638+
assertThat(loadedPets.getNumberOfElements()).isEqualTo(0);
639+
assertThat(loadedPets.isFirst()).isFalse();
640+
assertThat(loadedPets.isLast()).isTrue();
641+
}
642+
591643
}
592644

593645
@Nested
@@ -3176,6 +3228,16 @@ interface PetRepository extends Neo4jRepository<Pet, Long> {
31763228
@Query("MATCH (p:Pet)-[r1:Has]->(p2:Pet)-[r2:Has]->(p3:Pet) " +
31773229
"where id(p) = $petNode1Id return p, collect(r1), collect(p2), collect(r2), collect(p3)")
31783230
Pet customQueryWithDeepRelationshipMapping(@Param("petNode1Id") long petNode1Id);
3231+
@Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)")
3232+
Page<Pet> pagedPets(Pageable pageable);
3233+
3234+
@Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)")
3235+
Slice<Pet> slicedPets(Pageable pageable);
3236+
3237+
@Query(value = "MATCH (p:Pet) where p.name=$petName return p SKIP $skip LIMIT $limit",
3238+
countQuery = "MATCH (p:Pet) return count(p)")
3239+
Page<Pet> pagedPetsWithParameter(@Param("petName") String petName, Pageable pageable);
3240+
31793241
}
31803242

31813243
interface RelationshipRepository extends Neo4jRepository<PersonWithRelationship, Long> {

src/test/java/org/springframework/data/neo4j/repository/query/RepositoryQueryTest.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,6 @@ void spelQueryContextShouldBeConfiguredCorrectly() {
205205
assertThat(query).isEqualTo("MATCH (user:User) WHERE user.name=$__SpEL__0 and user.name=$__SpEL__1 RETURN user");
206206
}
207207

208-
@Test
209-
void shouldExtractQueryTemplate() {
210-
211-
Neo4jQueryMethod method = neo4jQueryMethod("annotatedQueryWithValidTemplate");
212-
213-
assertThat(StringBasedNeo4jQuery.getQueryTemplate(method.getQueryAnnotation().get()))
214-
.isEqualTo(CUSTOM_CYPHER_QUERY);
215-
}
216-
217208
@Test
218209
void shouldDetectInvalidAnnotation() {
219210

@@ -224,6 +215,16 @@ void shouldDetectInvalidAnnotation() {
224215
.withMessage("Expected @Query annotation to have a value, but it did not.");
225216
}
226217

218+
@Test // DATAGRAPH-1409
219+
void shouldDetectMissingCountQuery() {
220+
221+
Neo4jQueryMethod method = neo4jQueryMethod("missingCountQuery", Pageable.class);
222+
assertThatExceptionOfType(MappingException.class)
223+
.isThrownBy(() -> StringBasedNeo4jQuery.create(mock(Neo4jOperations.class), mock(Neo4jMappingContext.class),
224+
QueryMethodEvaluationContextProvider.DEFAULT, method))
225+
.withMessage("Expected paging query method to have a count query!");
226+
}
227+
227228
@Test
228229
void shouldBindParameters() {
229230

@@ -397,6 +398,9 @@ Optional<TestEntity> findByDontDoThisInRealLiveNamed(@Param("location") org.neo4
397398
List<TestEntityDTOProjection> findAllDTOProjections();
398399

399400
List<ExtendedTestEntity> findAllExtendedEntites();
401+
402+
@Query("MATCH (n:Test) SKIP $skip LIMIT $limit")
403+
Page<TestEntity> missingCountQuery(Pageable pageable);
400404
}
401405

402406
private RepositoryQueryTest() {}

0 commit comments

Comments
 (0)