Skip to content

Commit 82dab01

Browse files
GH-1807 - Add support for limiting, derived queries (findFirst and findTop).
This closes #1807.
1 parent 0aa1504 commit 82dab01

File tree

9 files changed

+93
-38
lines changed

9 files changed

+93
-38
lines changed

spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphParametersParameterAccessor.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import org.springframework.core.annotation.AnnotationUtils;
2121
import org.springframework.data.domain.Sort;
2222
import org.springframework.data.neo4j.annotation.Depth;
23+
import org.springframework.data.neo4j.util.PagingAndSortingUtils;
2324
import org.springframework.data.repository.query.ParametersParameterAccessor;
25+
import org.springframework.lang.Nullable;
2426

2527
/**
2628
* Custom {@link ParametersParameterAccessor} to allow access to the {@link Depth} parameter.
@@ -67,17 +69,7 @@ public int getDepth() {
6769

6870
@Override
6971
public SortOrder getOgmSort() {
70-
SortOrder sortOrder = new SortOrder();
7172

72-
if (getSort() != null) {
73-
for (Sort.Order order : getSort()) {
74-
if (order.isAscending()) {
75-
sortOrder.add(order.getProperty());
76-
} else {
77-
sortOrder.add(SortOrder.Direction.DESC, order.getProperty());
78-
}
79-
}
80-
}
81-
return sortOrder;
73+
return PagingAndSortingUtils.convert(getSort());
8274
}
8375
}

spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphQueryExecution.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
import java.util.Iterator;
1919
import java.util.List;
2020
import java.util.Map;
21-
import java.util.function.LongSupplier;
21+
import java.util.Optional;
2222

23+
import org.neo4j.ogm.cypher.query.Pagination;
24+
import org.neo4j.ogm.cypher.query.SortOrder;
2325
import org.neo4j.ogm.model.Result;
2426
import org.neo4j.ogm.session.Session;
2527
import org.springframework.dao.IncorrectResultSizeDataAccessException;
2628
import org.springframework.data.domain.Pageable;
2729
import org.springframework.data.domain.SliceImpl;
30+
import org.springframework.data.domain.Sort;
2831
import org.springframework.data.neo4j.annotation.QueryResult;
32+
import org.springframework.data.neo4j.util.PagingAndSortingUtils;
2933
import org.springframework.data.repository.support.PageableExecutionUtils;
3034
import org.springframework.util.Assert;
3135

@@ -89,7 +93,10 @@ final class CollectionExecution implements GraphQueryExecution {
8993
@Override
9094
public Object execute(Query query, Class<?> type) {
9195
if (query.isFilterQuery()) {
92-
return session.loadAll(type, query.getFilters(), accessor.getOgmSort(), accessor.getDepth());
96+
Pagination pagination = query.getOptionalPagination(null, false);
97+
SortOrder ogmSort = PagingAndSortingUtils.convert(Optional.ofNullable(accessor.getSort()).filter(Sort::isSorted).orElseGet(query::getOptionalSort));
98+
return pagination == null ? session.loadAll(type, query.getFilters(), ogmSort, accessor.getDepth()) : session.loadAll(type, query.getFilters(),
99+
ogmSort, pagination, accessor.getDepth());
93100
} else {
94101
if (type.getAnnotation(QueryResult.class) != null || Map.class.isAssignableFrom(type)) {
95102
return session.query(query.getCypherQuery(accessor.getSort()), query.getParameters()).queryResults();
@@ -135,7 +142,7 @@ public Object execute(Query query, Class<?> type) {
135142
long count;
136143
if (query.isFilterQuery()) {
137144
result = (List<?>) session.loadAll(type, query.getFilters(), accessor.getOgmSort(),
138-
query.getPagination(pageable, false), accessor.getDepth());
145+
query.getOptionalPagination(pageable, false), accessor.getDepth());
139146
count = session.count(type, query.getFilters());
140147
} else {
141148
if (type.getAnnotation(QueryResult.class) != null) {
@@ -146,7 +153,7 @@ public Object execute(Query query, Class<?> type) {
146153
count = (result.size() > 0) ? countTotalNumberOfElements(query) : 0;
147154
}
148155

149-
return PageableExecutionUtils.getPage(result, pageable, (LongSupplier) () -> count);
156+
return PageableExecutionUtils.getPage(result, pageable, () -> count);
150157
}
151158

152159
private Integer countTotalNumberOfElements(Query query) {
@@ -176,7 +183,7 @@ public Object execute(Query query, Class<?> type) {
176183
if (query.isFilterQuery()) {
177184
// For a slice, need one extra result to determine if there is a next page
178185
result = (List<?>) session.loadAll(type, query.getFilters(), accessor.getOgmSort(),
179-
query.getPagination(pageable, true), accessor.getDepth());
186+
query.getOptionalPagination(pageable, true), accessor.getDepth());
180187
} else {
181188
String cypherQuery = query.getCypherQuery(pageable, true);
182189
if (type.getAnnotation(QueryResult.class) != null) {

spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,12 @@ public PartTreeNeo4jQuery(GraphQueryMethod graphQueryMethod, MetaData metaData,
5252
Class<?> domainType = graphQueryMethod.getEntityInformation().getJavaType();
5353
this.graphQueryMethod = graphQueryMethod;
5454
this.tree = new PartTree(graphQueryMethod.getName(), domainType);
55-
5655
this.queryTemplate = new TemplatedQueryCreator(this.tree,
5756
(Neo4jMappingContext) this.graphQueryMethod.getMappingContext(), domainType).createQuery();
5857
}
5958

6059
@Override
61-
protected Object doExecute(Query params, Object[] parameters) {
60+
protected Object doExecute(Query query, Object[] parameters) {
6261

6362
if (LOG.isDebugEnabled()) {
6463
LOG.debug("Executing query for method {}", graphQueryMethod.getName());
@@ -72,7 +71,7 @@ protected Object doExecute(Query params, Object[] parameters) {
7271
}
7372

7473
ResultProcessor processor = graphQueryMethod.getResultProcessor().withDynamicProjection(accessor);
75-
Object results = getExecution(accessor).execute(params, processor.getReturnedType().getDomainType());
74+
Object results = getExecution(accessor).execute(query, processor.getReturnedType().getDomainType());
7675

7776
return processor.processResult(results);
7877
}
@@ -81,7 +80,7 @@ protected Object doExecute(Query params, Object[] parameters) {
8180
protected Query getQuery(Object[] parameters) {
8281

8382
Map<Integer, Object> resolvedParameters = resolveParameters(parameters);
84-
return this.queryTemplate.createExecutableQuery(resolvedParameters);
83+
return this.queryTemplate.createExecutableQuery(resolvedParameters, this.tree.isLimiting() ? this.tree.getMaxResults() : null, tree.getSort());
8584
}
8685

8786
@Override

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

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,25 @@
3737
*/
3838
public class Query {
3939

40-
private static final String SKIP = "sdnSkip";
41-
private static final String LIMIT = "sdnLimit";
42-
private static final String SKIP_LIMIT = " SKIP $" + SKIP + " LIMIT $" + LIMIT;
40+
private static final String SKIP_PARAM = "sdnSkip";
41+
private static final String LIMIT_PARAM = "sdnLimit";
42+
private static final String SKIP_LIMIT = " SKIP $" + SKIP_PARAM + " LIMIT $" + LIMIT_PARAM;
43+
private static final String LIMIT = "LIMIT $" + LIMIT_PARAM;
4344
private static final String ORDER_BY_CLAUSE = " ORDER BY %s";
4445

4546
private Filters filters;
4647
private String cypherQuery;
4748
private Map<String, Object> parameters;
4849
private @Nullable String countQuery;
50+
private @Nullable Integer optionalLimit;
51+
private @Nullable Sort optionalSort;
52+
53+
public Query(Filters filters, @Nullable Integer optionalLimit, Sort optionalSort) {
4954

50-
public Query(Filters filters) {
5155
Assert.notNull(filters, "Filters must not be null.");
5256
this.filters = filters;
57+
this.optionalLimit = optionalLimit;
58+
this.optionalSort = optionalSort;
5359
}
5460

5561
public Query(String cypherQuery, @Nullable String countQuery, Map<String, Object> parameters) {
@@ -107,11 +113,11 @@ private String addPaging(String cypherQuery, Pageable pageable, boolean forSlici
107113
// Custom queries in the OGM do not support pageable
108114
cypherQuery = formatBaseQuery(cypherQuery);
109115
cypherQuery = cypherQuery + SKIP_LIMIT;
110-
parameters.put(SKIP, pageable.getOffset());
116+
parameters.put(SKIP_PARAM, pageable.getOffset());
111117
if (forSlicing) {
112-
parameters.put(LIMIT, pageable.getPageSize() + 1);
118+
parameters.put(LIMIT_PARAM, pageable.getPageSize() + 1);
113119
} else {
114-
parameters.put(LIMIT, pageable.getPageSize());
120+
parameters.put(LIMIT_PARAM, pageable.getPageSize());
115121
}
116122
return cypherQuery;
117123
}
@@ -153,14 +159,25 @@ private String formatBaseQuery(String cypherQuery) {
153159
return cypherQuery;
154160
}
155161

156-
public Pagination getPagination(Pageable pageable, boolean forSlicing) {
157-
Pagination pagination = new Pagination(pageable.getPageNumber(), pageable.getPageSize() + ((forSlicing) ? 1 : 0));
158-
pagination.setOffset(pageable.getPageNumber() * pageable.getPageSize());
162+
@Nullable Pagination getOptionalPagination(@Nullable Pageable pageable, boolean forSlicing) {
163+
164+
if(pageable != null) {
165+
Pagination pagination = new Pagination(pageable.getPageNumber(),pageable.getPageSize() + ((forSlicing) ? 1 : 0));
166+
pagination.setOffset(pageable.getPageNumber() * pageable.getPageSize());
167+
return pagination;
168+
}
169+
170+
if(this.optionalLimit == null) {
171+
return null;
172+
}
173+
174+
Pagination pagination = new Pagination(0, optionalLimit);
175+
pagination.setOffset(0);
159176
return pagination;
160177
}
161178

162-
public SortOrder getSort(Pageable pageable) {
163-
return PagingAndSortingUtils.convert(pageable.getSort());
179+
@Nullable Sort getOptionalSort() {
180+
return optionalSort;
164181
}
165182

166183
private String sanitize(String cypherQuery) {

spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/TemplatedQuery.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323

2424
import org.neo4j.ogm.cypher.Filter;
2525
import org.neo4j.ogm.cypher.Filters;
26+
import org.springframework.data.domain.Sort;
2627
import org.springframework.data.neo4j.repository.query.filter.FilterBuilder;
28+
import org.springframework.lang.Nullable;
2729

2830
/**
2931
* A template query based on filters. {@link #createExecutableQuery(Map)} is used to create an executable query from
@@ -47,7 +49,8 @@ static TemplatedQuery unfiltered() {
4749
this.filterBuilders = filterBuilders;
4850
}
4951

50-
Query createExecutableQuery(Map<Integer, Object> resolvedParameters) {
52+
Query createExecutableQuery(Map<Integer, Object> resolvedParameters, @Nullable Integer optionalLimit, @Nullable
53+
Sort optionalSort) {
5154

5255
// building a stack of parameter values, so that the builders can pull them
5356
// according to their needs (zero, one or more parameters)
@@ -65,6 +68,6 @@ Query createExecutableQuery(Map<Integer, Object> resolvedParameters) {
6568
filters.addAll(filterBuilder.build(parametersStack));
6669
}
6770

68-
return new Query(new Filters(filters));
71+
return new Query(new Filters(filters), optionalLimit, optionalSort);
6972
}
7073
}

spring-data-neo4j/src/main/java/org/springframework/data/neo4j/util/PagingAndSortingUtils.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717

1818
import org.neo4j.ogm.cypher.query.SortOrder;
1919
import org.springframework.data.domain.Sort;
20+
import org.springframework.lang.Nullable;
2021

22+
/**
23+
* @author Nicolas Mervaillie
24+
* @author Michael J. Simons
25+
*/
2126
public class PagingAndSortingUtils {
2227

23-
public static SortOrder convert(Sort sort) {
24-
28+
public static SortOrder convert(@Nullable Sort sort) {
2529
SortOrder sortOrder = new SortOrder();
2630

27-
if (sort != Sort.unsorted()) {
31+
if (sort != null && sort != Sort.unsorted()) {
2832
for (Sort.Order order : sort) {
2933
if (order.isAscending()) {
3034
sortOrder.add(order.getProperty());

spring-data-neo4j/src/test/java/org/springframework/data/neo4j/examples/movies/repo/UserRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public interface UserRepository extends PersonRepository<User, Long> {
5555

5656
List<User> findByRatingsStarsIgnoreCase(int stars);
5757

58+
List<User> findFirstByOrderByNameDesc();
59+
60+
List<User> findTop5ByOrderByNameDesc();
61+
62+
Slice<User> findTop5ByOrderByNameDesc(Pageable pageable);
63+
5864
@Query("MATCH (c:User) SET c.surname = 'Helge' RETURN c")
5965
List<User> bulkUpdateReturningNode();
6066

spring-data-neo4j/src/test/java/org/springframework/data/neo4j/queries/DerivedQueryTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616
package org.springframework.data.neo4j.queries;
1717

18-
import static org.assertj.core.api.Assertions.*;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
1920

2021
import java.util.ArrayList;
2122
import java.util.Collection;
@@ -38,6 +39,7 @@
3839
import org.springframework.data.domain.PageRequest;
3940
import org.springframework.data.domain.Pageable;
4041
import org.springframework.data.domain.Slice;
42+
import org.springframework.data.domain.Sort;
4143
import org.springframework.data.neo4j.examples.movies.domain.Cinema;
4244
import org.springframework.data.neo4j.examples.movies.domain.Director;
4345
import org.springframework.data.neo4j.examples.movies.domain.TempMovie;
@@ -758,6 +760,30 @@ public void shouldFindNodeByNameContainingAndIgnoreCase() {
758760
assertThat(foundUser.getName()).isEqualTo(userName);
759761
}
760762

763+
@Test // GH-1807
764+
public void limitingQueriesShouldWork() {
765+
766+
transactionTemplate.executeWithoutResult(tx -> {
767+
768+
userRepository.deleteAll();
769+
for(int i=0; i<7; ++i) {
770+
User user = new User("U" + i);
771+
userRepository.save(user);
772+
}
773+
});
774+
775+
// Limiting via find
776+
assertThat(userRepository.findFirstByOrderByNameDesc()).extracting(User::getName).containsExactly("U6");
777+
778+
// Limiting via topN
779+
assertThat(userRepository.findTop5ByOrderByNameDesc()).extracting(User::getName)
780+
.containsExactly("U6", "U5", "U4", "U3", "U2");
781+
782+
// Mixing limiting and page request should make the slice win
783+
assertThat(userRepository.findTop5ByOrderByNameDesc(PageRequest.of(0, 1, Sort.by("name").ascending()))).extracting(User::getName)
784+
.containsExactly("U0");
785+
}
786+
761787
class DerivedQueryRunner implements Runnable {
762788

763789
private final CountDownLatch latch;

spring-data-neo4j/src/test/resources/logback-test.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<logger name="org.apache.http" level="warn" />
2727
<logger name="org.eclipse.jetty" level="warn" />
2828
<logger name="org.neo4j.ogm" level="warn" />
29+
<!-- <logger name="org.neo4j.ogm.drivers.bolt.request.BoltRequest" level="debug" /> -->
2930
<logger name="org.springframework" level="warn" />
3031
<logger name="org.springframework.data.neo4j" level="warn" />
3132
<root level="warn">

0 commit comments

Comments
 (0)