GH-2726 - Add scroll support to FluentQuery.

This will implement limit() and scroll()
methods in all (Reactive)FluentQuery.. classes.

Closes #2726
This commit is contained in:
Gerrit Meier
2023-05-22 12:45:39 +02:00
parent 0326bff379
commit 2186367cb8
28 changed files with 860 additions and 168 deletions

View File

@@ -624,7 +624,7 @@ public enum CypherGenerator {
}
public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntity<?> nodeDescription) {
return createReturnStatementForMatch(nodeDescription, (pp -> true));
return createReturnStatementForMatch(nodeDescription, PropertyFilter.NO_FILTER);
}
/**

View File

@@ -125,7 +125,7 @@ public final class NestedRelationshipContext {
Object value = propertyAccessor.getProperty(inverse);
boolean inverseValueIsEmpty = value == null;
RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((pp -> true)).stream()
RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((PropertyFilter.NO_FILTER)).stream()
.filter(r -> r.getFieldName().equals(inverse.getName())).findFirst().orElseThrow(() -> new MappingException(
neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName()));

View File

@@ -19,6 +19,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.schema.Property;
@@ -39,6 +40,8 @@ public abstract class PropertyFilter {
return new NonFilteringPropertyFilter();
}
public static final Predicate<RelaxedPropertyPath> NO_FILTER = (pp) -> true;
public abstract boolean contains(String dotPath, Class<?> typeToCheck);
public abstract boolean contains(RelaxedPropertyPath propertyPath);

View File

@@ -39,6 +39,7 @@ import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition.Direction;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
@@ -98,7 +99,7 @@ public final class CypherAdapterUtils {
};
}
public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity<?> entity, KeysetScrollPosition scrollPosition, Sort sort) {
public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity<?> entity, KeysetScrollPosition scrollPosition, Sort sort, Neo4jConversionService conversionService) {
var incomingKeys = scrollPosition.getKeys();
var orderedKeys = new LinkedHashMap<String, Object>();
@@ -135,7 +136,7 @@ public final class CypherAdapterUtils {
if (v == null || (v instanceof Value value && value.isNull())) {
throw new IllegalStateException("Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(k));
}
var parameter = Cypher.anonParameter(v);
var parameter = Cypher.anonParameter(conversionService.convert(v, Value.class));
Expression expression;

View File

@@ -239,7 +239,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
queryFragments.setLimit(limitModifier.apply(maxResults.intValue()));
if (!keysetScrollPosition.isInitial()) {
conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, keysetScrollPosition, theSort));
conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, keysetScrollPosition, theSort, mappingContext.getConversionService()));
}
queryFragments.setRequiresReverseSort(keysetScrollPosition.scrollsBackward());

View File

@@ -22,6 +22,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.LongSupplier;
import java.util.function.Predicate;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Condition;
@@ -35,6 +36,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation;
import org.springframework.data.support.PageableExecutionUtils;
@@ -66,7 +68,7 @@ public final class CypherdslConditionExecutorImpl<T> implements CypherdslConditi
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null)
QueryFragmentsAndParameters.forCondition(this.metaData, condition)
).getSingleResult();
}
@@ -75,17 +77,18 @@ public final class CypherdslConditionExecutorImpl<T> implements CypherdslConditi
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null)
QueryFragmentsAndParameters.forCondition(this.metaData, condition)
).getResults();
}
@Override
public Collection<T> findAll(Condition condition, Sort sort) {
Predicate<PropertyFilter.RelaxedPropertyPath> noFilter = PropertyFilter.NO_FILTER;
return this.neo4jOperations.toExecutableQuery(
metaData.getType(),
QueryFragmentsAndParameters.forCondition(
this.metaData, condition, null, CypherAdapterUtils.toSortItems(this.metaData, sort)
QueryFragmentsAndParameters.forConditionAndSort(
this.metaData, condition, sort, null, noFilter
)
).getResults();
}
@@ -95,8 +98,8 @@ public final class CypherdslConditionExecutorImpl<T> implements CypherdslConditi
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(
this.metaData, condition, null, Arrays.asList(sortItems)
QueryFragmentsAndParameters.forConditionAndSortItems(
this.metaData, condition, Arrays.asList(sortItems)
)
).getResults();
}
@@ -106,16 +109,17 @@ public final class CypherdslConditionExecutorImpl<T> implements CypherdslConditi
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, Conditions.noCondition(), null, Arrays.asList(sortItems))
QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Conditions.noCondition(), Arrays.asList(sortItems))
).getResults();
}
@Override
public Page<T> findAll(Condition condition, Pageable pageable) {
Predicate<PropertyFilter.RelaxedPropertyPath> noFilter = PropertyFilter.NO_FILTER;
List<T> page = this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, condition, pageable, null)
QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, condition, pageable, noFilter)
).getResults();
LongSupplier totalCountSupplier = () -> this.count(condition);
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);

View File

@@ -15,23 +15,29 @@
*/
package org.springframework.data.neo4j.repository.query;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Condition;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.core.FluentFindOperation;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.stream.Stream;
import org.apiguardian.api.API;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.FluentFindOperation;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
/**
* Immutable implementation of a {@link FetchableFluentQuery}. All
* 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
Function<Example<S>, Boolean> existsOperation
) {
this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(),
null);
null, null);
}
FetchableFluentQueryByExample(
@@ -75,9 +81,10 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
Function<Example<S>, Long> countOperation,
Function<Example<S>, Boolean> existsOperation,
Sort sort,
@Nullable Integer limit,
@Nullable Collection<String> properties
) {
super(resultType, sort, properties);
super(resultType, sort, limit, properties);
this.mappingContext = mappingContext;
this.example = example;
this.findOperation = findOperation;
@@ -90,7 +97,14 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
public FetchableFluentQuery<R> sortBy(Sort sort) {
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
this.countOperation, this.existsOperation, this.sort.and(sort), this.properties);
this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties);
}
@Override
@SuppressWarnings("HiddenField")
public FetchableFluentQuery<R> limit(int limit) {
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
this.countOperation, this.existsOperation, this.sort, limit, this.properties);
}
@Override
@@ -106,7 +120,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
public FetchableFluentQuery<R> project(Collection<String> properties) {
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
this.countOperation, this.existsOperation, this.sort, mergeProperties(properties));
this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties));
}
@Override
@@ -114,7 +128,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
return findOperation.find(example.getProbeType())
.as(resultType)
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
.matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit,
createIncludedFieldsPredicate()))
.oneValue();
}
@@ -131,7 +145,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
return findOperation.find(example.getProbeType())
.as(resultType)
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
.matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit,
createIncludedFieldsPredicate()))
.all();
}
@@ -141,7 +155,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
List<R> page = findOperation.find(example.getProbeType())
.as(resultType)
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable,
.matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable,
createIncludedFieldsPredicate()))
.all();
@@ -149,6 +163,28 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);
}
@Override
public Window<R> scroll(ScrollPosition scrollPosition) {
Class<S> domainType = this.example.getProbeType();
Neo4jPersistentEntity<?> entity = mappingContext.getPersistentEntity(domainType);
var skip = scrollPosition.isInitial()
? 0
: (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset()
: 0;
Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition
? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService())
: null;
List<R> rawResult = findOperation.find(domainType)
.as(resultType)
.matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate()))
.all();
return scroll(scrollPosition, rawResult, entity);
}
@Override
public Stream<R> stream() {
return all().stream();

View File

@@ -23,10 +23,14 @@ import java.util.stream.Stream;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Cypher;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.core.FluentFindOperation;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
@@ -58,29 +62,35 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
private final Function<Predicate, Boolean> existsOperation;
private final Neo4jMappingContext mappingContext;
FetchableFluentQueryByPredicate(
Predicate predicate,
Neo4jMappingContext mappingContext,
Neo4jPersistentEntity<S> metaData,
Class<R> resultType,
FluentFindOperation findOperation,
Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation
) {
this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null);
this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null, null);
}
FetchableFluentQueryByPredicate(
Predicate predicate,
Neo4jMappingContext mappingContext,
Neo4jPersistentEntity<S> metaData,
Class<R> resultType,
FluentFindOperation findOperation,
Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation,
Sort sort,
@Nullable Integer limit,
@Nullable Collection<String> properties
) {
super(resultType, sort, properties);
super(resultType, sort, limit, properties);
this.predicate = predicate;
this.mappingContext = mappingContext;
this.metaData = metaData;
this.findOperation = findOperation;
this.countOperation = countOperation;
@@ -91,15 +101,22 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
@SuppressWarnings("HiddenField")
public FetchableFluentQuery<R> sortBy(Sort sort) {
return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort.and(sort), this.properties);
return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties);
}
@Override
@SuppressWarnings("HiddenField")
public FetchableFluentQuery<R> limit(int limit) {
return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort, limit, this.properties);
}
@Override
@SuppressWarnings("HiddenField")
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation,
return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation,
this.countOperation, this.existsOperation);
}
@@ -107,8 +124,8 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
@SuppressWarnings("HiddenField")
public FetchableFluentQuery<R> project(Collection<String> properties) {
return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, sort, mergeProperties(properties));
return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties));
}
@Override
@@ -117,10 +134,10 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
return findOperation.find(metaData.getType())
.as(resultType)
.matching(
QueryFragmentsAndParameters.forCondition(metaData,
QueryFragmentsAndParameters.forConditionAndSort(metaData,
Cypher.adapt(predicate).asCondition(),
null,
CypherAdapterUtils.toSortItems(this.metaData, sort),
sort,
limit,
createIncludedFieldsPredicate()))
.oneValue();
}
@@ -138,10 +155,10 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
return findOperation.find(metaData.getType())
.as(resultType)
.matching(
QueryFragmentsAndParameters.forCondition(metaData,
QueryFragmentsAndParameters.forConditionAndSort(metaData,
Cypher.adapt(predicate).asCondition(),
null,
CypherAdapterUtils.toSortItems(this.metaData, sort),
sort,
limit,
createIncludedFieldsPredicate()))
.all();
}
@@ -152,9 +169,9 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
List<R> page = findOperation.find(metaData.getType())
.as(resultType)
.matching(
QueryFragmentsAndParameters.forCondition(metaData,
QueryFragmentsAndParameters.forConditionAndPageable(metaData,
Cypher.adapt(predicate).asCondition(),
pageable, null,
pageable,
createIncludedFieldsPredicate()))
.all();
@@ -162,6 +179,26 @@ final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R>
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);
}
@Override
public Window<R> scroll(ScrollPosition scrollPosition) {
QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters.forConditionWithScrollPosition(metaData,
Cypher.adapt(predicate).asCondition(),
(scrollPosition instanceof KeysetScrollPosition keysetScrollPosition
? CypherAdapterUtils.combineKeysetIntoCondition(metaData, keysetScrollPosition, sort, mappingContext.getConversionService())
: null),
scrollPosition, sort,
limit == null ? 1 : limit + 1,
createIncludedFieldsPredicate());
List<R> rawResult = findOperation.find(metaData.getType())
.as(resultType)
.matching(queryFragmentsAndParameters)
.all();
return scroll(scrollPosition, rawResult, metaData);
}
@Override
public Stream<R> stream() {
return all().stream();

View File

@@ -18,10 +18,19 @@ package org.springframework.data.neo4j.repository.query;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.lang.Nullable;
@@ -38,16 +47,20 @@ abstract class FluentQuerySupport<R> {
protected final Sort sort;
protected final Integer limit;
@Nullable
protected final Set<String> properties;
FluentQuerySupport(
Class<R> resultType,
Sort sort,
@Nullable Integer limit,
@Nullable Collection<String> properties
) {
this.resultType = resultType;
this.sort = sort;
this.limit = limit;
if (properties != null) {
this.properties = new HashSet<>(properties);
} else {
@@ -57,8 +70,8 @@ abstract class FluentQuerySupport<R> {
final Predicate<PropertyFilter.RelaxedPropertyPath> createIncludedFieldsPredicate() {
if (this.properties == null) {
return path -> true;
if (this.properties == null || this.properties.isEmpty()) {
return PropertyFilter.NO_FILTER;
}
return path -> this.properties.contains(path.toDotPath());
}
@@ -71,4 +84,49 @@ abstract class FluentQuerySupport<R> {
newProperties.addAll(additionalProperties);
return Collections.unmodifiableCollection(newProperties);
}
final Window<R> scroll(ScrollPosition scrollPosition, List<R> rawResult, Neo4jPersistentEntity<?> entity) {
var skip = scrollPosition.isInitial()
? 0
: (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset()
: 0;
var scrollDirection = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition ? keysetScrollPosition.getDirection() : ScrollPosition.Direction.FORWARD;
if (scrollDirection == ScrollPosition.Direction.BACKWARD) {
Collections.reverse(rawResult);
}
IntFunction<? extends ScrollPosition> ding = null;
if (scrollPosition instanceof OffsetScrollPosition) {
ding = OffsetScrollPosition.positionFunction(skip);
} else {
ding = v -> {
var accessor = entity.getPropertyAccessor(rawResult.get(v));
var keys = new LinkedHashMap<String, Object>();
sort.forEach(o -> {
// Storing the graph property name here
var persistentProperty = entity.getRequiredPersistentProperty(o.getProperty());
keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty));
});
keys.put(Constants.NAME_OF_ADDITIONAL_SORT, accessor.getProperty(entity.getRequiredIdProperty()));
return ScrollPosition.forward(keys);
};
}
return Window.from(getSubList(rawResult, limit, scrollDirection), ding, hasMoreElements(rawResult, limit));
}
private static boolean hasMoreElements(List<?> result, @Nullable Integer limit) {
return !result.isEmpty() && result.size() > (limit != null ? limit : 0);
}
private static <T> List<T> getSubList(List<T> result, @Nullable Integer limit, ScrollPosition.Direction scrollDirection) {
if (limit != null && limit > 0 && result.size() > limit) {
return scrollDirection == ScrollPosition.Direction.FORWARD ? result.subList(0, limit) : result.subList(1, limit + 1);
}
return result;
}
}

View File

@@ -32,7 +32,6 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
@@ -292,15 +291,17 @@ abstract class Neo4jQuerySupport {
return Window.from(getSubList(rawResult, limit, scrollDirection), v -> {
if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) {
return offsetScrollPosition.advanceBy(v + limit);
} else {
} else {
var accessor = neo4jPersistentEntity.getPropertyAccessor(rawResult.get(v));
var keys = new LinkedHashMap<String, Object>();
orderBy.getSort().forEach(o -> {
// Storing the graph property name here
var persistentProperty = neo4jPersistentEntity.getRequiredPersistentProperty(o.getProperty());
keys.put(persistentProperty.getPropertyName(), conversionService.convert(accessor.getProperty(persistentProperty), Value.class));
keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty));
// keys.put(persistentProperty.getPropertyName(), conversionService.convert(accessor.getProperty(persistentProperty), Value.class));
});
keys.put(Constants.NAME_OF_ADDITIONAL_SORT, conversionService.convert(accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()), Value.class));
keys.put(Constants.NAME_OF_ADDITIONAL_SORT, accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()));
// keys.put(Constants.NAME_OF_ADDITIONAL_SORT, conversionService.convert(accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()), Value.class));
return ScrollPosition.forward(keys);
}
}, hasMoreElements(rawResult, limit));

View File

@@ -24,7 +24,10 @@ import org.neo4j.cypherdsl.core.PatternElement;
import org.neo4j.cypherdsl.core.RelationshipPattern;
import org.neo4j.cypherdsl.core.SortItem;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
@@ -172,32 +175,58 @@ public final class QueryFragmentsAndParameters {
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, null);
}
public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity<?> neo4jPersistentEntity,
@Nullable Pageable pageable, @Nullable Sort sort) {
return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, null, null, null, Collections.emptyMap(), null, null, null);
}
/*
* Following methods are used by the Simple(Reactive)QueryByExampleExecutor
*/
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example) {
return QueryFragmentsAndParameters.forExample(mappingContext, example,
(java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath>) null);
return forExample(mappingContext, example, null, null, null, null, null, null, null);
}
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return QueryFragmentsAndParameters.forExample(mappingContext, example, null, null, includeField);
static QueryFragmentsAndParameters forExampleWithPageable(Neo4jMappingContext mappingContext, Example<?> example, Pageable pageable, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return forExample(mappingContext, example, null, pageable, null, null, null, null, includeField);
}
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example, Sort sort) {
return QueryFragmentsAndParameters.forExample(mappingContext, example, sort, null);
static QueryFragmentsAndParameters forExampleWithSort(Neo4jMappingContext mappingContext, Example<?> example, Sort sort, @Nullable Integer limit, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return forExample(mappingContext, example, null, null, sort, limit, null, null, includeField);
}
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example, Sort sort, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return QueryFragmentsAndParameters.forExample(mappingContext, example, null, sort, includeField);
static QueryFragmentsAndParameters forExampleWithScrollPosition(Neo4jMappingContext mappingContext, Example<?> example, @Nullable Condition keysetScrollPositionCondition, Sort sort, @Nullable Integer limit, @Nullable Long skip, ScrollPosition scrollPosition, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return forExample(mappingContext, example, keysetScrollPositionCondition, null, sort, limit, skip, scrollPosition, includeField);
}
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example, Pageable pageable) {
return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null);
}
private static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example,
@Nullable Condition keysetScrollPositionCondition,
@Nullable Pageable pageable,
@Nullable Sort sort,
@Nullable Integer limit,
@Nullable Long skip,
@Nullable ScrollPosition scrollPosition,
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example, Pageable pageable, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null, includeField);
Predicate predicate = Predicate.create(mappingContext, example);
Map<String, Object> parameters = predicate.getParameters();
Set<PropertyPathWrapper> propertyPathWrappers = predicate.getPropertyPathWrappers();
Condition condition = predicate.getCondition();
Neo4jPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(example.getProbeType());
if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) {
if (!keysetScrollPosition.isInitial()) {
condition = condition.and(keysetScrollPositionCondition);
}
QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(persistentEntity, pageable,
sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers);
queryFragmentsAndParameters.getQueryFragments().setRequiresReverseSort(keysetScrollPosition.scrollsBackward());
return queryFragmentsAndParameters;
}
return getQueryFragmentsAndParameters(persistentEntity, pageable,
sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers);
}
/**
@@ -208,78 +237,86 @@ public final class QueryFragmentsAndParameters {
* @return Fully populated fragments and parameter
*/
@API(status = API.Status.EXPERIMENTAL, since = "6.1.7")
public static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData, Condition condition) {
return forCondition(entityMetaData, condition, null, null);
public static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
Condition condition) {
return forCondition(entityMetaData, condition, null, null, null, null, null, null);
}
static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
Condition condition,
@Nullable Pageable pageable,
@Nullable Collection<SortItem> sortItems
) {
return forCondition(entityMetaData, condition, pageable, sortItems, null);
static QueryFragmentsAndParameters forConditionAndPageable(Neo4jPersistentEntity<?> entityMetaData,
Condition condition, Pageable pageable,
java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return forCondition(entityMetaData, condition, pageable, null, null, null, null, includeField);
}
static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
static QueryFragmentsAndParameters forConditionAndSort(Neo4jPersistentEntity<?> entityMetaData, Condition condition, Sort sort, @Nullable Integer limit,
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return forCondition(entityMetaData, condition, null, sort, null, limit, null, includeField);
}
static QueryFragmentsAndParameters forConditionAndSortItems(Neo4jPersistentEntity<?> entityMetaData, Condition condition, @Nullable Collection<SortItem> sortItems) {
return forCondition(entityMetaData, condition, null, null, sortItems, null, null, null);
}
static QueryFragmentsAndParameters forConditionWithScrollPosition(Neo4jPersistentEntity<?> entityMetaData,
Condition condition,
@Nullable Condition keysetCondition,
ScrollPosition scrollPosition,
@Nullable Sort sort,
@Nullable Integer limit,
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
long skip = 0L;
if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) {
skip = offsetScrollPosition.isInitial()
? 0
: offsetScrollPosition.getOffset();
return forCondition(entityMetaData, condition, null, sort, null, limit, skip, includeField);
}
if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) {
if (!scrollPosition.isInitial()) {
condition = condition.and(keysetCondition);
}
QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(entityMetaData, null,
sort, null, limit, skip, Collections.emptyMap(), condition, includeField, null);
queryFragmentsAndParameters.getQueryFragments().setRequiresReverseSort(keysetScrollPosition.scrollsBackward());
return queryFragmentsAndParameters;
}
throw new IllegalArgumentException("ScrollPosition must be of type OffsetScrollPosition or KeysetScrollPosition. Unexpected type %s found.".formatted(scrollPosition.getClass()));
}
// Parameter re-ordering helper
private static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
Condition condition,
@Nullable Pageable pageable,
@Nullable Sort sort,
@Nullable Collection<SortItem> sortItems,
@Nullable Integer limit,
@Nullable Long skip,
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField
) {
QueryFragments queryFragments = new QueryFragments();
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
queryFragments.setCondition(condition);
if (includeField == null) {
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData));
} else {
queryFragments.setReturnExpressions(
cypherGenerator.createReturnStatementForMatch(entityMetaData, includeField));
}
queryFragments.setRenderConstantsAsParameters(true);
if (pageable != null) {
adaptPageable(entityMetaData, pageable, queryFragments);
} else if (sortItems != null) {
queryFragments.setOrderBy(sortItems);
}
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Collections.emptyMap(), null);
}
private static void adaptPageable(
Neo4jPersistentEntity<?> entityMetaData,
Pageable pageable,
QueryFragments queryFragments
) {
Sort pageableSort = pageable.getSort();
queryFragments.setSkip(pageable.getOffset());
queryFragments.setLimit(pageable.getPageSize());
queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, pageableSort));
}
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example,
@Nullable Pageable pageable, @Nullable Sort sort, java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
Predicate predicate = Predicate.create(mappingContext, example);
Map<String, Object> parameters = predicate.getParameters();
Set<PropertyPathWrapper> propertyPathWrappers = predicate.getPropertyPathWrappers();
Condition condition = predicate.getCondition();
return getQueryFragmentsAndParameters(mappingContext.getPersistentEntity(example.getProbeType()), pageable,
sort, parameters, condition, includeField, propertyPathWrappers);
}
public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity<?> neo4jPersistentEntity,
@Nullable Pageable pageable, @Nullable Sort sort) {
return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, Collections.emptyMap(), null, null, null);
return getQueryFragmentsAndParameters(entityMetaData, pageable, sort, sortItems, limit, skip, Collections.emptyMap(), condition, includeField, null);
}
private static QueryFragmentsAndParameters getQueryFragmentsAndParameters(
Neo4jPersistentEntity<?> entityMetaData, @Nullable Pageable pageable, @Nullable Sort sort,
@Nullable Map<String, Object> parameters, @Nullable Condition condition, @Nullable
java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField,
Neo4jPersistentEntity<?> entityMetaData,
@Nullable Pageable pageable,
@Nullable Sort sort,
@Nullable Collection<SortItem> sortItems,
@Nullable Integer limit,
@Nullable Long skip,
@Nullable Map<String, Object> parameters,
@Nullable Condition condition,
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField,
@Nullable Set<PropertyPathWrapper> propertyPathWrappers) {
QueryFragments queryFragments = new QueryFragments();
@@ -305,11 +342,34 @@ public final class QueryFragmentsAndParameters {
if (pageable != null) {
adaptPageable(entityMetaData, pageable, queryFragments);
} else if (sort != null) {
queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, sort));
} else {
if (sort != null) {
queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, sort));
} else if (sortItems != null) {
queryFragments.setOrderBy(sortItems);
}
if (limit != null) {
// we don't need to additionally pass the limit to the constructor
// because it will get fetched from the QueryFragments later
queryFragments.setLimit(limit);
}
if (skip != null) {
queryFragments.setSkip(skip);
}
}
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, sort);
}
private static void adaptPageable(
Neo4jPersistentEntity<?> entityMetaData,
Pageable pageable,
QueryFragments queryFragments
) {
Sort pageableSort = pageable.getSort();
queryFragments.setSkip(pageable.getOffset());
queryFragments.setLimit(pageable.getPageSize());
queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, pageableSort));
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.FluentFindOperation;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation;
@@ -65,9 +66,15 @@ public final class QuerydslNeo4jPredicateExecutor<T> implements QuerydslPredicat
*/
private final Neo4jPersistentEntity<T> metaData;
public QuerydslNeo4jPredicateExecutor(Neo4jEntityInformation<T, Object> entityInformation,
Neo4jOperations neo4jOperations) {
/**
* Mapping context
*/
private final Neo4jMappingContext mappingContext;
public QuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, Neo4jEntityInformation<T, Object> entityInformation,
Neo4jOperations neo4jOperations) {
this.mappingContext = mappingContext;
this.delegate = new CypherdslConditionExecutorImpl<>(entityInformation, neo4jOperations);
this.neo4jOperations = neo4jOperations;
this.metaData = entityInformation.getEntityMetaData();
@@ -134,7 +141,7 @@ public final class QuerydslNeo4jPredicateExecutor<T> implements QuerydslPredicat
if (this.neo4jOperations instanceof FluentFindOperation ops) {
@SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same.
FetchableFluentQuery<S> fluentQuery =
(FetchableFluentQuery<S>) new FetchableFluentQueryByPredicate<>(predicate, metaData, metaData.getType(),
(FetchableFluentQuery<S>) new FetchableFluentQueryByPredicate<>(predicate, mappingContext, metaData, metaData.getType(),
ops, this::count, this::exists);
return queryFunction.apply(fluentQuery);
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repository.query;
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
import java.util.Arrays;
import java.util.function.Predicate;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Condition;
@@ -29,6 +30,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.ReactiveNeo4jOperations;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor;
import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation;
@@ -63,7 +65,7 @@ public final class ReactiveCypherdslConditionExecutorImpl<T> implements Reactive
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null)
QueryFragmentsAndParameters.forCondition(this.metaData, condition)
).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult);
}
@@ -72,17 +74,18 @@ public final class ReactiveCypherdslConditionExecutorImpl<T> implements Reactive
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null)
QueryFragmentsAndParameters.forCondition(this.metaData, condition)
).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults);
}
@Override
public Flux<T> findAll(Condition condition, Sort sort) {
Predicate<PropertyFilter.RelaxedPropertyPath> noFilter = PropertyFilter.NO_FILTER;
return this.neo4jOperations.toExecutableQuery(
metaData.getType(),
QueryFragmentsAndParameters.forCondition(
this.metaData, condition, null, CypherAdapterUtils.toSortItems(this.metaData, sort)
QueryFragmentsAndParameters.forConditionAndSort(
this.metaData, condition, sort, null, noFilter
)
).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults);
}
@@ -92,8 +95,8 @@ public final class ReactiveCypherdslConditionExecutorImpl<T> implements Reactive
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(
this.metaData, condition, null, Arrays.asList(sortItems)
QueryFragmentsAndParameters.forConditionAndSortItems(
this.metaData, condition, Arrays.asList(sortItems)
)
).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults);
}
@@ -103,7 +106,7 @@ public final class ReactiveCypherdslConditionExecutorImpl<T> implements Reactive
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, Conditions.noCondition(), null,
QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Conditions.noCondition(),
Arrays.asList(sortItems))
).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults);
}

View File

@@ -15,6 +15,12 @@
*/
package org.springframework.data.neo4j.repository.query;
import org.neo4j.cypherdsl.core.Condition;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -64,7 +70,7 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
Function<Example<S>, Mono<Boolean>> existsOperation
) {
this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(),
null);
null, null);
}
ReactiveFluentQueryByExample(
@@ -75,9 +81,10 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
Function<Example<S>, Mono<Long>> countOperation,
Function<Example<S>, Mono<Boolean>> existsOperation,
Sort sort,
@Nullable Integer limit,
@Nullable Collection<String> properties
) {
super(resultType, sort, properties);
super(resultType, sort, limit, properties);
this.mappingContext = mappingContext;
this.example = example;
this.findOperation = findOperation;
@@ -90,7 +97,15 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
public ReactiveFluentQuery<R> sortBy(Sort sort) {
return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
this.countOperation, this.existsOperation, this.sort.and(sort), this.properties);
this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties);
}
@Override
@SuppressWarnings("HiddenField")
public ReactiveFluentQuery<R> limit(int limit) {
return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
this.countOperation, this.existsOperation, this.sort, limit, this.properties);
}
@Override
@@ -106,7 +121,7 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
public ReactiveFluentQuery<R> project(Collection<String> properties) {
return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
this.countOperation, this.existsOperation, sort, mergeProperties(properties));
this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties));
}
@Override
@@ -114,7 +129,7 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
return findOperation.find(example.getProbeType())
.as(resultType)
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
.matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit,
createIncludedFieldsPredicate()))
.one();
}
@@ -130,7 +145,7 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
return findOperation.find(example.getProbeType())
.as(resultType)
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
.matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit,
createIncludedFieldsPredicate()))
.all();
}
@@ -140,7 +155,7 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
Flux<R> results = findOperation.find(example.getProbeType())
.as(resultType)
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable,
.matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable,
createIncludedFieldsPredicate()))
.all();
return results.collectList().zipWith(countOperation.apply(example)).map(tuple -> {
@@ -149,6 +164,28 @@ final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> imp
});
}
@Override
public Mono<Window<R>> scroll(ScrollPosition scrollPosition) {
Class<S> domainType = this.example.getProbeType();
Neo4jPersistentEntity<?> entity = mappingContext.getPersistentEntity(domainType);
var skip = scrollPosition.isInitial()
? 0
: (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset()
: 0;
Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition
? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService())
: null;
return findOperation.find(domainType)
.as(resultType)
.matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate()))
.all()
.collectList()
.map(rawResult -> scroll(scrollPosition, rawResult, entity));
}
@Override
public Mono<Long> count() {
return countOperation.apply(example);

View File

@@ -15,6 +15,10 @@
*/
package org.springframework.data.neo4j.repository.query;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -57,29 +61,35 @@ import com.querydsl.core.types.Predicate;
private final Function<Predicate, Mono<Boolean>> existsOperation;
private final Neo4jMappingContext mappingContext;
ReactiveFluentQueryByPredicate(
Predicate predicate,
Neo4jMappingContext mappingContext,
Neo4jPersistentEntity<S> metaData,
Class<R> resultType,
ReactiveFluentFindOperation findOperation,
Function<Predicate, Mono<Long>> countOperation,
Function<Predicate, Mono<Boolean>> existsOperation
) {
this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null);
this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null, null);
}
ReactiveFluentQueryByPredicate(
Predicate predicate,
Neo4jMappingContext mappingContext,
Neo4jPersistentEntity<S> metaData,
Class<R> resultType,
ReactiveFluentFindOperation findOperation,
Function<Predicate, Mono<Long>> countOperation,
Function<Predicate, Mono<Boolean>> existsOperation,
Sort sort,
@Nullable Integer limit,
@Nullable Collection<String> properties
) {
super(resultType, sort, properties);
super(resultType, sort, limit, properties);
this.predicate = predicate;
this.mappingContext = mappingContext;
this.metaData = metaData;
this.findOperation = findOperation;
this.countOperation = countOperation;
@@ -90,15 +100,22 @@ import com.querydsl.core.types.Predicate;
@SuppressWarnings("HiddenField")
public ReactiveFluentQuery<R> sortBy(Sort sort) {
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort.and(sort), this.properties);
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties);
}
@Override
@SuppressWarnings("HiddenField")
public ReactiveFluentQuery<R> limit(int limit) {
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort, limit, this.properties);
}
@Override
@SuppressWarnings("HiddenField")
public <NR> ReactiveFluentQuery<NR> as(Class<NR> resultType) {
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation,
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation,
this.countOperation, this.existsOperation);
}
@@ -106,8 +123,8 @@ import com.querydsl.core.types.Predicate;
@SuppressWarnings("HiddenField")
public ReactiveFluentQuery<R> project(Collection<String> properties) {
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation,
this.countOperation, this.existsOperation, sort, mergeProperties(properties));
return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation,
this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties));
}
@Override
@@ -116,10 +133,10 @@ import com.querydsl.core.types.Predicate;
return findOperation.find(metaData.getType())
.as(resultType)
.matching(
QueryFragmentsAndParameters.forCondition(metaData,
QueryFragmentsAndParameters.forConditionAndSort(metaData,
Cypher.adapt(predicate).asCondition(),
null,
CypherAdapterUtils.toSortItems(this.metaData, sort),
sort,
limit,
createIncludedFieldsPredicate()))
.one();
}
@@ -136,10 +153,10 @@ import com.querydsl.core.types.Predicate;
return findOperation.find(metaData.getType())
.as(resultType)
.matching(
QueryFragmentsAndParameters.forCondition(metaData,
QueryFragmentsAndParameters.forConditionAndSort(metaData,
Cypher.adapt(predicate).asCondition(),
null,
CypherAdapterUtils.toSortItems(this.metaData, sort),
sort,
limit,
createIncludedFieldsPredicate()))
.all();
}
@@ -150,9 +167,9 @@ import com.querydsl.core.types.Predicate;
Flux<R> results = findOperation.find(metaData.getType())
.as(resultType)
.matching(
QueryFragmentsAndParameters.forCondition(metaData,
QueryFragmentsAndParameters.forConditionAndPageable(metaData,
Cypher.adapt(predicate).asCondition(),
pageable, null,
pageable,
createIncludedFieldsPredicate()))
.all();
@@ -162,6 +179,25 @@ import com.querydsl.core.types.Predicate;
});
}
@Override
public Mono<Window<R>> scroll(ScrollPosition scrollPosition) {
QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters.forConditionWithScrollPosition(metaData,
Cypher.adapt(predicate).asCondition(),
(scrollPosition instanceof KeysetScrollPosition keysetScrollPosition
? CypherAdapterUtils.combineKeysetIntoCondition(metaData, keysetScrollPosition, sort, mappingContext.getConversionService())
: null),
scrollPosition, sort,
limit == null ? 1 : limit + 1,
createIncludedFieldsPredicate());
return findOperation.find(metaData.getType())
.as(resultType)
.matching(queryFragmentsAndParameters)
.all()
.collectList()
.map(rawResult -> scroll(scrollPosition, rawResult, metaData));
}
@Override
public Mono<Long> count() {
return countOperation.apply(predicate);

View File

@@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.query;
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -63,9 +64,15 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor<T> implements Reactive
private final Neo4jPersistentEntity<T> metaData;
public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jEntityInformation<T, Object> entityInformation,
/**
* Mapping context
*/
private final Neo4jMappingContext mappingContext;
public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, Neo4jEntityInformation<T, Object> entityInformation,
ReactiveNeo4jOperations neo4jOperations) {
this.mappingContext = mappingContext;
this.entityInformation = entityInformation;
this.neo4jOperations = neo4jOperations;
this.metaData = this.entityInformation.getEntityMetaData();
@@ -76,8 +83,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor<T> implements Reactive
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition(), null,
null)
QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition())
).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult);
}
@@ -108,7 +114,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor<T> implements Reactive
private Flux<T> doFindAll(Condition condition, Collection<SortItem> sortItems) {
return this.neo4jOperations.toExecutableQuery(
this.metaData.getType(),
QueryFragmentsAndParameters.forCondition(this.metaData, condition, null,
QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition,
sortItems)
).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults);
}
@@ -132,7 +138,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor<T> implements Reactive
if (this.neo4jOperations instanceof ReactiveFluentFindOperation ops) {
@SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same.
ReactiveFluentQuery<S> fluentQuery = (ReactiveFluentQuery<S>) new ReactiveFluentQueryByPredicate<>(predicate, metaData, metaData.getType(),
ReactiveFluentQuery<S> fluentQuery = (ReactiveFluentQuery<S>) new ReactiveFluentQueryByPredicate<>(predicate, mappingContext, metaData, metaData.getType(),
ops, this::count, this::exists);
return queryFunction.apply(fluentQuery);
}

View File

@@ -26,6 +26,7 @@ import org.springframework.data.neo4j.core.FluentFindOperation;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.support.PageableExecutionUtils;
@@ -76,14 +77,14 @@ public final class SimpleQueryByExampleExecutor<T> implements QueryByExampleExec
@Override
public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
return this.neo4jOperations.toExecutableQuery(example.getProbeType(),
QueryFragmentsAndParameters.forExample(mappingContext, example, sort)).getResults();
QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, null, PropertyFilter.NO_FILTER)).getResults();
}
@Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
List<S> page = this.neo4jOperations.toExecutableQuery(example.getProbeType(),
QueryFragmentsAndParameters.forExample(mappingContext, example, pageable)).getResults();
QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, PropertyFilter.NO_FILTER)).getResults();
LongSupplier totalCountSupplier = () -> this.count(example);
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);

View File

@@ -25,6 +25,7 @@ import org.springframework.data.neo4j.core.ReactiveFluentFindOperation;
import org.springframework.data.neo4j.core.ReactiveNeo4jOperations;
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import reactor.core.publisher.Flux;
@@ -75,7 +76,7 @@ public final class SimpleReactiveQueryByExampleExecutor<T> implements ReactiveQu
@Override
public <S extends T> Flux<S> findAll(Example<S> example, Sort sort) {
return this.neo4jOperations
.toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExample(mappingContext, example, sort))
.toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, null, PropertyFilter.NO_FILTER))
.flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults);
}

View File

@@ -90,7 +90,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
if (isQueryDslRepository) {
fragments = fragments.append(createDSLExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class));
fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class));
}
if (CypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
@@ -101,6 +101,14 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
return fragments;
}
private RepositoryFragment<Object> createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class<?> implementor) {
Neo4jEntityInformation<?, Object> entityInformation = getEntityInformation(metadata.getDomainType());
Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations);
return RepositoryFragment.implemented(querydslFragment);
}
private RepositoryFragment<Object> createDSLExecutorFragment(RepositoryMetadata metadata, Class<?> implementor) {
Neo4jEntityInformation<?, Object> entityInformation = getEntityInformation(metadata.getDomainType());

View File

@@ -92,7 +92,7 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp
if (isQueryDslRepository) {
fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class));
fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class));
}
if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
@@ -103,6 +103,14 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp
return fragments;
}
private RepositoryFragment<Object> createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class<?> implementor) {
Neo4jEntityInformation<?, Object> entityInformation = getEntityInformation(metadata.getDomainType());
Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations);
return RepositoryFragment.implemented(querydslFragment);
}
private RepositoryFragment<Object> createDSLExecutorFragment(RepositoryMetadata metadata, Class<?> implementor) {
Neo4jEntityInformation<?, Object> entityInformation = getEntityInformation(metadata.getDomainType());

View File

@@ -18,9 +18,11 @@ package org.springframework.data.neo4j.integration.imperative;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
@@ -28,9 +30,12 @@ import org.neo4j.driver.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
@@ -126,6 +131,96 @@ class QuerydslNeo4jPredicateExecutorIT {
});
}
@Test
@Tag("GH-2726")
void scrollByExampleWithNoOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
Window<Person> peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(0)));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Helge");
assertThat(peopleWindow.isLast()).isFalse();
assertThat(peopleWindow.hasNext()).isTrue();
assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(1));
}
@Test
@Tag("GH-2726")
void scrollByExampleWithOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
Window<Person> peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(1)));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Bela");
assertThat(peopleWindow.isLast()).isTrue();
assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(2));
}
@Test
@Tag("GH-2726")
void scrollByExampleWithContinuingOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
Window<Person> peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(0)));
ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0));
peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Bela");
assertThat(peopleWindow.isLast()).isTrue();
}
@Test
@Tag("GH-2726")
void scrollByExampleWithKeysetOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
Window<Person> peopleWindow = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(ScrollPosition.keyset()));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactly("Bela");
ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.size() - 1);
peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Helge");
assertThat(peopleWindow.isLast()).isTrue();
}
@Test
@Tag("GH-2726")
void scrollByExampleWithKeysetOffsetBackward(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
KeysetScrollPosition startPosition = ScrollPosition.backward(Map.of(
"lastName", "Schneider"
));
Window<Person> peopleWindow = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(startPosition));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactly("Helge");
var nextPos = ScrollPosition.backward(
((KeysetScrollPosition) peopleWindow.positionAt(0)).getKeys());
peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(nextPos));
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Bela");
}
static class DtoPersonProjection {
private final String firstName;
@@ -222,6 +317,18 @@ class QuerydslNeo4jPredicateExecutorIT {
assertThat(count).isEqualTo(2);
}
@Test // GH-2726
void fluentFindAllWithLimitShouldWork(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
List<Person> people = repository.findBy(predicate,
q -> q.limit(1)).all();
assertThat(people).hasSize(1);
assertThat(people).extracting(Person::getFirstName).containsExactly("Helge");
}
@Test
void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) {

View File

@@ -84,6 +84,7 @@ import org.springframework.data.domain.Range.Bound;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
@@ -2880,6 +2881,23 @@ class RepositoryIT {
assertThat(person).isEqualTo(person1);
}
@Test // GH-2726
void scrollByExample(@Autowired PersonRepository repository) {
PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null);
Example<PersonWithAllConstructor> example = Example.of(sameValuePerson,
ExampleMatcher.matchingAll().withIgnoreNullValues());
Window<PersonWithAllConstructor> person = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset(0)));
assertThat(person).isNotNull();
assertThat(person.getContent().get(0)).isEqualTo(person1);
ScrollPosition currentPosition = person.positionAt(person1);
person = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition));
assertThat(person.getContent().get(0)).isEqualTo(person2);
}
@Test
void findAllByExampleWithDifferentMatchers(@Autowired PersonRepository repository) {
@@ -2964,6 +2982,16 @@ class RepositoryIT {
assertThat(persons).containsExactly(person2);
}
@Test // GH-2726
void findAllByExampleWithScrollFluent(@Autowired PersonRepository repository) {
Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
Window<PersonWithAllConstructor> persons = repository.findBy(example,
q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.keyset().forward()));
assertThat(persons.getContent()).containsExactly(person1);
}
@Test
void existsByExample(@Autowired PersonRepository repository) {

View File

@@ -23,12 +23,15 @@ import java.util.function.Function;
import org.assertj.core.data.Index;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Values;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
@@ -107,6 +110,32 @@ class ScrollingIT {
.containsExactly("H0", "I0");
}
@Test
@Tag("GH-2726")
void forwardWithFluentQueryByExample(@Autowired ScrollingRepository scrollingRepository) {
ScrollingEntity scrollingEntity = new ScrollingEntity();
Example<ScrollingEntity> example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues());
var window = scrollingRepository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset()));
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(ScrollingEntity::getA)
.containsExactly("A0", "B0", "C0", "D0");
ScrollPosition newPosition = ScrollPosition.forward(((KeysetScrollPosition) window.positionAt(window.size() - 1)).getKeys());
window = scrollingRepository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(newPosition));
assertThat(window)
.hasSize(4)
.extracting(ScrollingEntity::getA)
.containsExactly("D0", "E0", "F0", "G0");
window = scrollingRepository.findTop4By(ScrollingEntity.SORT_BY_C, window.positionAt(window.size() - 1));
assertThat(window.isLast()).isTrue();
assertThat(window).extracting(ScrollingEntity::getA)
.containsExactly("H0", "I0");
}
@Test
void forwardWithDuplicatesIteratorIteration(@Autowired ScrollingRepository repository) {
@@ -162,6 +191,42 @@ class ScrollingIT {
.containsExactly("A0", "B0");
}
@Test
void backwardWithFluentQueryByExample(@Autowired ScrollingRepository repository) {
ScrollingEntity scrollingEntity = new ScrollingEntity();
Example<ScrollingEntity> example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues());
var last = repository.findFirstByA("I0");
var keys = Map.of(
"c", last.getC(),
Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString())
);
var window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys)));
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(ScrollingEntity::getA)
.containsExactly("F0", "G0", "H0", "I0");
var pos = ((KeysetScrollPosition) window.positionAt(0));
var nextPos = ScrollPosition.backward(pos.getKeys());
window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextPos));
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(Function.identity())
.extracting(ScrollingEntity::getA)
.containsExactly("C0", "D0", "D0", "E0");
var nextNextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys());
window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextPos));
assertThat(window.isLast()).isTrue();
assertThat(window).extracting(ScrollingEntity::getA)
.containsExactly("A0", "B0");
}
@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement

View File

@@ -17,6 +17,8 @@ package org.springframework.data.neo4j.integration.reactive;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Tag;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import reactor.test.StepVerifier;
@@ -188,6 +190,70 @@ class ReactiveQuerydslNeo4jPredicateExecutorIT {
}).verifyComplete();
}
@Test
@Tag("GH-2726")
void scrollByExampleWithNoOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(0)))
.as(StepVerifier::create)
.expectNextMatches(peopleWindow -> {
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Helge");
assertThat(peopleWindow.isLast()).isFalse();
assertThat(peopleWindow.hasNext()).isTrue();
assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(1));
return true;
}).verifyComplete();
}
@Test
@Tag("GH-2726")
void scrollByExampleWithOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(1)))
.as(StepVerifier::create)
.expectNextMatches(peopleWindow -> {
assertThat(peopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Bela");
assertThat(peopleWindow.isLast()).isTrue();
assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(2));
return true;
}).verifyComplete();
}
@Test
@Tag("GH-2726")
void scrollByExampleWithContinuingOffset(@Autowired QueryDSLPersonRepository repository) {
Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))
.or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")));
repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(0)))
.as(StepVerifier::create)
.expectNextMatches(peopleWindow -> {
ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0));
repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition))
.as(StepVerifier::create)
.expectNextMatches(nextPeopleWindow -> {
assertThat(nextPeopleWindow.getContent()).extracting(Person::getFirstName)
.containsExactlyInAnyOrder("Bela");
assertThat(nextPeopleWindow.isLast()).isTrue();
return true;
});
return true;
});
}
@Test // GH-2361
void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) {

View File

@@ -60,6 +60,7 @@ import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.neo4j.core.DatabaseSelection;
@@ -360,6 +361,30 @@ class ReactiveRepositoryIT {
.verifyComplete();
}
@Test // GH-2726
void scrollByExample(@Autowired ReactivePersonRepository repository) {
PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null);
Example<PersonWithAllConstructor> example = Example.of(sameValuePerson,
ExampleMatcher.matchingAll().withIgnoreNullValues());
repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset(0)))
.as(StepVerifier::create)
.expectNextMatches(person -> {
assertThat(person).isNotNull();
assertThat(person.getContent().get(0)).isEqualTo(person1);
ScrollPosition currentPosition = person.positionAt(person1);
repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition))
.as(StepVerifier::create)
.expectNextMatches(nextPerson -> {
assertThat(nextPerson.getContent().get(0)).isEqualTo(person2);
return true;
});
return true;
});
}
@Test
void findAllByExampleWithDifferentMatchers(@Autowired ReactivePersonRepository repository) {
PersonWithAllConstructor person;

View File

@@ -24,12 +24,15 @@ import java.util.function.Function;
import org.assertj.core.data.Index;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Values;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
@@ -180,6 +183,96 @@ class ReactiveScrollingIT {
.containsExactly("A0", "B0");
}
@Test
@Tag("GH-2726")
void forwardWithFluentQueryByExample(@Autowired ReactiveScrollingRepository repository) {
ScrollingEntity scrollingEntity = new ScrollingEntity();
Example<ScrollingEntity> example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues());
var windowContainer = new AtomicReference<Window<ScrollingEntity>>();
repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset()))
.as(StepVerifier::create)
.consumeNextWith(windowContainer::set)
.verifyComplete();
var window = windowContainer.get();
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(ScrollingEntity::getA)
.containsExactly("A0", "B0", "C0", "D0");
ScrollPosition nextScrollPosition = window.positionAt(window.size() - 1);
repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextScrollPosition))
.as(StepVerifier::create)
.consumeNextWith(windowContainer::set)
.verifyComplete();
window = windowContainer.get();
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(ScrollingEntity::getA)
.containsExactly("D0", "E0", "F0", "G0");
ScrollPosition nextNextScrollPosition = window.positionAt(window.size() - 1);
repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextScrollPosition))
.as(StepVerifier::create)
.consumeNextWith(windowContainer::set)
.verifyComplete();
window = windowContainer.get();
assertThat(window.isLast()).isTrue();
assertThat(window).extracting(ScrollingEntity::getA)
.containsExactly("H0", "I0");
}
@Test
@Tag("GH-2726")
void backwardWithFluentQueryByExample(@Autowired ReactiveScrollingRepository repository) {
Example<ScrollingEntity> example = Example.of(new ScrollingEntity(), ExampleMatcher.matchingAll().withIgnoreNullValues());
// Recreate the last position
var last = repository.findFirstByA("I0").block();
var keys = Map.of(
"c", Values.value(last.getC()),
Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString())
);
var windowContainer = new AtomicReference<Window<ScrollingEntity>>();
repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys)))
.as(StepVerifier::create)
.consumeNextWith(windowContainer::set)
.verifyComplete();
var window = windowContainer.get();
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(ScrollingEntity::getA)
.containsExactly("F0", "G0", "H0", "I0");
var nextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys());
repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextPos))
.as(StepVerifier::create)
.consumeNextWith(windowContainer::set)
.verifyComplete();
window = windowContainer.get();
assertThat(window.hasNext()).isTrue();
assertThat(window)
.hasSize(4)
.extracting(Function.identity())
.extracting(ScrollingEntity::getA)
.containsExactly("C0", "D0", "D0", "E0");
var nextNextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys());
repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextPos))
.as(StepVerifier::create)
.consumeNextWith(windowContainer::set)
.verifyComplete();
window = windowContainer.get();
assertThat(window.isLast()).isTrue();
assertThat(window).extracting(ScrollingEntity::getA)
.containsExactly("A0", "B0");
}
@Configuration
@EnableNeo4jRepositories
@EnableReactiveNeo4jRepositories

View File

@@ -37,6 +37,7 @@ public class ScrollingEntity {
* Sorting by b and a will not be unique for 3 and D0, so this will trigger the additional condition based on the id
*/
public static final Sort SORT_BY_B_AND_A = Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"));
public static final Sort SORT_BY_C = Sort.by(Sort.Order.asc("c"));
public static void createTestData(QueryRunner queryRunner) {
queryRunner.run("MATCH (n) DETACH DELETE n");

View File

@@ -44,7 +44,7 @@ class CypherAdapterUtilsTest {
var condition = CypherAdapterUtils.combineKeysetIntoCondition(entity,
ScrollPosition.forward(Map.of("foobar", "D0", "b", 3, "c", LocalDateTime.of(2023, 3, 19, 14, 21, 8, 716))),
Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"), Sort.Order.asc("c"))
Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"), Sort.Order.asc("c")), mappingContext.getConversionService()
);
var expected = """