Skip to content

Commit 9cc5a85

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 48691a7 commit 9cc5a85

22 files changed

+681
-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
@@ -1259,21 +1259,20 @@ private Optional<Neo4jClient.RecordFetchSpec<T>> createFetchSpec() {
12591259

12601260
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
12611261
if (cypherQuery == null || containsPossibleCircles) {
1262-
1262+
Statement statement;
12631263
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
12641264
NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById =
12651265
createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
12661266

12671267
if (nodesAndRelationshipsById.hasRootNodeIds()) {
12681268
return Optional.empty();
12691269
}
1270-
cypherQuery = renderer.render(nodesAndRelationshipsById.toStatement(entityMetaData));
1271-
finalParameters = nodesAndRelationshipsById.getParameters();
1270+
statement = nodesAndRelationshipsById.toStatement(entityMetaData);
12721271
} else {
1273-
Statement statement = queryFragments.toStatement();
1274-
cypherQuery = renderer.render(statement);
1275-
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
1272+
statement = queryFragments.toStatement();
12761273
}
1274+
cypherQuery = renderer.render(statement);
1275+
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
12771276
}
12781277

12791278
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
@@ -728,9 +728,11 @@ private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType,
728728
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
729729
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
730730
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters())
731-
.flatMap(finalQueryAndParameters ->
732-
createExecutableQuery(domainType, resultType, renderer.render(finalQueryAndParameters.toStatement(entityMetaData)),
733-
finalQueryAndParameters.getParameters()));
731+
.flatMap(finalQueryAndParameters -> {
732+
var statement = finalQueryAndParameters.toStatement(entityMetaData);
733+
return createExecutableQuery(domainType, resultType, renderer.render(statement),
734+
statement.getCatalog().getParameters());
735+
});
734736
}
735737

736738
return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters());
@@ -1146,9 +1148,11 @@ public <T> Mono<ExecutableQuery<T>> toExecutableQuery(PreparedQuery<T> preparedQ
11461148
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
11471149
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters)
11481150
.map(nodesAndRelationshipsById -> {
1149-
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(renderer.render(
1150-
nodesAndRelationshipsById.toStatement(entityMetaData)))
1151-
.bindAll(nodesAndRelationshipsById.getParameters()).fetchAs(resultType);
1151+
var statement = nodesAndRelationshipsById.toStatement(entityMetaData);
1152+
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient
1153+
.query(renderer.render(statement))
1154+
.bindAll(statement.getCatalog().getParameters())
1155+
.fetchAs(resultType);
11521156

11531157
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery.getOptionalMappingFunction()
11541158
.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.Functions;
4042
import org.neo4j.cypherdsl.core.Named;
@@ -200,15 +202,7 @@ static final class NodesAndRelationshipsByIdStatementProvider {
200202
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
201203
this.parameters.put(RELATED_NODE_IDS, relatedNodeIds);
202204
this.queryFragments = queryFragments;
203-
}
204-
205-
Map<String, Object> getParameters() {
206-
Map<String, Object> result = new HashMap<>(3);
207-
result.put(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)));
208-
result.put(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)));
209-
result.put(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)));
210205

211-
return Collections.unmodifiableMap(result);
212206
}
213207

214208
boolean hasRootNodeIds() {
@@ -220,15 +214,22 @@ Statement toStatement(NodeDescription<?> nodeDescription) {
220214
String primaryLabel = nodeDescription.getPrimaryLabel();
221215
Node rootNodes = Cypher.node(primaryLabel).named(ROOT_NODE_IDS);
222216
Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS);
217+
218+
List<Expression> projection = new ArrayList<>();
219+
projection.add(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
220+
projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
221+
projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
222+
projection.addAll(queryFragments.getAdditionalReturnExpressions());
223+
223224
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS);
224225
return Cypher.match(rootNodes)
225-
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
226+
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)))))
226227
.with(Functions.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE))
227228
.optionalMatch(relationships)
228-
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
229+
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)))))
229230
.with(Constants.NAME_OF_ROOT_NODE, Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS))
230231
.optionalMatch(relatedNodes)
231-
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
232+
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)))))
232233
.with(
233234
Constants.NAME_OF_ROOT_NODE,
234235
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
@@ -240,11 +241,7 @@ Statement toStatement(NodeDescription<?> nodeDescription) {
240241
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
241242
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES))
242243
.orderBy(queryFragments.getOrderBy())
243-
.returning(
244-
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE),
245-
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
246-
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)
247-
)
244+
.returning(projection)
248245
.skip(queryFragments.getSkip())
249246
.limit(queryFragments.getLimit()).build();
250247
}

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
@@ -725,35 +725,40 @@ public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntit
725725
* @param nodeDescription Description of the root node
726726
* @param includeField A predicate derived from the set of included properties. This is only relevant in various forms
727727
* of projections which allow to exclude one or more fields.
728+
* @param additionalExpressions any additional expressions to add to the return statement
728729
* @return An expression to be returned by a Cypher statement
729730
*/
730731
public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntity<?> nodeDescription,
731-
Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
732-
732+
Predicate<PropertyFilter.RelaxedPropertyPath> includeField, Expression... additionalExpressions) {
733733
List<RelationshipDescription> processedRelationships = new ArrayList<>();
734+
734735
if (nodeDescription.containsPossibleCircles(includeField)) {
735-
return createGenericReturnStatement();
736+
return createGenericReturnStatement(additionalExpressions);
736737
} else {
737-
return Collections.singleton(projectPropertiesAndRelationships(
738-
PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()),
739-
nodeDescription,
740-
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
741-
includeField,
742-
null,
743-
processedRelationships));
738+
List<Expression> returnContent = new ArrayList<>();
739+
returnContent.add(projectPropertiesAndRelationships(
740+
PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()),
741+
nodeDescription,
742+
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
743+
includeField,
744+
null,
745+
processedRelationships));
746+
Collections.addAll(returnContent, additionalExpressions);
747+
return returnContent;
744748
}
745749
}
746750

747-
public Collection<Expression> createGenericReturnStatement() {
751+
public Collection<Expression> createGenericReturnStatement(Expression... additionalExpressions) {
748752
List<Expression> returnExpressions = new ArrayList<>();
749753
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
750754
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
751755
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
756+
returnExpressions.addAll(Arrays.asList(additionalExpressions));
752757
return returnExpressions;
753758
}
754759

755760
private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity<?> nodeDescription, SymbolicName nodeName,
756-
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships) {
761+
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships, Expression... additionalExpressions) {
757762

758763
Collection<RelationshipDescription> relationships = ((DefaultNeo4jPersistentEntity<?>) nodeDescription).getRelationshipsInHierarchy(includedProperties, parentPath);
759764
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)