Skip to content

Commit 77d94bc

Browse files
committed
GH-2726 - Add scroll support to FluentQuery.
This will implement limit() and scroll() methods in all (Reactive)FluentQuery.. classes. Closes #2726
1 parent 78afd1c commit 77d94bc

28 files changed

+851
-159
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ public Collection<Expression> createReturnStatementForExists(Neo4jPersistentEnti
624624
}
625625

626626
public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntity<?> nodeDescription) {
627-
return createReturnStatementForMatch(nodeDescription, (pp -> true));
627+
return createReturnStatementForMatch(nodeDescription, PropertyFilter.NO_FILTER);
628628
}
629629

630630
/**

src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public static NestedRelationshipContext of(Association<Neo4jPersistentProperty>
125125
Object value = propertyAccessor.getProperty(inverse);
126126
boolean inverseValueIsEmpty = value == null;
127127

128-
RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((pp -> true)).stream()
128+
RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((PropertyFilter.NO_FILTER)).stream()
129129
.filter(r -> r.getFieldName().equals(inverse.getName())).findFirst().orElseThrow(() -> new MappingException(
130130
neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName()));
131131

src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.HashSet;
2020
import java.util.Optional;
2121
import java.util.Set;
22+
import java.util.function.Predicate;
2223

2324
import org.apiguardian.api.API;
2425
import org.springframework.data.neo4j.core.schema.Property;
@@ -39,6 +40,8 @@ public static PropertyFilter acceptAll() {
3940
return new NonFilteringPropertyFilter();
4041
}
4142

43+
public static final Predicate<RelaxedPropertyPath> NO_FILTER = (pp) -> true;
44+
4245
public abstract boolean contains(String dotPath, Class<?> typeToCheck);
4346

4447
public abstract boolean contains(RelaxedPropertyPath propertyPath);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.data.domain.Pageable;
4040
import org.springframework.data.domain.ScrollPosition.Direction;
4141
import org.springframework.data.domain.Sort;
42+
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
4243
import org.springframework.data.neo4j.core.mapping.Constants;
4344
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
4445
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
@@ -98,7 +99,7 @@ public static Function<Sort.Order, SortItem> sortAdapterFor(NodeDescription<?> n
9899
};
99100
}
100101

101-
public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity<?> entity, KeysetScrollPosition scrollPosition, Sort sort) {
102+
public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity<?> entity, KeysetScrollPosition scrollPosition, Sort sort, Neo4jConversionService conversionService) {
102103

103104
var incomingKeys = scrollPosition.getKeys();
104105
var orderedKeys = new LinkedHashMap<String, Object>();
@@ -135,7 +136,7 @@ record PropertyAndOrder(Neo4jPersistentProperty property, Sort.Order order) {
135136
if (v == null || (v instanceof Value value && value.isNull())) {
136137
throw new IllegalStateException("Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(k));
137138
}
138-
var parameter = Cypher.anonParameter(v);
139+
var parameter = Cypher.anonParameter(conversionService.convert(v, Value.class));
139140

140141
Expression expression;
141142

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private QueryFragments createQueryFragments(@Nullable Condition condition, Sort
239239

240240
queryFragments.setLimit(limitModifier.apply(maxResults.intValue()));
241241
if (!keysetScrollPosition.isInitial()) {
242-
conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, keysetScrollPosition, theSort));
242+
conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, keysetScrollPosition, theSort, mappingContext.getConversionService()));
243243
}
244244

245245
queryFragments.setRequiresReverseSort(keysetScrollPosition.scrollsBackward());

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Optional;
2424
import java.util.function.LongSupplier;
25+
import java.util.function.Predicate;
2526

2627
import org.apiguardian.api.API;
2728
import org.neo4j.cypherdsl.core.Condition;
@@ -35,6 +36,7 @@
3536
import org.springframework.data.neo4j.core.Neo4jOperations;
3637
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
3738
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
39+
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
3840
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
3941
import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation;
4042
import org.springframework.data.support.PageableExecutionUtils;
@@ -66,7 +68,7 @@ public Optional<T> findOne(Condition condition) {
6668

6769
return this.neo4jOperations.toExecutableQuery(
6870
this.metaData.getType(),
69-
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null)
71+
QueryFragmentsAndParameters.forCondition(this.metaData, condition)
7072
).getSingleResult();
7173
}
7274

@@ -75,17 +77,18 @@ public Collection<T> findAll(Condition condition) {
7577

7678
return this.neo4jOperations.toExecutableQuery(
7779
this.metaData.getType(),
78-
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null)
80+
QueryFragmentsAndParameters.forCondition(this.metaData, condition)
7981
).getResults();
8082
}
8183

8284
@Override
8385
public Collection<T> findAll(Condition condition, Sort sort) {
8486

87+
Predicate<PropertyFilter.RelaxedPropertyPath> noFilter = PropertyFilter.NO_FILTER;
8588
return this.neo4jOperations.toExecutableQuery(
8689
metaData.getType(),
87-
QueryFragmentsAndParameters.forCondition(
88-
this.metaData, condition, null, CypherAdapterUtils.toSortItems(this.metaData, sort)
90+
QueryFragmentsAndParameters.forConditionAndSort(
91+
this.metaData, condition, sort, null, noFilter
8992
)
9093
).getResults();
9194
}
@@ -95,8 +98,8 @@ public Collection<T> findAll(Condition condition, SortItem... sortItems) {
9598

9699
return this.neo4jOperations.toExecutableQuery(
97100
this.metaData.getType(),
98-
QueryFragmentsAndParameters.forCondition(
99-
this.metaData, condition, null, Arrays.asList(sortItems)
101+
QueryFragmentsAndParameters.forConditionAndSortItems(
102+
this.metaData, condition, Arrays.asList(sortItems)
100103
)
101104
).getResults();
102105
}
@@ -106,16 +109,17 @@ public Collection<T> findAll(SortItem... sortItems) {
106109

107110
return this.neo4jOperations.toExecutableQuery(
108111
this.metaData.getType(),
109-
QueryFragmentsAndParameters.forCondition(this.metaData, Conditions.noCondition(), null, Arrays.asList(sortItems))
112+
QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Conditions.noCondition(), Arrays.asList(sortItems))
110113
).getResults();
111114
}
112115

113116
@Override
114117
public Page<T> findAll(Condition condition, Pageable pageable) {
115118

119+
Predicate<PropertyFilter.RelaxedPropertyPath> noFilter = PropertyFilter.NO_FILTER;
116120
List<T> page = this.neo4jOperations.toExecutableQuery(
117121
this.metaData.getType(),
118-
QueryFragmentsAndParameters.forCondition(this.metaData, condition, pageable, null)
122+
QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, condition, pageable, noFilter)
119123
).getResults();
120124
LongSupplier totalCountSupplier = () -> this.count(condition);
121125
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);

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

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,29 @@
1515
*/
1616
package org.springframework.data.neo4j.repository.query;
1717

18-
import java.util.Collection;
19-
import java.util.List;
20-
import java.util.function.Function;
21-
import java.util.function.LongSupplier;
22-
import java.util.stream.Stream;
23-
2418
import org.apiguardian.api.API;
19+
import org.neo4j.cypherdsl.core.Condition;
2520
import org.springframework.data.domain.Example;
21+
import org.springframework.data.domain.KeysetScrollPosition;
22+
import org.springframework.data.domain.OffsetScrollPosition;
2623
import org.springframework.data.domain.Page;
2724
import org.springframework.data.domain.Pageable;
25+
import org.springframework.data.domain.ScrollPosition;
2826
import org.springframework.data.domain.Sort;
27+
import org.springframework.data.domain.Window;
2928
import org.springframework.data.neo4j.core.FluentFindOperation;
3029
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
30+
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
3131
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
3232
import org.springframework.data.support.PageableExecutionUtils;
3333
import org.springframework.lang.Nullable;
3434

35+
import java.util.Collection;
36+
import java.util.List;
37+
import java.util.function.Function;
38+
import java.util.function.LongSupplier;
39+
import java.util.stream.Stream;
40+
3541
/**
3642
* Immutable implementation of a {@link FetchableFluentQuery}. All
3743
* methods that return a {@link FetchableFluentQuery} return a new instance, the original instance won't be
@@ -64,7 +70,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
6470
Function<Example<S>, Boolean> existsOperation
6571
) {
6672
this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(),
67-
null);
73+
null, null);
6874
}
6975

7076
FetchableFluentQueryByExample(
@@ -75,9 +81,10 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
7581
Function<Example<S>, Long> countOperation,
7682
Function<Example<S>, Boolean> existsOperation,
7783
Sort sort,
84+
@Nullable Integer limit,
7885
@Nullable Collection<String> properties
7986
) {
80-
super(resultType, sort, properties);
87+
super(resultType, sort, limit, properties);
8188
this.mappingContext = mappingContext;
8289
this.example = example;
8390
this.findOperation = findOperation;
@@ -90,7 +97,14 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
9097
public FetchableFluentQuery<R> sortBy(Sort sort) {
9198

9299
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
93-
this.countOperation, this.existsOperation, this.sort.and(sort), this.properties);
100+
this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties);
101+
}
102+
103+
@Override
104+
@SuppressWarnings("HiddenField")
105+
public FetchableFluentQuery<R> limit(int limit) {
106+
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
107+
this.countOperation, this.existsOperation, this.sort, limit, this.properties);
94108
}
95109

96110
@Override
@@ -106,15 +120,15 @@ public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
106120
public FetchableFluentQuery<R> project(Collection<String> properties) {
107121

108122
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
109-
this.countOperation, this.existsOperation, this.sort, mergeProperties(properties));
123+
this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties));
110124
}
111125

112126
@Override
113127
public R oneValue() {
114128

115129
return findOperation.find(example.getProbeType())
116130
.as(resultType)
117-
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
131+
.matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit,
118132
createIncludedFieldsPredicate()))
119133
.oneValue();
120134
}
@@ -131,7 +145,7 @@ public List<R> all() {
131145

132146
return findOperation.find(example.getProbeType())
133147
.as(resultType)
134-
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
148+
.matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit,
135149
createIncludedFieldsPredicate()))
136150
.all();
137151
}
@@ -141,14 +155,36 @@ public Page<R> page(Pageable pageable) {
141155

142156
List<R> page = findOperation.find(example.getProbeType())
143157
.as(resultType)
144-
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable,
158+
.matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable,
145159
createIncludedFieldsPredicate()))
146160
.all();
147161

148162
LongSupplier totalCountSupplier = this::count;
149163
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);
150164
}
151165

166+
@Override
167+
public Window<R> scroll(ScrollPosition scrollPosition) {
168+
Class<S> domainType = this.example.getProbeType();
169+
Neo4jPersistentEntity<?> entity = mappingContext.getPersistentEntity(domainType);
170+
171+
var skip = scrollPosition.isInitial()
172+
? 0
173+
: (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset()
174+
: 0;
175+
176+
Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition
177+
? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService())
178+
: null;
179+
180+
List<R> rawResult = findOperation.find(domainType)
181+
.as(resultType)
182+
.matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate()))
183+
.all();
184+
185+
return scroll(scrollPosition, rawResult, entity);
186+
}
187+
152188
@Override
153189
public Stream<R> stream() {
154190
return all().stream();

0 commit comments

Comments
 (0)