Skip to content

Commit f1fb0bf

Browse files
feat: Add support for GeoResults, GeoPage and Flux<GeoResult>. (#2939)
This adds support for returning `GeoResults<T>` and `GeoPage<T>` from any imperative repository on "near" queries, with and without maximum or range distance. These iterates will contain `GeoResult<T>`, with each including the distance to the required reference point. For reactive only `Flux<GeoResult<T>>` is supported. This PR also fixes an issues that would occur when using a "near" query on a domain that might contain circles, as the reference place was not passed in the pen-ultimate query generated in those cases. Closes #2908
1 parent 4b552da commit f1fb0bf

22 files changed

+682
-113
lines changed

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,21 +1310,20 @@ private Optional<Neo4jClient.RecordFetchSpec<T>> createFetchSpec() {
13101310

13111311
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
13121312
if (cypherQuery == null || containsPossibleCircles) {
1313-
1313+
Statement statement;
13141314
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
13151315
NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById =
13161316
createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
13171317

13181318
if (nodesAndRelationshipsById.hasRootNodeIds()) {
13191319
return Optional.empty();
13201320
}
1321-
cypherQuery = renderer.render(nodesAndRelationshipsById.toStatement(entityMetaData));
1322-
finalParameters = nodesAndRelationshipsById.getParameters();
1321+
statement = nodesAndRelationshipsById.toStatement(entityMetaData);
13231322
} else {
1324-
Statement statement = queryFragments.toStatement();
1325-
cypherQuery = renderer.render(statement);
1326-
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
1323+
statement = queryFragments.toStatement();
13271324
}
1325+
cypherQuery = renderer.render(statement);
1326+
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
13281327
}
13291328

13301329
Neo4jClient.MappingSpec<T> newMappingSpec = neo4jClient.query(cypherQuery)

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -738,9 +738,11 @@ private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType,
738738
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
739739
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
740740
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters())
741-
.flatMap(finalQueryAndParameters ->
742-
createExecutableQuery(domainType, resultType, renderer.render(finalQueryAndParameters.toStatement(entityMetaData)),
743-
finalQueryAndParameters.getParameters()));
741+
.flatMap(finalQueryAndParameters -> {
742+
var statement = finalQueryAndParameters.toStatement(entityMetaData);
743+
return createExecutableQuery(domainType, resultType, renderer.render(statement),
744+
statement.getCatalog().getParameters());
745+
});
744746
}
745747

746748
return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters());
@@ -1220,9 +1222,11 @@ public <T> Mono<ExecutableQuery<T>> toExecutableQuery(PreparedQuery<T> preparedQ
12201222
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
12211223
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters)
12221224
.map(nodesAndRelationshipsById -> {
1223-
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(renderer.render(
1224-
nodesAndRelationshipsById.toStatement(entityMetaData)))
1225-
.bindAll(nodesAndRelationshipsById.getParameters()).fetchAs(resultType);
1225+
var statement = nodesAndRelationshipsById.toStatement(entityMetaData);
1226+
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient
1227+
.query(renderer.render(statement))
1228+
.bindAll(statement.getCatalog().getParameters())
1229+
.fetchAs(resultType);
12261230

12271231
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery.getOptionalMappingFunction()
12281232
.map(mappingSpec::mappedBy).orElse(mappingSpec);

src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.neo4j.core;
1717

18+
import java.util.ArrayList;
1819
import java.util.Arrays;
1920
import java.util.Collection;
2021
import java.util.Collections;
@@ -35,6 +36,7 @@
3536

3637
import org.apiguardian.api.API;
3738
import org.neo4j.cypherdsl.core.Cypher;
39+
import org.neo4j.cypherdsl.core.Expression;
3840
import org.neo4j.cypherdsl.core.FunctionInvocation;
3941
import org.neo4j.cypherdsl.core.Named;
4042
import org.neo4j.cypherdsl.core.Node;
@@ -199,15 +201,7 @@ static final class NodesAndRelationshipsByIdStatementProvider {
199201
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
200202
this.parameters.put(RELATED_NODE_IDS, relatedNodeIds);
201203
this.queryFragments = queryFragments;
202-
}
203-
204-
Map<String, Object> getParameters() {
205-
Map<String, Object> result = new HashMap<>(3);
206-
result.put(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)));
207-
result.put(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)));
208-
result.put(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)));
209204

210-
return Collections.unmodifiableMap(result);
211205
}
212206

213207
boolean hasRootNodeIds() {
@@ -219,15 +213,22 @@ Statement toStatement(NodeDescription<?> nodeDescription) {
219213
String primaryLabel = nodeDescription.getPrimaryLabel();
220214
Node rootNodes = Cypher.node(primaryLabel).named(ROOT_NODE_IDS);
221215
Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS);
216+
217+
List<Expression> projection = new ArrayList<>();
218+
projection.add(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
219+
projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
220+
projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
221+
projection.addAll(queryFragments.getAdditionalReturnExpressions());
222+
222223
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS);
223224
return Cypher.match(rootNodes)
224-
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
225+
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)))))
225226
.with(Cypher.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE))
226227
.optionalMatch(relationships)
227-
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
228+
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)))))
228229
.with(Constants.NAME_OF_ROOT_NODE, Cypher.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS))
229230
.optionalMatch(relatedNodes)
230-
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
231+
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)))))
231232
.with(
232233
Constants.NAME_OF_ROOT_NODE,
233234
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
@@ -239,11 +240,7 @@ Statement toStatement(NodeDescription<?> nodeDescription) {
239240
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
240241
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES))
241242
.orderBy(queryFragments.getOrderBy())
242-
.returning(
243-
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE),
244-
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
245-
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)
246-
)
243+
.returning(projection)
247244
.skip(queryFragments.getSkip())
248245
.limit(queryFragments.getLimit()).build();
249246
}

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -736,30 +736,35 @@ public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntit
736736
* @param nodeDescription Description of the root node
737737
* @param includeField A predicate derived from the set of included properties. This is only relevant in various forms
738738
* of projections which allow to exclude one or more fields.
739+
* @param additionalExpressions any additional expressions to add to the return statement
739740
* @return An expression to be returned by a Cypher statement
740741
*/
741742
public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntity<?> nodeDescription,
742-
Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
743-
743+
Predicate<PropertyFilter.RelaxedPropertyPath> includeField, Expression... additionalExpressions) {
744744
List<RelationshipDescription> processedRelationships = new ArrayList<>();
745+
745746
if (nodeDescription.containsPossibleCircles(includeField)) {
746-
return createGenericReturnStatement();
747+
return createGenericReturnStatement(additionalExpressions);
747748
} else {
748-
return Collections.singleton(projectPropertiesAndRelationships(
749-
PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()),
750-
nodeDescription,
751-
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
752-
includeField,
753-
null,
754-
processedRelationships));
749+
List<Expression> returnContent = new ArrayList<>();
750+
returnContent.add(projectPropertiesAndRelationships(
751+
PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()),
752+
nodeDescription,
753+
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
754+
includeField,
755+
null,
756+
processedRelationships));
757+
Collections.addAll(returnContent, additionalExpressions);
758+
return returnContent;
755759
}
756760
}
757761

758-
public Collection<Expression> createGenericReturnStatement() {
762+
public Collection<Expression> createGenericReturnStatement(Expression... additionalExpressions) {
759763
List<Expression> returnExpressions = new ArrayList<>();
760764
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
761765
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
762766
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
767+
returnExpressions.addAll(Arrays.asList(additionalExpressions));
763768
return returnExpressions;
764769
}
765770

@@ -770,7 +775,7 @@ public StatementBuilder.OngoingReading prepareFindOf(NodeDescription<?> nodeDesc
770775
}
771776

772777
private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity<?> nodeDescription, SymbolicName nodeName,
773-
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships) {
778+
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships, Expression... additionalExpressions) {
774779

775780
Collection<RelationshipDescription> relationships = ((DefaultNeo4jPersistentEntity<?>) nodeDescription).getRelationshipsInHierarchy(includedProperties, parentPath);
776781
relationships.removeIf(r -> !includedProperties.test(parentPath.append(r.getFieldName())));

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.springframework.data.domain.Pageable;
3333
import org.springframework.data.domain.Slice;
3434
import org.springframework.data.domain.SliceImpl;
35+
import org.springframework.data.geo.GeoPage;
36+
import org.springframework.data.geo.GeoResult;
3537
import org.springframework.data.neo4j.core.Neo4jOperations;
3638
import org.springframework.data.neo4j.core.PreparedQuery;
3739
import org.springframework.data.neo4j.core.PropertyFilterSupport;
@@ -45,6 +47,7 @@
4547
import org.springframework.data.repository.query.ResultProcessor;
4648
import org.springframework.data.repository.query.ReturnedType;
4749
import org.springframework.data.support.PageableExecutionUtils;
50+
import org.springframework.data.util.TypeInformation;
4851
import org.springframework.lang.Nullable;
4952
import org.springframework.util.Assert;
5053

@@ -76,10 +79,32 @@ public QueryMethod getQueryMethod() {
7679
return this.queryMethod;
7780
}
7881

82+
/**
83+
* {@return whether the query is a geo near query}
84+
*/
85+
boolean isGeoNearQuery() {
86+
var repositoryMethod = queryMethod.getMethod();
87+
Class<?> returnType = repositoryMethod.getReturnType();
88+
89+
for (Class<?> type : Neo4jQueryMethod.GEO_NEAR_RESULTS) {
90+
if (type.isAssignableFrom(returnType)) {
91+
return true;
92+
}
93+
}
94+
95+
if (Iterable.class.isAssignableFrom(returnType)) {
96+
TypeInformation<?> from = TypeInformation.fromReturnTypeOf(repositoryMethod);
97+
return GeoResult.class.equals(from.getComponentType().getType());
98+
}
99+
100+
return GeoPage.class.isAssignableFrom(returnType);
101+
}
102+
79103
@Override
80104
public final Object execute(Object[] parameters) {
81105

82106
boolean incrementLimit = queryMethod.incrementLimit();
107+
boolean geoNearQuery = isGeoNearQuery();
83108
Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor(
84109
(Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(),
85110
parameters);
@@ -88,7 +113,7 @@ public final Object execute(Object[] parameters) {
88113
ReturnedType returnedType = resultProcessor.getReturnedType();
89114
PreparedQuery<?> preparedQuery = prepareQuery(returnedType.getReturnedType(),
90115
PropertyFilterSupport.getInputProperties(resultProcessor, factory, mappingContext), parameterAccessor,
91-
null, getMappingFunction(resultProcessor), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
116+
null, getMappingFunction(resultProcessor, geoNearQuery), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
92117

93118
Object rawResult = new Neo4jQueryExecution.DefaultQueryExecution(neo4jOperations).execute(preparedQuery, queryMethod.asCollectionQuery());
94119

@@ -107,7 +132,10 @@ public final Object execute(Object[] parameters) {
107132
rawResult = createSlice(incrementLimit, parameterAccessor, (List<?>) rawResult);
108133
} else if (queryMethod.isScrollQuery()) {
109134
rawResult = createWindow(resultProcessor, incrementLimit, parameterAccessor, (List<?>) rawResult, preparedQuery.getQueryFragmentsAndParameters());
135+
} else if (geoNearQuery) {
136+
rawResult = newGeoResults(rawResult);
110137
}
138+
111139
return resultProcessor.processResult(rawResult, preparingConverter);
112140
}
113141

@@ -121,6 +149,11 @@ private Page<?> createPage(Neo4jParameterAccessor parameterAccessor, List<?> raw
121149

122150
return neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult();
123151
};
152+
153+
if (isGeoNearQuery()) {
154+
return new GeoPage<>(newGeoResults(rawResult), parameterAccessor.getPageable(), totalSupplier.getAsLong());
155+
}
156+
124157
return PageableExecutionUtils.getPage(rawResult, parameterAccessor.getPageable(), totalSupplier);
125158
}
126159

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.neo4j.driver.types.MapAccessor;
2424
import org.neo4j.driver.types.TypeSystem;
2525
import org.springframework.core.convert.converter.Converter;
26+
import org.springframework.data.geo.GeoResult;
2627
import org.springframework.data.neo4j.core.PreparedQuery;
2728
import org.springframework.data.neo4j.core.PropertyFilterSupport;
2829
import org.springframework.data.neo4j.core.ReactiveNeo4jOperations;
@@ -35,6 +36,7 @@
3536
import org.springframework.data.repository.query.RepositoryQuery;
3637
import org.springframework.data.repository.query.ResultProcessor;
3738
import org.springframework.data.repository.query.ReturnedType;
39+
import org.springframework.data.util.TypeInformation;
3840
import org.springframework.lang.Nullable;
3941
import org.springframework.util.Assert;
4042

@@ -67,17 +69,39 @@ public QueryMethod getQueryMethod() {
6769
return this.queryMethod;
6870
}
6971

72+
/**
73+
* {@return whether the query is a geo near query}
74+
*/
75+
boolean isGeoNearQuery() {
76+
var repositoryMethod = queryMethod.getMethod();
77+
Class<?> returnType = repositoryMethod.getReturnType();
78+
79+
for (Class<?> type : Neo4jQueryMethod.GEO_NEAR_RESULTS) {
80+
if (type.isAssignableFrom(returnType)) {
81+
return true;
82+
}
83+
}
84+
85+
if (Flux.class.isAssignableFrom(returnType)) {
86+
TypeInformation<?> from = TypeInformation.fromReturnTypeOf(repositoryMethod);
87+
return GeoResult.class.equals(from.getComponentType().getType());
88+
}
89+
90+
return false;
91+
}
92+
7093
@Override
7194
public final Object execute(Object[] parameters) {
7295

7396
boolean incrementLimit = queryMethod.incrementLimit();
97+
boolean geoNearQuery = isGeoNearQuery();
7498
Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(), parameters);
7599
ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
76100

77101
ReturnedType returnedType = resultProcessor.getReturnedType();
78102
PreparedQuery<?> preparedQuery = prepareQuery(returnedType.getReturnedType(),
79103
PropertyFilterSupport.getInputProperties(resultProcessor, factory, mappingContext), parameterAccessor,
80-
null, getMappingFunction(resultProcessor), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
104+
null, getMappingFunction(resultProcessor, geoNearQuery), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
81105

82106
Object rawResult = new Neo4jQueryExecution.ReactiveQueryExecution(neo4jOperations).execute(preparedQuery,
83107
queryMethod.asCollectionQuery());

0 commit comments

Comments
 (0)