Skip to content

Commit 889253d

Browse files
GH-2618 - Allow the use of composite values as ids.
Also allow the use of composite values in derived findBy… methods. Closes #2618.
1 parent 985da3a commit 889253d

File tree

9 files changed

+482
-32
lines changed

9 files changed

+482
-32
lines changed

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

+22-9
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,22 @@ public Statement prepareDeleteOf(NodeDescription<?> nodeDescription, @Nullable C
259259
return ongoingUpdate.build();
260260
}
261261

262+
public Condition createCompositePropertyCondition(GraphPropertyDescription idProperty, SymbolicName containerName, Expression actualParameter) {
263+
264+
if (!idProperty.isComposite()) {
265+
return Cypher.property(containerName, idProperty.getPropertyName()).isEqualTo(actualParameter);
266+
}
267+
268+
Neo4jPersistentProperty property = (Neo4jPersistentProperty) idProperty;
269+
270+
Condition result = Conditions.noCondition();
271+
for (String key : property.getOptionalConverter().write(null).keys()) {
272+
Property expression = Cypher.property(containerName, key);
273+
result = result.and(expression.isEqualTo(actualParameter.property(key)));
274+
}
275+
return result;
276+
}
277+
262278
public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
263279
UnaryOperator<OngoingMatchAndUpdate> updateDecorator) {
264280

@@ -271,16 +287,16 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
271287
Parameter<?> idParameter = parameter(Constants.NAME_OF_ID);
272288

273289
if (!idDescription.isInternallyGeneratedId()) {
274-
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
275-
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
290+
291+
GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity<?>) nodeDescription).getRequiredIdProperty();
276292

277293
if (((Neo4jPersistentEntity<?>) nodeDescription).hasVersionProperty()) {
278294
Property versionProperty = rootNode.property(((Neo4jPersistentEntity<?>) nodeDescription).getRequiredVersionProperty().getName());
279295
String nameOfPossibleExistingNode = "hlp";
280296
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
281297

282298
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
283-
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
299+
.where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter))
284300
.with(possibleExistingNode)
285301
.where(possibleExistingNode.isNull())
286302
.create(rootNode.withProperties(versionProperty, literalOf(0)))
@@ -289,7 +305,7 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
289305
.build();
290306

291307
Statement updateIfExists = updateDecorator.apply(match(rootNode)
292-
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
308+
.where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter))
293309
.and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check
294310
.set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock
295311
.with(rootNode)
@@ -301,14 +317,11 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
301317
return Cypher.union(createIfNew, updateIfExists);
302318

303319
} else {
304-
// if (1==1)
305-
// return updateDecorator.apply(Cypher.merge(rootNode.withProperties(nameOfIdProperty, idParameter)).mutate(rootNode,
306-
// parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode).build();
307320
String nameOfPossibleExistingNode = "hlp";
308321
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
309322

310323
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
311-
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
324+
.where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter))
312325
.with(possibleExistingNode)
313326
.where(possibleExistingNode.isNull())
314327
.create(rootNode)
@@ -317,7 +330,7 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
317330
.build();
318331

319332
Statement updateIfExists = updateDecorator.apply(match(rootNode)
320-
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
333+
.where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter))
321334
.with(rootNode)
322335
.mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
323336
.returning(rootNode)

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

+7
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ default boolean isUsingInternalIds() {
139139
*/
140140
default Expression getIdExpression() {
141141

142+
if (this.getIdDescription().getOptionalGraphPropertyName()
143+
.flatMap(this::getGraphProperty)
144+
.filter(GraphPropertyDescription::isComposite)
145+
.isPresent()) {
146+
throw new IllegalStateException("A composite id property cannot be used as ID expression.");
147+
}
148+
142149
return this.getIdDescription().asIdExpression();
143150
}
144151

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

+36-15
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,19 @@ private Condition createImpl(Part part, Iterator<Object> actualParameters) {
331331
Neo4jPersistentProperty property = path.getRequiredLeafProperty();
332332

333333
boolean ignoreCase = ignoreCase(part);
334+
335+
if (property.isComposite()) {
336+
337+
Condition compositePropertyCondition = CypherGenerator.INSTANCE.createCompositePropertyCondition(
338+
property,
339+
Cypher.name(getContainerName(path, (Neo4jPersistentEntity<?>) property.getOwner())),
340+
toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
341+
if (part.getType() == Part.Type.NEGATING_SIMPLE_PROPERTY) {
342+
compositePropertyCondition = Conditions.not(compositePropertyCondition);
343+
}
344+
return compositePropertyCondition;
345+
}
346+
334347
switch (part.getType()) {
335348
case AFTER:
336349
case GREATER_THAN:
@@ -557,25 +570,15 @@ private Expression toCypherProperty(PersistentPropertyPath<Neo4jPersistentProper
557570
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) leafProperty.getOwner();
558571
Expression expression;
559572

573+
String containerName = getContainerName(path, owner);
560574
if (owner.equals(this.nodeDescription) && path.getLength() == 1) {
561575
expression = leafProperty.isInternalIdProperty() ?
562576
Cypher.call("id").withArgs(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)).asFunction() :
563-
Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), leafProperty.getPropertyName());
577+
Cypher.property(containerName, leafProperty.getPropertyName());
578+
} else if (leafProperty.isInternalIdProperty()) {
579+
expression = Cypher.call("id").withArgs(Cypher.name(containerName)).asFunction();
564580
} else {
565-
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
566-
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
567-
String cypherElementName;
568-
// this "entity" is a representation of a relationship with properties
569-
if (owner.isRelationshipPropertiesEntity()) {
570-
cypherElementName = propertyPathWrapper.getRelationshipName();
571-
} else {
572-
cypherElementName = propertyPathWrapper.getNodeName();
573-
}
574-
if (leafProperty.isInternalIdProperty()) {
575-
expression = Cypher.call("id").withArgs(Cypher.name(cypherElementName)).asFunction();
576-
} else {
577-
expression = Cypher.property(cypherElementName, leafProperty.getPropertyName());
578-
}
581+
expression = Cypher.property(containerName, leafProperty.getPropertyName());
579582
}
580583

581584
if (addToLower) {
@@ -585,6 +588,24 @@ private Expression toCypherProperty(PersistentPropertyPath<Neo4jPersistentProper
585588
return expression;
586589
}
587590

591+
private String getContainerName(PersistentPropertyPath<Neo4jPersistentProperty> path, Neo4jPersistentEntity<?> owner) {
592+
593+
if (owner.equals(this.nodeDescription) && path.getLength() == 1) {
594+
return Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription).getValue();
595+
}
596+
597+
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
598+
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
599+
String cypherElementName;
600+
// this "entity" is a representation of a relationship with properties
601+
if (owner.isRelationshipPropertiesEntity()) {
602+
cypherElementName = propertyPathWrapper.getRelationshipName();
603+
} else {
604+
cypherElementName = propertyPathWrapper.getNodeName();
605+
}
606+
return cypherElementName;
607+
}
608+
588609
private Expression toCypherParameter(Parameter parameter, boolean addToLower) {
589610

590611
return createCypherParameter(parameter.nameOrIndex, addToLower);

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class PartValidator {
6363
Part.Type.ENDING_WITH, Part.Type.LIKE, Part.Type.NEGATING_SIMPLE_PROPERTY, Part.Type.NOT_CONTAINING,
6464
Part.Type.NOT_LIKE, Part.Type.SIMPLE_PROPERTY, Part.Type.STARTING_WITH);
6565

66+
private static final EnumSet<Part.Type> TYPES_SUPPORTED_FOR_COMPOSITES = EnumSet.of(Part.Type.SIMPLE_PROPERTY, Part.Type.NEGATING_SIMPLE_PROPERTY);
67+
6668
private final Neo4jMappingContext mappingContext;
6769
private final Neo4jQueryMethod queryMethod;
6870

@@ -89,7 +91,9 @@ void validatePart(Part part) {
8991
break;
9092
}
9193

92-
validateNotACompositeProperty(part);
94+
if (!TYPES_SUPPORTED_FOR_COMPOSITES.contains(part.getType())) {
95+
validateNotACompositeProperty(part);
96+
}
9397
}
9498

9599
private void validateNotACompositeProperty(Part part) {
@@ -138,7 +142,7 @@ private static String formatTypes(Collection<Part.Type> types) {
138142
* Checks whether the given part can be queried without case sensitivity.
139143
*
140144
* @param part query part to check if ignoring case sensitivity is possible
141-
* @return True when {@code part} can be queried case insensitive.
145+
* @return True when {@code part} can be queried case-insensitive.
142146
*/
143147
static boolean canIgnoreCase(Part part) {
144148
return part.getProperty().getLeafType() == String.class

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

+36-6
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717

1818
import static org.neo4j.cypherdsl.core.Cypher.parameter;
1919

20+
import java.util.ArrayList;
2021
import java.util.Collection;
2122
import java.util.Collections;
23+
import java.util.List;
2224
import java.util.Map;
2325

2426
import org.apiguardian.api.API;
2527
import org.neo4j.cypherdsl.core.Condition;
2628
import org.neo4j.cypherdsl.core.Conditions;
29+
import org.neo4j.cypherdsl.core.Cypher;
30+
import org.neo4j.cypherdsl.core.Node;
2731
import org.neo4j.cypherdsl.core.SortItem;
2832
import org.springframework.data.domain.Example;
2933
import org.springframework.data.domain.Pageable;
@@ -94,9 +98,16 @@ public void setParameters(Map<String, Object> newParameters) {
9498
public static QueryFragmentsAndParameters forFindById(Neo4jPersistentEntity<?> entityMetaData, Object idValues) {
9599
Map<String, Object> parameters = Collections.singletonMap(Constants.NAME_OF_ID, idValues);
96100

97-
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
101+
Node container = cypherGenerator.createRootNode(entityMetaData);
102+
Condition condition;
103+
if (entityMetaData.getIdProperty().isComposite()) {
104+
condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(entityMetaData.getIdProperty(), container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID));
105+
} else {
106+
condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
107+
}
108+
98109
QueryFragments queryFragments = new QueryFragments();
99-
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
110+
queryFragments.addMatchOn(container);
100111
queryFragments.setCondition(condition);
101112
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData));
102113
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);
@@ -105,9 +116,21 @@ public static QueryFragmentsAndParameters forFindById(Neo4jPersistentEntity<?> e
105116
public static QueryFragmentsAndParameters forFindByAllId(Neo4jPersistentEntity<?> entityMetaData, Object idValues) {
106117
Map<String, Object> parameters = Collections.singletonMap(Constants.NAME_OF_IDS, idValues);
107118

108-
Condition condition = entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS)));
119+
Node container = cypherGenerator.createRootNode(entityMetaData);
120+
Condition condition;
121+
if (entityMetaData.getIdProperty().isComposite()) {
122+
List<Object> args = new ArrayList<>();
123+
for (String key : entityMetaData.getIdProperty().getOptionalConverter().write(null).keys()) {
124+
args.add(key);
125+
args.add(container.property(key));
126+
}
127+
condition = Cypher.mapOf(args.toArray()).in(parameter(Constants.NAME_OF_IDS));
128+
} else {
129+
condition = entityMetaData.getIdExpression().in(parameter(Constants.NAME_OF_IDS));
130+
}
131+
109132
QueryFragments queryFragments = new QueryFragments();
110-
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
133+
queryFragments.addMatchOn(container);
111134
queryFragments.setCondition(condition);
112135
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData));
113136
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);
@@ -124,9 +147,16 @@ public static QueryFragmentsAndParameters forFindAll(Neo4jPersistentEntity<?> en
124147
public static QueryFragmentsAndParameters forExistsById(Neo4jPersistentEntity<?> entityMetaData, Object idValues) {
125148
Map<String, Object> parameters = Collections.singletonMap(Constants.NAME_OF_ID, idValues);
126149

127-
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
150+
Node container = cypherGenerator.createRootNode(entityMetaData);
151+
Condition condition;
152+
if (entityMetaData.getIdProperty().isComposite()) {
153+
condition = CypherGenerator.INSTANCE.createCompositePropertyCondition(entityMetaData.getIdProperty(), container.getRequiredSymbolicName(), parameter(Constants.NAME_OF_ID));
154+
} else {
155+
condition = entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID));
156+
}
157+
128158
QueryFragments queryFragments = new QueryFragments();
129-
queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData));
159+
queryFragments.addMatchOn(container);
130160
queryFragments.setCondition(condition);
131161
queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForExists(entityMetaData));
132162
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);

0 commit comments

Comments
 (0)