DATAGRAPH-1424 - Make derived queries target the correct path with multiple relationships to the same target node.

This commit is contained in:
Michael Simons
2020-11-09 11:47:18 +01:00
parent 77fa1b03c8
commit fa6a1aaede
3 changed files with 167 additions and 64 deletions

View File

@@ -147,17 +147,15 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
private static final String NAME_OF_RELATED_FILTER_RELATIONSHIP = "r";
private final int index;
private final Neo4jPersistentProperty leafProperty;
private final List<PersistentProperty<?>> propertyPathList;
private final PersistentPropertyPath<?> propertyPath;
PropertyPathWrapper(int index, PersistentPropertyPath<?> propertyPath) {
this.index = index;
propertyPathList = (List<PersistentProperty<?>>) propertyPath.toList();
this.leafProperty = (Neo4jPersistentProperty) propertyPath.getRequiredLeafProperty();
this.propertyPath = propertyPath;
}
private Neo4jPersistentProperty getLeafProperty() {
return this.leafProperty;
public PersistentPropertyPath<?> getPropertyPath() {
return propertyPath;
}
private String getNodeName() {
@@ -168,18 +166,11 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
return NAME_OF_RELATED_FILTER_RELATIONSHIP + "_" + index;
}
private boolean isLastNode(PersistentProperty<?> persistentProperty) {
// size - 1 = last index
// size - 2 = property on last node
// size - 3 = last node itself
return propertyPathList.indexOf(persistentProperty) > propertyPathList.size() - 3;
}
private ExposesRelationships<?> createRelationshipChain(ExposesRelationships<?> existingRelationshipChain) {
ExposesRelationships<?> cypherRelationship = existingRelationshipChain;
for (PersistentProperty<?> persistentProperty : propertyPathList) {
int cnt = 0;
for (PersistentProperty<?> persistentProperty : propertyPath) {
RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty.getAssociation();
@@ -193,7 +184,10 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
NodeDescription<?> targetEntity = relationshipDescription.getTarget();
Node relatedNode = Cypher.node(targetEntity.getPrimaryLabel(), targetEntity.getAdditionalLabels());
boolean lastNode = isLastNode(persistentProperty);
// length - 1 = last index
// length - 2 = property on last node
// length - 3 = last node itself
boolean lastNode = cnt++ > (propertyPath.getLength() - 3);
if (lastNode || hasTargetNode) {
relatedNode = relatedNode.named(getNodeName());
}
@@ -229,7 +223,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
// if there is no direct property access, the list size is greater than 1 and as a consequence has to contain
// relationships.
private boolean hasRelationships() {
return this.propertyPathList.size() > 1;
return this.propertyPath.getLength() > 1;
}
}
@@ -326,69 +320,69 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
switch (part.getType()) {
case AFTER:
case GREATER_THAN:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.gt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case BEFORE:
case LESS_THAN:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.lt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case BETWEEN:
return betweenCondition(property, actualParameters, ignoreCase);
return betweenCondition(path, actualParameters, ignoreCase);
case CONTAINING:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.contains(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case ENDING_WITH:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.endsWith(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case EXISTS:
return Predicates.exists(toCypherProperty(property));
case FALSE:
return toCypherProperty(property, ignoreCase).isFalse();
return toCypherProperty(path, ignoreCase).isFalse();
case GREATER_THAN_EQUAL:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.gte(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case IN:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.in(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case IS_EMPTY:
return toCypherProperty(property, ignoreCase).isEmpty();
return toCypherProperty(path, ignoreCase).isEmpty();
case IS_NOT_EMPTY:
return toCypherProperty(property, ignoreCase).isEmpty().not();
return toCypherProperty(path, ignoreCase).isEmpty().not();
case IS_NOT_NULL:
return toCypherProperty(property, ignoreCase).isNotNull();
return toCypherProperty(path, ignoreCase).isNotNull();
case IS_NULL:
return toCypherProperty(property, ignoreCase).isNull();
return toCypherProperty(path, ignoreCase).isNull();
case LESS_THAN_EQUAL:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.lte(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case LIKE:
return likeCondition(property, nextRequiredParameter(actualParameters, property).nameOrIndex, ignoreCase);
return likeCondition(path, nextRequiredParameter(actualParameters, property).nameOrIndex, ignoreCase);
case NEAR:
return createNearCondition(property, actualParameters);
return createNearCondition(path, actualParameters);
case NEGATING_SIMPLE_PROPERTY:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.isNotEqualTo(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case NOT_CONTAINING:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.contains(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)).not();
case NOT_IN:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.in(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase)).not();
case NOT_LIKE:
return likeCondition(property, nextRequiredParameter(actualParameters, property).nameOrIndex, ignoreCase).not();
return likeCondition(path, nextRequiredParameter(actualParameters, property).nameOrIndex, ignoreCase).not();
case SIMPLE_PROPERTY:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.isEqualTo(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case STARTING_WITH:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.startsWith(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case REGEX:
return toCypherProperty(property, ignoreCase)
return toCypherProperty(path, ignoreCase)
.matches(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
case TRUE:
return toCypherProperty(property, ignoreCase).isTrue();
return toCypherProperty(path, ignoreCase).isTrue();
case WITHIN:
return createWithinCondition(property, actualParameters);
return createWithinCondition(path, actualParameters);
default:
throw new IllegalArgumentException("Unsupported part type: " + part.getType());
}
@@ -415,32 +409,34 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
}
}
private Condition likeCondition(Neo4jPersistentProperty persistentProperty, String parameterName,
private Condition likeCondition(PersistentPropertyPath<Neo4jPersistentProperty> path, String parameterName,
boolean ignoreCase) {
String regexOptions = ignoreCase ? "(?i)" : "";
return toCypherProperty(persistentProperty, false).matches(
return toCypherProperty(path, false).matches(
Cypher.literalOf(regexOptions + ".*").concat(Cypher.parameter(parameterName)).concat(Cypher.literalOf(".*")));
}
private Condition betweenCondition(Neo4jPersistentProperty persistentProperty, Iterator<Object> actualParameters,
private Condition betweenCondition(PersistentPropertyPath<Neo4jPersistentProperty> path, Iterator<Object> actualParameters,
boolean ignoreCase) {
Parameter lowerBoundOrRange = nextRequiredParameter(actualParameters, persistentProperty);
Neo4jPersistentProperty leafProperty = path.getLeafProperty();
Parameter lowerBoundOrRange = nextRequiredParameter(actualParameters, leafProperty);
Expression property = toCypherProperty(persistentProperty, ignoreCase);
Expression property = toCypherProperty(path, ignoreCase);
if (lowerBoundOrRange.value instanceof Range) {
return createRangeConditionForProperty(property, lowerBoundOrRange);
} else {
Parameter upperBound = nextRequiredParameter(actualParameters, persistentProperty);
Parameter upperBound = nextRequiredParameter(actualParameters, leafProperty);
return property.gte(toCypherParameter(lowerBoundOrRange, ignoreCase))
.and(property.lte(toCypherParameter(upperBound, ignoreCase)));
}
}
private Condition createNearCondition(Neo4jPersistentProperty persistentProperty, Iterator<Object> actualParameters) {
private Condition createNearCondition(PersistentPropertyPath<Neo4jPersistentProperty> path, Iterator<Object> actualParameters) {
Parameter p1 = nextRequiredParameter(actualParameters, persistentProperty);
Optional<Parameter> p2 = nextOptionalParameter(actualParameters, persistentProperty);
Neo4jPersistentProperty leafProperty = path.getRequiredLeafProperty();
Parameter p1 = nextRequiredParameter(actualParameters, leafProperty);
Optional<Parameter> p2 = nextOptionalParameter(actualParameters, leafProperty);
Expression referencePoint;
@@ -456,7 +452,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
String.format("The NEAR operation requires a reference point of type %s", Point.class));
}
Expression distanceFunction = Functions.distance(toCypherProperty(persistentProperty, false), referencePoint);
Expression distanceFunction = Functions.distance(toCypherProperty(path, false), referencePoint);
if (other.filter(p -> p.hasValueOfType(Distance.class)).isPresent()) {
return distanceFunction.lte(toCypherParameter(other.get(), false));
@@ -473,15 +469,16 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
}
}
private Condition createWithinCondition(Neo4jPersistentProperty persistentProperty,
Iterator<Object> actualParameters) {
Parameter area = nextRequiredParameter(actualParameters, persistentProperty);
private Condition createWithinCondition(PersistentPropertyPath<Neo4jPersistentProperty> path, Iterator<Object> actualParameters) {
Neo4jPersistentProperty leafProperty = path.getRequiredLeafProperty();
Parameter area = nextRequiredParameter(actualParameters, leafProperty);
if (area.hasValueOfType(Circle.class)) {
// We don't know the CRS of the point, so we assume the same as the reference toCypherProperty
Expression referencePoint = point(Cypher.mapOf("x", createCypherParameter(area.nameOrIndex + ".x", false), "y",
createCypherParameter(area.nameOrIndex + ".y", false), "srid",
Cypher.property(toCypherProperty(persistentProperty, false), "srid")));
Expression distanceFunction = Functions.distance(toCypherProperty(persistentProperty, false), referencePoint);
Cypher.property(toCypherProperty(path, false), "srid")));
Expression distanceFunction = Functions.distance(toCypherProperty(path, false), referencePoint);
return distanceFunction.lte(createCypherParameter(area.nameOrIndex + ".radius", false));
} else if (area.hasValueOfType(BoundingBox.class) || area.hasValueOfType(Box.class)) {
Expression llx = createCypherParameter(area.nameOrIndex + ".llx", false);
@@ -489,8 +486,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
Expression urx = createCypherParameter(area.nameOrIndex + ".urx", false);
Expression ury = createCypherParameter(area.nameOrIndex + ".ury", false);
Expression x = Cypher.property(toCypherProperty(persistentProperty, false), "x");
Expression y = Cypher.property(toCypherProperty(persistentProperty, false), "y");
Expression x = Cypher.property(toCypherProperty(path, false), "x");
Expression y = Cypher.property(toCypherProperty(path, false), "y");
return llx.lte(x).and(x.lte(urx)).and(lly.lte(y)).and(y.lte(ury));
} else if (area.hasValueOfType(Polygon.class)) {
@@ -531,17 +528,17 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
return Cypher.property(Constants.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName());
}
private Expression toCypherProperty(Neo4jPersistentProperty persistentProperty, boolean addToLower) {
private Expression toCypherProperty(PersistentPropertyPath<Neo4jPersistentProperty> path, boolean addToLower) {
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) persistentProperty.getOwner();
Neo4jPersistentProperty leafProperty = path.getRequiredLeafProperty();
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) leafProperty.getOwner();
Expression expression;
if (owner.equals(this.nodeDescription)) {
expression = Cypher.property(Constants.NAME_OF_ROOT_NODE, persistentProperty.getPropertyName());
expression = Cypher.property(Constants.NAME_OF_ROOT_NODE, leafProperty.getPropertyName());
} else {
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
.filter(rp -> rp.getLeafProperty().equals(persistentProperty)).findFirst().get();
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
String cypherElementName;
// this "entity" is a representation of a relationship with properties
if (owner.isRelationshipPropertiesEntity()) {
@@ -549,7 +546,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
} else {
cypherElementName = propertyPathWrapper.getNodeName();
}
expression = Cypher.property(cypherElementName, persistentProperty.getPropertyName());
expression = Cypher.property(cypherElementName, leafProperty.getPropertyName());
}
if (addToLower) {

View File

@@ -26,10 +26,12 @@ import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.integration.shared.Multiple1O1Relationships;
import org.springframework.data.neo4j.integration.shared.MultipleRelationshipsThing;
import org.springframework.data.neo4j.integration.shared.RelationshipsITBase;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@@ -218,8 +220,41 @@ class RelationshipsIT extends RelationshipsITBase {
}
}
@Test // DATAGRAPH-1424
void shouldMatchOnTheCorrectRelationship(@Autowired Multiple1O1RelationshipsRepository repository) {
try (Session session = driver.session();
Transaction tx = session.beginTransaction()) {
tx.run(""
+ "CREATE (p1:AltPerson {name: 'val1'})\n"
+ "CREATE (p2:AltPerson {name: 'val2'})\n"
+ "CREATE (p3:AltPerson {name: 'val3'})\n"
+ "CREATE (m1:Multiple1O1Relationships {name: 'm1'})\n"
+ "CREATE (m2:Multiple1O1Relationships {name: 'm2'})\n"
+ "CREATE (m1) - [:REL_1] -> (p1)\n"
+ "CREATE (m1) - [:REL_2] -> (p2)\n"
+ "CREATE (m2) - [:REL_1] -> (p1)\n"
+ "CREATE (m2) - [:REL_2] -> (p3)");
tx.commit();
}
List<Multiple1O1Relationships> objects = repository.findAllByPerson1NameAndPerson2Name("val1", "val2");
assertThat(objects).hasSize(1).first()
.satisfies(m -> {
assertThat(m.getName()).isEqualTo("m1");
assertThat(m.getPerson1().getName()).isEqualTo("val1");
assertThat(m.getPerson2().getName()).isEqualTo("val2");
});
}
interface MultipleRelationshipsThingRepository extends CrudRepository<MultipleRelationshipsThing, Long> {}
interface Multiple1O1RelationshipsRepository extends CrudRepository<Multiple1O1Relationships, Long> {
List<Multiple1O1Relationships> findAllByPerson1NameAndPerson2Name(String name1, String name2);
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2011-2020 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.shared;
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;
/**
* @author Michael J. Simons
* @soundtrack Dream Theater - Scenes From A Memory
*/
@Node
public class Multiple1O1Relationships {
@Id @GeneratedValue private Long id;
private String name;
@Relationship("REL_1")
private AltPerson person1;
@Relationship("REL_2")
private AltPerson person2;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public AltPerson getPerson1() {
return person1;
}
public void setPerson1(AltPerson person1) {
this.person1 = person1;
}
public AltPerson getPerson2() {
return person2;
}
public void setPerson2(AltPerson person2) {
this.person2 = person2;
}
}