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
This commit is contained in:
@@ -1259,7 +1259,7 @@ public final class Neo4jTemplate implements
|
||||
|
||||
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
|
||||
if (cypherQuery == null || containsPossibleCircles) {
|
||||
|
||||
Statement statement;
|
||||
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
|
||||
NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById =
|
||||
createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
|
||||
@@ -1267,13 +1267,12 @@ public final class Neo4jTemplate implements
|
||||
if (nodesAndRelationshipsById.hasRootNodeIds()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
cypherQuery = renderer.render(nodesAndRelationshipsById.toStatement(entityMetaData));
|
||||
finalParameters = nodesAndRelationshipsById.getParameters();
|
||||
statement = nodesAndRelationshipsById.toStatement(entityMetaData);
|
||||
} else {
|
||||
Statement statement = queryFragments.toStatement();
|
||||
cypherQuery = renderer.render(statement);
|
||||
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
|
||||
statement = queryFragments.toStatement();
|
||||
}
|
||||
cypherQuery = renderer.render(statement);
|
||||
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
|
||||
}
|
||||
|
||||
Neo4jClient.MappingSpec<T> newMappingSpec = neo4jClient.query(cypherQuery)
|
||||
|
||||
@@ -728,9 +728,11 @@ public final class ReactiveNeo4jTemplate implements
|
||||
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
|
||||
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
|
||||
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters())
|
||||
.flatMap(finalQueryAndParameters ->
|
||||
createExecutableQuery(domainType, resultType, renderer.render(finalQueryAndParameters.toStatement(entityMetaData)),
|
||||
finalQueryAndParameters.getParameters()));
|
||||
.flatMap(finalQueryAndParameters -> {
|
||||
var statement = finalQueryAndParameters.toStatement(entityMetaData);
|
||||
return createExecutableQuery(domainType, resultType, renderer.render(statement),
|
||||
statement.getCatalog().getParameters());
|
||||
});
|
||||
}
|
||||
|
||||
return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters());
|
||||
@@ -1146,9 +1148,11 @@ public final class ReactiveNeo4jTemplate implements
|
||||
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
|
||||
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters)
|
||||
.map(nodesAndRelationshipsById -> {
|
||||
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(renderer.render(
|
||||
nodesAndRelationshipsById.toStatement(entityMetaData)))
|
||||
.bindAll(nodesAndRelationshipsById.getParameters()).fetchAs(resultType);
|
||||
var statement = nodesAndRelationshipsById.toStatement(entityMetaData);
|
||||
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient
|
||||
.query(renderer.render(statement))
|
||||
.bindAll(statement.getCatalog().getParameters())
|
||||
.fetchAs(resultType);
|
||||
|
||||
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery.getOptionalMappingFunction()
|
||||
.map(mappingSpec::mappedBy).orElse(mappingSpec);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -35,6 +36,7 @@ import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Expression;
|
||||
import org.neo4j.cypherdsl.core.FunctionInvocation;
|
||||
import org.neo4j.cypherdsl.core.Functions;
|
||||
import org.neo4j.cypherdsl.core.Named;
|
||||
@@ -200,15 +202,7 @@ public final class TemplateSupport {
|
||||
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
|
||||
this.parameters.put(RELATED_NODE_IDS, relatedNodeIds);
|
||||
this.queryFragments = queryFragments;
|
||||
}
|
||||
|
||||
Map<String, Object> getParameters() {
|
||||
Map<String, Object> result = new HashMap<>(3);
|
||||
result.put(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)));
|
||||
result.put(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)));
|
||||
result.put(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)));
|
||||
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
boolean hasRootNodeIds() {
|
||||
@@ -220,15 +214,22 @@ public final class TemplateSupport {
|
||||
String primaryLabel = nodeDescription.getPrimaryLabel();
|
||||
Node rootNodes = Cypher.node(primaryLabel).named(ROOT_NODE_IDS);
|
||||
Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS);
|
||||
|
||||
List<Expression> projection = new ArrayList<>();
|
||||
projection.add(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
|
||||
projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
|
||||
projection.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
|
||||
projection.addAll(queryFragments.getAdditionalReturnExpressions());
|
||||
|
||||
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS);
|
||||
return Cypher.match(rootNodes)
|
||||
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
|
||||
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)))))
|
||||
.with(Functions.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE))
|
||||
.optionalMatch(relationships)
|
||||
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
|
||||
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)))))
|
||||
.with(Constants.NAME_OF_ROOT_NODE, Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS))
|
||||
.optionalMatch(relatedNodes)
|
||||
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
|
||||
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)))))
|
||||
.with(
|
||||
Constants.NAME_OF_ROOT_NODE,
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
@@ -240,11 +241,7 @@ public final class TemplateSupport {
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES))
|
||||
.orderBy(queryFragments.getOrderBy())
|
||||
.returning(
|
||||
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)
|
||||
)
|
||||
.returning(projection)
|
||||
.skip(queryFragments.getSkip())
|
||||
.limit(queryFragments.getLimit()).build();
|
||||
}
|
||||
|
||||
@@ -725,35 +725,40 @@ public enum CypherGenerator {
|
||||
* @param nodeDescription Description of the root node
|
||||
* @param includeField A predicate derived from the set of included properties. This is only relevant in various forms
|
||||
* of projections which allow to exclude one or more fields.
|
||||
* @param additionalExpressions any additional expressions to add to the return statement
|
||||
* @return An expression to be returned by a Cypher statement
|
||||
*/
|
||||
public Collection<Expression> createReturnStatementForMatch(Neo4jPersistentEntity<?> nodeDescription,
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
|
||||
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> includeField, Expression... additionalExpressions) {
|
||||
List<RelationshipDescription> processedRelationships = new ArrayList<>();
|
||||
|
||||
if (nodeDescription.containsPossibleCircles(includeField)) {
|
||||
return createGenericReturnStatement();
|
||||
return createGenericReturnStatement(additionalExpressions);
|
||||
} else {
|
||||
return Collections.singleton(projectPropertiesAndRelationships(
|
||||
PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()),
|
||||
nodeDescription,
|
||||
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
|
||||
includeField,
|
||||
null,
|
||||
processedRelationships));
|
||||
List<Expression> returnContent = new ArrayList<>();
|
||||
returnContent.add(projectPropertiesAndRelationships(
|
||||
PropertyFilter.RelaxedPropertyPath.withRootType(nodeDescription.getUnderlyingClass()),
|
||||
nodeDescription,
|
||||
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
|
||||
includeField,
|
||||
null,
|
||||
processedRelationships));
|
||||
Collections.addAll(returnContent, additionalExpressions);
|
||||
return returnContent;
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<Expression> createGenericReturnStatement() {
|
||||
public Collection<Expression> createGenericReturnStatement(Expression... additionalExpressions) {
|
||||
List<Expression> returnExpressions = new ArrayList<>();
|
||||
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
|
||||
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
|
||||
returnExpressions.add(Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
|
||||
returnExpressions.addAll(Arrays.asList(additionalExpressions));
|
||||
return returnExpressions;
|
||||
}
|
||||
|
||||
private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity<?> nodeDescription, SymbolicName nodeName,
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships) {
|
||||
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships, Expression... additionalExpressions) {
|
||||
|
||||
Collection<RelationshipDescription> relationships = ((DefaultNeo4jPersistentEntity<?>) nodeDescription).getRelationshipsInHierarchy(includedProperties, parentPath);
|
||||
relationships.removeIf(r -> !includedProperties.test(parentPath.append(r.getFieldName())));
|
||||
|
||||
@@ -32,6 +32,8 @@ import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.SliceImpl;
|
||||
import org.springframework.data.geo.GeoPage;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.neo4j.core.Neo4jOperations;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.PropertyFilterSupport;
|
||||
@@ -45,6 +47,7 @@ import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -76,10 +79,32 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
|
||||
return this.queryMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return whether the query is a geo near query}
|
||||
*/
|
||||
boolean isGeoNearQuery() {
|
||||
var repositoryMethod = queryMethod.getMethod();
|
||||
Class<?> returnType = repositoryMethod.getReturnType();
|
||||
|
||||
for (Class<?> type : Neo4jQueryMethod.GEO_NEAR_RESULTS) {
|
||||
if (type.isAssignableFrom(returnType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Iterable.class.isAssignableFrom(returnType)) {
|
||||
TypeInformation<?> from = TypeInformation.fromReturnTypeOf(repositoryMethod);
|
||||
return GeoResult.class.equals(from.getComponentType().getType());
|
||||
}
|
||||
|
||||
return GeoPage.class.isAssignableFrom(returnType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Object execute(Object[] parameters) {
|
||||
|
||||
boolean incrementLimit = queryMethod.incrementLimit();
|
||||
boolean geoNearQuery = isGeoNearQuery();
|
||||
Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor(
|
||||
(Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(),
|
||||
parameters);
|
||||
@@ -88,7 +113,7 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
|
||||
ReturnedType returnedType = resultProcessor.getReturnedType();
|
||||
PreparedQuery<?> preparedQuery = prepareQuery(returnedType.getReturnedType(),
|
||||
PropertyFilterSupport.getInputProperties(resultProcessor, factory, mappingContext), parameterAccessor,
|
||||
null, getMappingFunction(resultProcessor), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
|
||||
null, getMappingFunction(resultProcessor, geoNearQuery), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
|
||||
|
||||
Object rawResult = new Neo4jQueryExecution.DefaultQueryExecution(neo4jOperations).execute(preparedQuery, queryMethod.asCollectionQuery());
|
||||
|
||||
@@ -107,7 +132,10 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
|
||||
rawResult = createSlice(incrementLimit, parameterAccessor, (List<?>) rawResult);
|
||||
} else if (queryMethod.isScrollQuery()) {
|
||||
rawResult = createWindow(resultProcessor, incrementLimit, parameterAccessor, (List<?>) rawResult, preparedQuery.getQueryFragmentsAndParameters());
|
||||
} else if (geoNearQuery) {
|
||||
rawResult = newGeoResults(rawResult);
|
||||
}
|
||||
|
||||
return resultProcessor.processResult(rawResult, preparingConverter);
|
||||
}
|
||||
|
||||
@@ -121,6 +149,11 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
|
||||
|
||||
return neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult();
|
||||
};
|
||||
|
||||
if (isGeoNearQuery()) {
|
||||
return new GeoPage<>(newGeoResults(rawResult), parameterAccessor.getPageable(), totalSupplier.getAsLong());
|
||||
}
|
||||
|
||||
return PageableExecutionUtils.getPage(rawResult, parameterAccessor.getPageable(), totalSupplier);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.function.UnaryOperator;
|
||||
import org.neo4j.driver.types.MapAccessor;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.PropertyFilterSupport;
|
||||
import org.springframework.data.neo4j.core.ReactiveNeo4jOperations;
|
||||
@@ -35,6 +36,7 @@ import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -67,17 +69,39 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R
|
||||
return this.queryMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return whether the query is a geo near query}
|
||||
*/
|
||||
boolean isGeoNearQuery() {
|
||||
var repositoryMethod = queryMethod.getMethod();
|
||||
Class<?> returnType = repositoryMethod.getReturnType();
|
||||
|
||||
for (Class<?> type : Neo4jQueryMethod.GEO_NEAR_RESULTS) {
|
||||
if (type.isAssignableFrom(returnType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Flux.class.isAssignableFrom(returnType)) {
|
||||
TypeInformation<?> from = TypeInformation.fromReturnTypeOf(repositoryMethod);
|
||||
return GeoResult.class.equals(from.getComponentType().getType());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Object execute(Object[] parameters) {
|
||||
|
||||
boolean incrementLimit = queryMethod.incrementLimit();
|
||||
boolean geoNearQuery = isGeoNearQuery();
|
||||
Neo4jParameterAccessor parameterAccessor = new Neo4jParameterAccessor((Neo4jQueryMethod.Neo4jParameters) this.queryMethod.getParameters(), parameters);
|
||||
ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
|
||||
|
||||
ReturnedType returnedType = resultProcessor.getReturnedType();
|
||||
PreparedQuery<?> preparedQuery = prepareQuery(returnedType.getReturnedType(),
|
||||
PropertyFilterSupport.getInputProperties(resultProcessor, factory, mappingContext), parameterAccessor,
|
||||
null, getMappingFunction(resultProcessor), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
|
||||
null, getMappingFunction(resultProcessor, geoNearQuery), incrementLimit ? l -> l + 1 : UnaryOperator.identity());
|
||||
|
||||
Object rawResult = new Neo4jQueryExecution.ReactiveQueryExecution(neo4jOperations).execute(preparedQuery,
|
||||
queryMethod.asCollectionQuery());
|
||||
|
||||
@@ -30,7 +30,6 @@ import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.neo4j.cypherdsl.core.Condition;
|
||||
import org.neo4j.cypherdsl.core.Conditions;
|
||||
@@ -82,9 +81,6 @@ import org.springframework.lang.Nullable;
|
||||
final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndParameters, Condition> {
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
private final QueryMethod queryMethod;
|
||||
|
||||
private final Class<?> domainType;
|
||||
private final NodeDescription<?> nodeDescription;
|
||||
|
||||
private final Neo4jQueryType queryType;
|
||||
@@ -118,6 +114,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
|
||||
private final boolean keysetRequiresSort;
|
||||
|
||||
private final List<Expression> distanceExpressions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Can be used to modify the limit of a paged or sliced query.
|
||||
*/
|
||||
@@ -130,10 +128,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
|
||||
super(tree, actualParameters);
|
||||
this.mappingContext = mappingContext;
|
||||
this.queryMethod = queryMethod;
|
||||
|
||||
this.domainType = domainType;
|
||||
this.nodeDescription = this.mappingContext.getRequiredNodeDescription(this.domainType);
|
||||
this.nodeDescription = this.mappingContext.getRequiredNodeDescription(domainType);
|
||||
|
||||
this.queryType = queryType;
|
||||
this.isDistinct = tree.isDistinct();
|
||||
@@ -248,11 +244,11 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
queryFragments.setLimit(limitModifier.apply(pagingParameter.isUnpaged() ? maxResults.intValue() : pagingParameter.getPageSize()));
|
||||
}
|
||||
|
||||
queryFragments.setReturnBasedOn(nodeDescription, includedProperties, isDistinct);
|
||||
queryFragments.setOrderBy(Stream
|
||||
.concat(sortItems.stream(),
|
||||
theSort.stream().map(CypherAdapterUtils.sortAdapterFor(nodeDescription)))
|
||||
.collect(Collectors.toList()));
|
||||
var finalSortItems = new ArrayList<>(this.sortItems);
|
||||
theSort.stream().map(CypherAdapterUtils.sortAdapterFor(nodeDescription)).forEach(finalSortItems::add);
|
||||
|
||||
queryFragments.setReturnBasedOn(nodeDescription, includedProperties, isDistinct, this.distanceExpressions);
|
||||
queryFragments.setOrderBy(finalSortItems);
|
||||
}
|
||||
|
||||
// closing action: add the condition and path match
|
||||
@@ -371,7 +367,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
|
||||
Expression property = toCypherProperty(path, ignoreCase);
|
||||
if (lowerBoundOrRange.value instanceof Range) {
|
||||
return createRangeConditionForProperty(property, lowerBoundOrRange);
|
||||
return createRangeConditionForExpression(property, lowerBoundOrRange);
|
||||
} else {
|
||||
Parameter upperBound = nextRequiredParameter(actualParameters, leafProperty);
|
||||
return property.gte(toCypherParameter(lowerBoundOrRange, ignoreCase))
|
||||
@@ -401,18 +397,22 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
|
||||
Expression distanceFunction = Functions.distance(toCypherProperty(path, false), referencePoint);
|
||||
|
||||
// Add the distance expression for that property as additional, artificial property to be later retrieved and mapped
|
||||
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) leafProperty.getOwner();
|
||||
String containerName = getContainerName(path, owner);
|
||||
this.distanceExpressions.add(distanceFunction.as("__distance_" + containerName + "_" + leafProperty.getPropertyName() + "__"));
|
||||
|
||||
this.sortItems.add(distanceFunction.ascending());
|
||||
|
||||
if (other.filter(p -> p.hasValueOfType(Distance.class)).isPresent()) {
|
||||
return distanceFunction.lte(toCypherParameter(other.get(), false));
|
||||
} else if (other.filter(p -> p.hasValueOfType(Range.class)).isPresent()) {
|
||||
return createRangeConditionForProperty(distanceFunction, other.get());
|
||||
return createRangeConditionForExpression(distanceFunction, other.get());
|
||||
} else {
|
||||
// We only have a point toCypherParameter, that's ok, but we have to put back the last toCypherParameter when it
|
||||
// wasn't null
|
||||
// We only have a point toCypherParameter, that's ok, but we have to put back the last toCypherParameter when it wasn't null
|
||||
other.ifPresent(this.lastParameter::offer);
|
||||
|
||||
// Also, we cannot filter, but need to sort in the end.
|
||||
this.sortItems.add(distanceFunction.ascending());
|
||||
return Conditions.noCondition();
|
||||
// A `NULL` distance makes no sense in a result asking for places nearby. It would be an arbitrary choice mapping null to zero or a max value.
|
||||
return distanceFunction.isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,24 +448,24 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
}
|
||||
|
||||
/**
|
||||
* @param property property for which the range should get checked
|
||||
* @param expression property for which the range should get checked
|
||||
* @param rangeParameter parameter that expresses the range
|
||||
* @return The equivalent of a A BETWEEN B AND C expression for a given range.
|
||||
* @return The equivalent of a {@code A BETWEEN B AND C} expression for a given range.
|
||||
*/
|
||||
private Condition createRangeConditionForProperty(Expression property, Parameter rangeParameter) {
|
||||
private Condition createRangeConditionForExpression(Expression expression, Parameter rangeParameter) {
|
||||
|
||||
Range range = (Range) rangeParameter.value;
|
||||
Range<?> range = (Range<?>) rangeParameter.value;
|
||||
Condition betweenCondition = Conditions.noCondition();
|
||||
if (range.getLowerBound().isBounded()) {
|
||||
Expression parameterPlaceholder = createCypherParameter(rangeParameter.nameOrIndex + ".lb", false);
|
||||
betweenCondition = betweenCondition.and(
|
||||
range.getLowerBound().isInclusive() ? property.gte(parameterPlaceholder) : property.gt(parameterPlaceholder));
|
||||
range.getLowerBound().isInclusive() ? expression.gte(parameterPlaceholder) : expression.gt(parameterPlaceholder));
|
||||
}
|
||||
|
||||
if (range.getUpperBound().isBounded()) {
|
||||
Expression parameterPlaceholder = createCypherParameter(rangeParameter.nameOrIndex + ".ub", false);
|
||||
betweenCondition = betweenCondition.and(
|
||||
range.getUpperBound().isInclusive() ? property.lte(parameterPlaceholder) : property.lt(parameterPlaceholder));
|
||||
range.getUpperBound().isInclusive() ? expression.lte(parameterPlaceholder) : expression.lt(parameterPlaceholder));
|
||||
}
|
||||
return betweenCondition;
|
||||
}
|
||||
|
||||
@@ -15,17 +15,22 @@
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.data.geo.GeoPage;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.neo4j.repository.support.CypherdslStatementExecutor;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.ParametersSource;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -44,6 +49,8 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
class Neo4jQueryMethod extends QueryMethod {
|
||||
|
||||
static final List<Class<? extends Serializable>> GEO_NEAR_RESULTS = List.of(GeoResult.class, GeoResults.class, GeoPage.class);
|
||||
|
||||
/**
|
||||
* Optional query annotation of the method.
|
||||
*/
|
||||
@@ -53,6 +60,8 @@ class Neo4jQueryMethod extends QueryMethod {
|
||||
|
||||
private final boolean cypherBasedProjection;
|
||||
|
||||
private final Method method;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Neo4jQueryMethod} from the given parameters. Looks up the correct query to use for following
|
||||
* invocations of the method given.
|
||||
@@ -78,10 +87,10 @@ class Neo4jQueryMethod extends QueryMethod {
|
||||
boolean cypherBasedProjection) {
|
||||
super(method, metadata, factory);
|
||||
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
this.repositoryName = declaringClass.getName();
|
||||
this.method = method;
|
||||
this.repositoryName = this.method.getDeclaringClass().getName();
|
||||
this.cypherBasedProjection = cypherBasedProjection;
|
||||
this.queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
|
||||
this.queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(this.method, Query.class);
|
||||
}
|
||||
|
||||
String getRepositoryName() {
|
||||
@@ -111,14 +120,14 @@ class Neo4jQueryMethod extends QueryMethod {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parameters<Neo4jParameters, Neo4jParameter> createParameters(Method method, TypeInformation<?> domainType) {
|
||||
return new Neo4jParameters(method, domainType);
|
||||
protected Parameters<Neo4jParameters, Neo4jParameter> createParameters(ParametersSource parametersSource) {
|
||||
return new Neo4jParameters(parametersSource);
|
||||
}
|
||||
|
||||
static class Neo4jParameters extends Parameters<Neo4jParameters, Neo4jParameter> {
|
||||
|
||||
Neo4jParameters(Method method, TypeInformation<?> domainType) {
|
||||
super(method, it -> new Neo4jParameter(it, domainType));
|
||||
Neo4jParameters(ParametersSource parametersSource) {
|
||||
super(parametersSource, it -> new Neo4jParameter(it, parametersSource.getDomainTypeInformation()));
|
||||
}
|
||||
|
||||
private Neo4jParameters(List<Neo4jParameter> originals) {
|
||||
@@ -131,6 +140,15 @@ class Neo4jQueryMethod extends QueryMethod {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getReturnedObjectType() {
|
||||
Class<?> returnedObjectType = super.getReturnedObjectType();
|
||||
if (returnedObjectType.equals(GeoResult.class)) {
|
||||
return getDomainClass();
|
||||
}
|
||||
return returnedObjectType;
|
||||
}
|
||||
|
||||
static class Neo4jParameter extends Parameter {
|
||||
|
||||
private static final String NAMED_PARAMETER_TEMPLATE = "$%s";
|
||||
@@ -149,15 +167,11 @@ class Neo4jQueryMethod extends QueryMethod {
|
||||
public String getPlaceholder() {
|
||||
|
||||
if (isNamedParameter()) {
|
||||
return String.format(NAMED_PARAMETER_TEMPLATE, getName().get());
|
||||
return String.format(NAMED_PARAMETER_TEMPLATE, getName().orElseThrow());
|
||||
} else {
|
||||
return String.format(POSITION_PARAMETER_TEMPLATE, getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
public String getNameOrIndex() {
|
||||
return this.getName().orElseGet(() -> Integer.toString(this.getIndex()));
|
||||
}
|
||||
}
|
||||
|
||||
boolean incrementLimit() {
|
||||
@@ -165,6 +179,11 @@ class Neo4jQueryMethod extends QueryMethod {
|
||||
}
|
||||
|
||||
boolean asCollectionQuery() {
|
||||
return this.isCollectionLikeQuery() || this.isPageQuery() || this.isSliceQuery() || this.isScrollQuery();
|
||||
return this.isCollectionLikeQuery() || this.isPageQuery() || this.isSliceQuery() || this.isScrollQuery() ||
|
||||
GeoResults.class.isAssignableFrom(this.method.getReturnType());
|
||||
}
|
||||
|
||||
Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.neo4j.driver.Values;
|
||||
@@ -46,6 +47,8 @@ import org.springframework.data.domain.Window;
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.neo4j.core.TemplateSupport;
|
||||
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
|
||||
@@ -110,7 +113,7 @@ abstract class Neo4jQuerySupport {
|
||||
this.queryType = queryType;
|
||||
}
|
||||
|
||||
protected final Supplier<BiFunction<TypeSystem, MapAccessor, ?>> getMappingFunction(final ResultProcessor resultProcessor) {
|
||||
protected final Supplier<BiFunction<TypeSystem, MapAccessor, ?>> getMappingFunction(final ResultProcessor resultProcessor, boolean isGeoNearQuery) {
|
||||
|
||||
return () -> {
|
||||
final ReturnedType returnedTypeMetadata = resultProcessor.getReturnedType();
|
||||
@@ -125,7 +128,9 @@ abstract class Neo4jQuerySupport {
|
||||
mappingFunction = null;
|
||||
} else if (returnedTypeMetadata.isProjecting()) {
|
||||
mappingFunction = EntityInstanceWithSource.decorateMappingFunction(
|
||||
this.mappingContext.getRequiredMappingFunctionFor(domainType));
|
||||
this.mappingContext.getRequiredMappingFunctionFor(domainType));
|
||||
} else if (isGeoNearQuery) {
|
||||
mappingFunction = decorateAsGeoResult(this.mappingContext.getRequiredMappingFunctionFor(domainType));
|
||||
} else {
|
||||
mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(domainType);
|
||||
}
|
||||
@@ -133,6 +138,20 @@ abstract class Neo4jQuerySupport {
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<TypeSystem, MapAccessor, ?> decorateAsGeoResult(BiFunction<TypeSystem, MapAccessor, ?> target) {
|
||||
return (t, r) -> {
|
||||
Object intermediateResult = target.apply(t, r);
|
||||
var distances = StreamSupport.stream(r.keys().spliterator(), false).filter(k -> k.startsWith("__distance_")).toList();
|
||||
if (distances.isEmpty()) {
|
||||
throw new RuntimeException("No distance has been returned by the query, cannot create `GeoResult`");
|
||||
} else if (distances.size() > 1) {
|
||||
throw new RuntimeException("More than one distance has been returned by the query, cannot create `GeoResult`; avoid using multiple near operations when returning `GeoResult`");
|
||||
}
|
||||
var distance = new Distance(r.get(distances.get(0)).asDouble() / 1000.0, Metrics.KILOMETERS);
|
||||
return new GeoResult<>(intermediateResult, distance);
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasValidReturnTypeForDelete(Neo4jQueryMethod queryMethod) {
|
||||
return VALID_RETURN_TYPES_FOR_DELETE.contains(queryMethod.getResultProcessor().getReturnedType().getReturnedType());
|
||||
}
|
||||
@@ -312,6 +331,11 @@ abstract class Neo4jQuerySupport {
|
||||
}, hasMoreElements(rawResult, limit));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static GeoResults<Object> newGeoResults(Object rawResult) {
|
||||
return new GeoResults<>((List<GeoResult<Object>>) rawResult, Metrics.KILOMETERS);
|
||||
}
|
||||
|
||||
private static boolean hasMoreElements(List<?> result, int limit) {
|
||||
return !result.isEmpty() && result.size() > limit;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ public final class QueryFragments {
|
||||
private Long skip;
|
||||
private ReturnTuple returnTuple;
|
||||
private boolean scalarValueReturn = false;
|
||||
private boolean renderConstantsAsParameters = false;
|
||||
private Expression deleteExpression;
|
||||
/**
|
||||
* This flag becomes {@literal true} for backward scrolling keyset pagination. Any {@code AbstractNeo4jQuery} will in turn reverse the result list.
|
||||
@@ -119,26 +118,18 @@ public final class QueryFragments {
|
||||
}
|
||||
|
||||
public void setReturnBasedOn(NodeDescription<?> nodeDescription, Collection<PropertyFilter.ProjectedPath> includedProperties,
|
||||
boolean isDistinct) {
|
||||
this.returnTuple = new ReturnTuple(nodeDescription, includedProperties, isDistinct);
|
||||
boolean isDistinct, List<Expression> additionalExpressions) {
|
||||
this.returnTuple = new ReturnTuple(nodeDescription, includedProperties, isDistinct, additionalExpressions);
|
||||
}
|
||||
|
||||
public boolean isScalarValueReturn() {
|
||||
return scalarValueReturn;
|
||||
}
|
||||
|
||||
public boolean requiresReverseSort() {
|
||||
return requiresReverseSort;
|
||||
}
|
||||
|
||||
public void setRequiresReverseSort(boolean requiresReverseSort) {
|
||||
this.requiresReverseSort = requiresReverseSort;
|
||||
}
|
||||
|
||||
public void setRenderConstantsAsParameters(boolean renderConstantsAsParameters) {
|
||||
this.renderConstantsAsParameters = renderConstantsAsParameters;
|
||||
}
|
||||
|
||||
public Statement toStatement() {
|
||||
|
||||
StatementBuilder.OngoingReadingWithoutWhere match = null;
|
||||
@@ -166,15 +157,17 @@ public final class QueryFragments {
|
||||
.skip(skip)
|
||||
.limit(limit).build();
|
||||
|
||||
statement.setRenderConstantsAsParameters(renderConstantsAsParameters);
|
||||
statement.setRenderConstantsAsParameters(false);
|
||||
return statement;
|
||||
}
|
||||
|
||||
private Collection<Expression> getReturnExpressions() {
|
||||
return returnExpressions.size() > 0
|
||||
? returnExpressions
|
||||
: CypherGenerator.INSTANCE.createReturnStatementForMatch((Neo4jPersistentEntity<?>) returnTuple.nodeDescription,
|
||||
this::includeField);
|
||||
return returnExpressions.isEmpty() ? CypherGenerator.INSTANCE.createReturnStatementForMatch((Neo4jPersistentEntity<?>) returnTuple.nodeDescription,
|
||||
this::includeField, returnTuple.additionalExpressions.toArray(Expression[]::new)) : returnExpressions;
|
||||
}
|
||||
|
||||
public Collection<Expression> getAdditionalReturnExpressions() {
|
||||
return this.returnTuple == null ? List.of() : returnTuple.additionalExpressions;
|
||||
}
|
||||
|
||||
private boolean isDistinctReturn() {
|
||||
@@ -219,6 +212,7 @@ public final class QueryFragments {
|
||||
return skip;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Describes which fields of an entity needs to get returned.
|
||||
*/
|
||||
@@ -226,11 +220,13 @@ public final class QueryFragments {
|
||||
final NodeDescription<?> nodeDescription;
|
||||
final PropertyFilter filteredProperties;
|
||||
final boolean isDistinct;
|
||||
final List<Expression> additionalExpressions;
|
||||
|
||||
private ReturnTuple(NodeDescription<?> nodeDescription, Collection<PropertyFilter.ProjectedPath> filteredProperties, boolean isDistinct) {
|
||||
private ReturnTuple(NodeDescription<?> nodeDescription, Collection<PropertyFilter.ProjectedPath> filteredProperties, boolean isDistinct, List<Expression> additionalExpressions) {
|
||||
this.nodeDescription = nodeDescription;
|
||||
this.filteredProperties = PropertyFilter.from(filteredProperties, nodeDescription);
|
||||
this.isDistinct = isDistinct;
|
||||
this.additionalExpressions = List.copyOf(additionalExpressions);
|
||||
}
|
||||
|
||||
boolean include(PropertyFilter.RelaxedPropertyPath fieldName) {
|
||||
|
||||
@@ -3161,11 +3161,11 @@ class RepositoryIT {
|
||||
|
||||
Record record = doWithSession(session -> session
|
||||
.run("""
|
||||
CREATE
|
||||
(n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}),
|
||||
(n)-[:Has]->(p1:Pet{name: 'Jerry'}),
|
||||
(n)-[:Has]->(p2:Pet{name: 'Tom'}),
|
||||
(p1)-[:Has]->(p3:Pet{name: 'Silvester'})-[:Has]->(h2:Hobby{name: 'Hunt Tweety'})
|
||||
CREATE
|
||||
(n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}),
|
||||
(n)-[:Has]->(p1:Pet{name: 'Jerry'}),
|
||||
(n)-[:Has]->(p2:Pet{name: 'Tom'}),
|
||||
(p1)-[:Has]->(p3:Pet{name: 'Silvester'})-[:Has]->(h2:Hobby{name: 'Hunt Tweety'})
|
||||
RETURN n, h1, p1, p2
|
||||
""").single());
|
||||
|
||||
@@ -3212,10 +3212,10 @@ class RepositoryIT {
|
||||
|
||||
Record record = doWithSession(session -> session
|
||||
.run("""
|
||||
CREATE
|
||||
(n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}),
|
||||
(n)-[:Has]->(p1:Pet{name: 'Jerry'}),
|
||||
(n)-[:Has]->(p2:Pet{name: 'Tom'})
|
||||
CREATE
|
||||
(n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}),
|
||||
(n)-[:Has]->(p1:Pet{name: 'Jerry'}),
|
||||
(n)-[:Has]->(p2:Pet{name: 'Tom'})
|
||||
RETURN n, h1, p1, p2
|
||||
""").single());
|
||||
|
||||
@@ -3251,10 +3251,10 @@ class RepositoryIT {
|
||||
|
||||
Record record = doWithSession(session -> session
|
||||
.run("""
|
||||
CREATE
|
||||
(n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}),
|
||||
(n)-[:Has]->(p1:Pet{name: 'Jerry'}),
|
||||
(n)-[:Has]->(p2:Pet{name: 'Tom'})
|
||||
CREATE
|
||||
(n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}),
|
||||
(n)-[:Has]->(p1:Pet{name: 'Jerry'}),
|
||||
(n)-[:Has]->(p2:Pet{name: 'Tom'})
|
||||
RETURN n, h1, p1, p2
|
||||
""").single());
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.assertj.core.data.Percentage;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||
@@ -60,7 +62,14 @@ import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoPage;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
|
||||
@@ -152,6 +161,11 @@ import org.springframework.data.neo4j.integration.issues.gh2858.GH2858Repository
|
||||
import org.springframework.data.neo4j.integration.issues.gh2886.Apple;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2886.FruitRepository;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2886.Orange;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.HasNameAndPlace;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.HasNameAndPlaceRepository;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.LocatedNodeRepository;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.LocatedNodeWithSelfRefRepository;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.Place;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2918.ConditionNode;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2918.ConditionRepository;
|
||||
import org.springframework.data.neo4j.integration.issues.qbe.A;
|
||||
@@ -212,6 +226,7 @@ class IssuesIT extends TestBase {
|
||||
setupGH2459(transaction);
|
||||
setupGH2572(transaction);
|
||||
setupGH2583(transaction);
|
||||
setupGH2908(transaction);
|
||||
|
||||
transaction.run("CREATE (:A {name: 'A name', id: randomUUID()}) -[:HAS] ->(:B {anotherName: 'Whatever', id: randomUUID()})");
|
||||
|
||||
@@ -1232,6 +1247,75 @@ class IssuesIT extends TestBase {
|
||||
assertThatNoException().isThrownBy(() -> conditionRepository.findById(conditionSaved.uuid));
|
||||
}
|
||||
|
||||
private void assertSupportedGeoResultBehavior(HasNameAndPlaceRepository<? extends HasNameAndPlace> repository) {
|
||||
|
||||
ThrowingConsumer<GeoResult<? extends HasNameAndPlace>> neo4jFoundInTheNearDistance = gr -> {
|
||||
assertThat(gr.getContent().getName()).isEqualTo("NEO4J_HQ");
|
||||
assertThat(gr.getDistance().getValue()).isCloseTo(90 / 1000.0, Percentage.withPercentage(5));
|
||||
};
|
||||
|
||||
GeoResults<? extends HasNameAndPlace> nodes = repository.findAllAsGeoResultsByPlaceNear(Place.SFO.getValue());
|
||||
assertThat(nodes).hasSize(2);
|
||||
var distanceBetweenSFOAndNeo4jHQ = 8830.306;
|
||||
assertThat(nodes.getAverageDistance()).satisfies(d -> {
|
||||
assertThat(d.getValue()).isCloseTo(distanceBetweenSFOAndNeo4jHQ / 2.0, Percentage.withPercentage(1));
|
||||
assertThat(d.getMetric()).isEqualTo(Metrics.KILOMETERS);
|
||||
});
|
||||
|
||||
var zeroTo9k = Distance.between(0, Metrics.KILOMETERS, 90000, Metrics.KILOMETERS);
|
||||
GeoPage<? extends HasNameAndPlace> pagedNodes = repository.findAllByPlaceNear(Place.SFO.getValue(), zeroTo9k, Pageable.ofSize(1));
|
||||
assertThat(pagedNodes).hasSize(1);
|
||||
assertThat(pagedNodes.getAverageDistance().getValue()).isCloseTo(0, Percentage.withPercentage(1));
|
||||
assertThat(pagedNodes.getContent().get(0).getContent().getName()).isEqualTo("SFO");
|
||||
|
||||
pagedNodes = repository.findAllByPlaceNear(Place.SFO.getValue(), zeroTo9k, pagedNodes.nextPageable());
|
||||
assertThat(pagedNodes).hasSize(1);
|
||||
assertThat(pagedNodes.getAverageDistance().getValue()).isCloseTo(distanceBetweenSFOAndNeo4jHQ, Percentage.withPercentage(1));
|
||||
assertThat(pagedNodes.getContent().get(0).getContent().getName()).isEqualTo("NEO4J_HQ");
|
||||
|
||||
var distance = new Distance(200.0 / 1000.0, Metrics.KILOMETERS);
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distance);
|
||||
assertThat(nodes).hasSize(1)
|
||||
.first()
|
||||
.satisfies(neo4jFoundInTheNearDistance);
|
||||
|
||||
nodes = repository.findAllByPlaceNear(Place.CLARION.getValue(), distance);
|
||||
assertThat(nodes).isEmpty();
|
||||
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(),
|
||||
Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS));
|
||||
assertThat(nodes).hasSize(1).first()
|
||||
.satisfies(neo4jFoundInTheNearDistance);
|
||||
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(),
|
||||
Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS));
|
||||
assertThat(nodes).isEmpty();
|
||||
|
||||
final Range<Distance> distanceRange = Range.of(Range.Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)),
|
||||
Range.Bound.unbounded());
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distanceRange);
|
||||
assertThat(nodes).hasSize(1).first().satisfies(gr -> {
|
||||
var d = gr.getDistance();
|
||||
assertThat(d.getValue()).isCloseTo(8800, Percentage.withPercentage(1));
|
||||
assertThat(d.getMetric()).isEqualTo(Metrics.KILOMETERS);
|
||||
assertThat(gr.getContent().getName()).isEqualTo("SFO");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("GH-2908")
|
||||
void shouldSupportGeoResult(@Autowired LocatedNodeRepository repository) {
|
||||
|
||||
assertSupportedGeoResultBehavior(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("GH-2908")
|
||||
void shouldSupportGeoResultWithSelfRef(@Autowired LocatedNodeWithSelfRefRepository repository) {
|
||||
|
||||
assertSupportedGeoResultBehavior(repository);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties")
|
||||
|
||||
@@ -18,6 +18,12 @@ package org.springframework.data.neo4j.integration.issues;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.assertj.core.data.Percentage;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
@@ -65,6 +71,9 @@ import org.springframework.data.neo4j.integration.issues.gh2533.EntitiesAndProje
|
||||
import org.springframework.data.neo4j.integration.issues.gh2533.ReactiveGH2533Repository;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2572.GH2572Child;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2572.ReactiveGH2572Repository;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.LocatedNode;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.Place;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.ReactiveLocatedNodeRepository;
|
||||
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
|
||||
import org.springframework.data.neo4j.test.BookmarkCapture;
|
||||
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
|
||||
@@ -89,6 +98,7 @@ class ReactiveIssuesIT extends TestBase {
|
||||
|
||||
setupGH2328(transaction);
|
||||
setupGH2572(transaction);
|
||||
setupGH2908(transaction);
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
@@ -425,6 +435,47 @@ class ReactiveIssuesIT extends TestBase {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("GH-2908")
|
||||
void shouldSupportGeoResult(@Autowired ReactiveLocatedNodeRepository repository) {
|
||||
|
||||
ThrowingConsumer<GeoResult<LocatedNode>> neo4jFoundInTheNearDistance = gr -> {
|
||||
assertThat(gr.getContent().getName()).isEqualTo("NEO4J_HQ");
|
||||
assertThat(gr.getDistance().getValue()).isCloseTo(90 / 1000.0, Percentage.withPercentage(5));
|
||||
};
|
||||
|
||||
List<GeoResult<LocatedNode>> nodes = repository.findAllAsGeoResultsByPlaceNear(Place.SFO.getValue()).collectList().block();
|
||||
assertThat(nodes).hasSize(2);
|
||||
|
||||
var distance = new Distance(200.0 / 1000.0, Metrics.KILOMETERS);
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distance).collectList().block();
|
||||
assertThat(nodes).hasSize(1)
|
||||
.first()
|
||||
.satisfies(neo4jFoundInTheNearDistance);
|
||||
|
||||
nodes = repository.findAllByPlaceNear(Place.CLARION.getValue(), distance).collectList().block();
|
||||
assertThat(nodes).isEmpty();
|
||||
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(),
|
||||
Distance.between(60.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)).collectList().block();
|
||||
assertThat(nodes).hasSize(1).first()
|
||||
.satisfies(neo4jFoundInTheNearDistance);
|
||||
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(),
|
||||
Distance.between(100.0 / 1000.0, Metrics.KILOMETERS, 200.0 / 1000.0, Metrics.KILOMETERS)).collectList().block();
|
||||
assertThat(nodes).isEmpty();
|
||||
|
||||
final Range<Distance> distanceRange = Range.of(Range.Bound.inclusive(new Distance(100.0 / 1000.0, Metrics.KILOMETERS)),
|
||||
Range.Bound.unbounded());
|
||||
nodes = repository.findAllByPlaceNear(Place.MINC.getValue(), distanceRange).collectList().block();
|
||||
assertThat(nodes).hasSize(1).first().satisfies(gr -> {
|
||||
var d = gr.getDistance();
|
||||
assertThat(d.getValue()).isCloseTo(8800, Percentage.withPercentage(1));
|
||||
assertThat(d.getMetric()).isEqualTo(Metrics.KILOMETERS);
|
||||
assertThat(gr.getContent().getName()).isEqualTo("SFO");
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableReactiveNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties")
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -36,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2328.Entity2328;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2347.Application;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2347.Workflow;
|
||||
import org.springframework.data.neo4j.integration.issues.gh2908.Place;
|
||||
import org.springframework.data.neo4j.test.BookmarkCapture;
|
||||
import org.springframework.data.neo4j.test.Neo4jExtension;
|
||||
|
||||
@@ -119,6 +121,14 @@ abstract class TestBase {
|
||||
queryRunner.run("MATCH (p:GH2572Parent {id: 'GH2572Parent-2'}) CREATE (p) <-[:IS_PET]- (:GH2572Child {id: 'GH2572Child-4', name: 'another-pet'})");
|
||||
}
|
||||
|
||||
protected static void setupGH2908(QueryRunner queryRunner) {
|
||||
EnumSet<Place> places = EnumSet.of(Place.NEO4J_HQ, Place.SFO);
|
||||
for (Place value : places) {
|
||||
queryRunner.run("CREATE (l:LocatedNode {name: $name, place: $place})", Map.of("name", value.name(), "place", value.getValue()));
|
||||
queryRunner.run("CREATE (l:LocatedNodeWithSelfRef {name: $name, place: $place})-[:NEXT]->(n:LocatedNodeWithSelfRef {name: $name + 'next'})", Map.of("name", value.name(), "place", value.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
protected static void assertLabels(BookmarkCapture bookmarkCapture, List<String> ids) {
|
||||
try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) {
|
||||
for (String id : ids) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.neo4j.driver.types.Point;
|
||||
|
||||
/**
|
||||
* Introduced for unifying tests
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public interface HasNameAndPlace {
|
||||
|
||||
String getName();
|
||||
|
||||
Point getPlace();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoPage;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
|
||||
/**
|
||||
* Just extending {@link Neo4jRepository} here did not work, so it's split in the concrete interface.
|
||||
* @author Michael J. Simons
|
||||
* @param <T> Concrete type of the entity
|
||||
*/
|
||||
public interface HasNameAndPlaceRepository<T extends HasNameAndPlace> {
|
||||
|
||||
GeoResults<T> findAllAsGeoResultsByPlaceNear(Point point);
|
||||
|
||||
GeoResults<T> findAllByPlaceNear(Point p, Distance max);
|
||||
|
||||
GeoResults<T> findAllByPlaceNear(Point p, Range<Distance> between);
|
||||
|
||||
GeoPage<T> findAllByPlaceNear(Point p, Range<Distance> between, Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.springframework.data.neo4j.core.schema.GeneratedValue;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
|
||||
/**
|
||||
* A located node without circles.
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node
|
||||
public class LocatedNode implements HasNameAndPlace {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private String id;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Point place;
|
||||
|
||||
public LocatedNode(String name, Point place) {
|
||||
this.name = name;
|
||||
this.place = place;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point getPlace() {
|
||||
return place;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
|
||||
/**
|
||||
* Repository spotting all supported Geo* results.
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public interface LocatedNodeRepository extends HasNameAndPlaceRepository<LocatedNode>, Neo4jRepository<LocatedNode, String> {
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.springframework.data.neo4j.core.schema.GeneratedValue;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
import org.springframework.data.neo4j.core.schema.Relationship;
|
||||
|
||||
/**
|
||||
* A located node with circles.
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node
|
||||
public class LocatedNodeWithSelfRef implements HasNameAndPlace {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private String id;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Point place;
|
||||
|
||||
@Relationship
|
||||
private LocatedNodeWithSelfRef next;
|
||||
|
||||
public LocatedNodeWithSelfRef(String name, Point place) {
|
||||
this.name = name;
|
||||
this.place = place;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point getPlace() {
|
||||
return place;
|
||||
}
|
||||
|
||||
public LocatedNodeWithSelfRef getNext() {
|
||||
return next;
|
||||
}
|
||||
|
||||
public void setNext(LocatedNodeWithSelfRef next) {
|
||||
this.next = next;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
|
||||
/**
|
||||
* Repository spotting all supported Geo* results.
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public interface LocatedNodeWithSelfRefRepository extends HasNameAndPlaceRepository<LocatedNodeWithSelfRef>, Neo4jRepository<LocatedNodeWithSelfRef, String> {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.Point;
|
||||
|
||||
/**
|
||||
* Cool places to be.
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public enum Place {
|
||||
|
||||
NEO4J_HQ(Values.point(4326, 12.994823, 55.612191).asPoint()),
|
||||
SFO(Values.point(4326, -122.38681, 37.61649).asPoint()),
|
||||
CLARION(Values.point(4326, 12.994243, 55.607726).asPoint()),
|
||||
MINC(Values.point(4326, 12.994039, 55.611496).asPoint());
|
||||
|
||||
private final Point value;
|
||||
|
||||
Place(Point value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Point getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration.issues.gh2908;
|
||||
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Repository spotting all supported Geo* results.
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public interface ReactiveLocatedNodeRepository extends ReactiveNeo4jRepository<LocatedNode, String> {
|
||||
|
||||
Flux<GeoResult<LocatedNode>> findAllAsGeoResultsByPlaceNear(Point point);
|
||||
|
||||
Flux<GeoResult<LocatedNode>> findAllByPlaceNear(Point p, Distance max);
|
||||
|
||||
Flux<GeoResult<LocatedNode>> findAllByPlaceNear(Point p, Range<Distance> between);
|
||||
}
|
||||
Reference in New Issue
Block a user