Skip to content

Commit 0738bc0

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. # Conflicts: # src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java # src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java
1 parent 8464d1d commit 0738bc0

File tree

9 files changed

+449
-29
lines changed

9 files changed

+449
-29
lines changed

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,22 @@ public Statement prepareDeleteOf(NodeDescription<?> nodeDescription, @Nullable C
253253
return ongoingUpdate.build();
254254
}
255255

256+
public Condition createCompositePropertyCondition(GraphPropertyDescription idProperty, SymbolicName containerName, Expression actualParameter) {
257+
258+
if (!idProperty.isComposite()) {
259+
return Cypher.property(containerName, idProperty.getPropertyName()).isEqualTo(actualParameter);
260+
}
261+
262+
Neo4jPersistentProperty property = (Neo4jPersistentProperty) idProperty;
263+
264+
Condition result = Conditions.noCondition();
265+
for (String key : property.getOptionalConverter().write(null).keys()) {
266+
Property expression = Cypher.property(containerName, key);
267+
result = result.and(expression.isEqualTo(actualParameter.property(key)));
268+
}
269+
return result;
270+
}
271+
256272
public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
257273
UnaryOperator<OngoingMatchAndUpdate> updateDecorator) {
258274

@@ -265,16 +281,15 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
265281
Parameter<?> idParameter = parameter(Constants.NAME_OF_ID);
266282

267283
if (!idDescription.isInternallyGeneratedId()) {
268-
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
269-
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property"));
284+
GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity<?>) nodeDescription).getRequiredIdProperty();
270285

271286
if (((Neo4jPersistentEntity<?>) nodeDescription).hasVersionProperty()) {
272287
Property versionProperty = rootNode.property(((Neo4jPersistentEntity<?>) nodeDescription).getRequiredVersionProperty().getName());
273288
String nameOfPossibleExistingNode = "hlp";
274289
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
275290

276291
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
277-
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
292+
.where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter))
278293
.with(possibleExistingNode)
279294
.where(possibleExistingNode.isNull())
280295
.create(rootNode.withProperties(versionProperty, literalOf(0)))
@@ -283,7 +298,7 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
283298
.build();
284299

285300
Statement updateIfExists = updateDecorator.apply(match(rootNode)
286-
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
301+
.where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter))
287302
.and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check
288303
.set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock
289304
.with(rootNode)
@@ -299,7 +314,7 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
299314
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
300315

301316
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
302-
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
317+
.where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter))
303318
.with(possibleExistingNode)
304319
.where(possibleExistingNode.isNull())
305320
.create(rootNode)
@@ -308,7 +323,7 @@ public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
308323
.build();
309324

310325
Statement updateIfExists = updateDecorator.apply(match(rootNode)
311-
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
326+
.where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter))
312327
.with(rootNode)
313328
.mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
314329
.returning(rootNode)

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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,19 @@ private Condition createImpl(Part part, Iterator<Object> actualParameters) {
324324
Neo4jPersistentProperty property = path.getRequiredLeafProperty();
325325

326326
boolean ignoreCase = ignoreCase(part);
327+
328+
if (property.isComposite()) {
329+
330+
Condition compositePropertyCondition = CypherGenerator.INSTANCE.createCompositePropertyCondition(
331+
property,
332+
Cypher.name(getContainerName(path, (Neo4jPersistentEntity<?>) property.getOwner())),
333+
toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
334+
if (part.getType() == Part.Type.NEGATING_SIMPLE_PROPERTY) {
335+
compositePropertyCondition = Conditions.not(compositePropertyCondition);
336+
}
337+
return compositePropertyCondition;
338+
}
339+
327340
return switch (part.getType()) {
328341
case AFTER, GREATER_THAN -> toCypherProperty(path, ignoreCase)
329342
.gt(toCypherParameter(nextRequiredParameter(actualParameters, property), ignoreCase));
@@ -517,25 +530,15 @@ private Expression toCypherProperty(PersistentPropertyPath<Neo4jPersistentProper
517530
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) leafProperty.getOwner();
518531
Expression expression;
519532

533+
String containerName = getContainerName(path, owner);
520534
if (owner.equals(this.nodeDescription) && path.getLength() == 1) {
521535
expression = leafProperty.isInternalIdProperty() ?
522536
Cypher.call("id").withArgs(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription)).asFunction() :
523-
Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), leafProperty.getPropertyName());
537+
Cypher.property(containerName, leafProperty.getPropertyName());
538+
} else if (leafProperty.isInternalIdProperty()) {
539+
expression = Cypher.call("id").withArgs(Cypher.name(containerName)).asFunction();
524540
} else {
525-
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
526-
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
527-
String cypherElementName;
528-
// this "entity" is a representation of a relationship with properties
529-
if (owner.isRelationshipPropertiesEntity()) {
530-
cypherElementName = propertyPathWrapper.getRelationshipName();
531-
} else {
532-
cypherElementName = propertyPathWrapper.getNodeName();
533-
}
534-
if (leafProperty.isInternalIdProperty()) {
535-
expression = Cypher.call("id").withArgs(Cypher.name(cypherElementName)).asFunction();
536-
} else {
537-
expression = Cypher.property(cypherElementName, leafProperty.getPropertyName());
538-
}
541+
expression = Cypher.property(containerName, leafProperty.getPropertyName());
539542
}
540543

541544
if (addToLower) {
@@ -545,6 +548,24 @@ private Expression toCypherProperty(PersistentPropertyPath<Neo4jPersistentProper
545548
return expression;
546549
}
547550

551+
private String getContainerName(PersistentPropertyPath<Neo4jPersistentProperty> path, Neo4jPersistentEntity<?> owner) {
552+
553+
if (owner.equals(this.nodeDescription) && path.getLength() == 1) {
554+
return Constants.NAME_OF_TYPED_ROOT_NODE.apply(this.nodeDescription).getValue();
555+
}
556+
557+
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
558+
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
559+
String cypherElementName;
560+
// this "entity" is a representation of a relationship with properties
561+
if (owner.isRelationshipPropertiesEntity()) {
562+
cypherElementName = propertyPathWrapper.getRelationshipName();
563+
} else {
564+
cypherElementName = propertyPathWrapper.getNodeName();
565+
}
566+
return cypherElementName;
567+
}
568+
548569
private Expression toCypherParameter(Parameter parameter, boolean addToLower) {
549570

550571
return createCypherParameter(parameter.nameOrIndex, addToLower);

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

Lines changed: 6 additions & 2 deletions
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

@@ -80,7 +82,9 @@ void validatePart(Part part) {
8082
case NEAR, WITHIN -> validatePointProperty(part);
8183
}
8284

83-
validateNotACompositeProperty(part);
85+
if (!TYPES_SUPPORTED_FOR_COMPOSITES.contains(part.getType())) {
86+
validateNotACompositeProperty(part);
87+
}
8488
}
8589

8690
private void validateNotACompositeProperty(Part part) {
@@ -129,7 +133,7 @@ private static String formatTypes(Collection<Part.Type> types) {
129133
* Checks whether the given part can be queried without case sensitivity.
130134
*
131135
* @param part query part to check if ignoring case sensitivity is possible
132-
* @return True when {@code part} can be queried case insensitive.
136+
* @return True when {@code part} can be queried case-insensitive.
133137
*/
134138
static boolean canIgnoreCase(Part part) {
135139
return part.getProperty().getLeafType() == String.class

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

Lines changed: 36 additions & 6 deletions
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)