Skip to content

Commit 934f6f5

Browse files
Treat @TargetNode as association to the outside world. (#2489)
This improves the interoperability with Spring Data Rest by a magnitude as no resource processing needs to be done on `@RelationshipProperties` to have embedded links for their target node. Two support classes for both associations and properties have been added as internal API to chim our philosophy from that change.
1 parent 124d27e commit 934f6f5

16 files changed

+156
-29
lines changed

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@
6060
import org.springframework.dao.IncorrectResultSizeDataAccessException;
6161
import org.springframework.dao.OptimisticLockingFailureException;
6262
import org.springframework.data.mapping.Association;
63-
import org.springframework.data.mapping.AssociationHandler;
6463
import org.springframework.data.mapping.PersistentPropertyAccessor;
6564
import org.springframework.data.mapping.PropertyPath;
6665
import org.springframework.data.mapping.callback.EntityCallbacks;
6766
import org.springframework.data.neo4j.core.TemplateSupport.NodesAndRelationshipsByIdStatementProvider;
67+
import org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport;
6868
import org.springframework.data.neo4j.core.mapping.Constants;
6969
import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder;
7070
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
@@ -713,7 +713,7 @@ private <T> T processNestedRelations(
713713

714714
Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
715715

716-
sourceEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
716+
AssociationHandlerSupport.of(sourceEntity).doWithAssociations(association -> {
717717

718718
// create context to bundle parameters
719719
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor, sourceEntity);

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.data.mapping.Association;
2323
import org.springframework.data.neo4j.core.TemplateSupport.FilteredBinderFunction;
24+
import org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport;
2425
import org.springframework.data.neo4j.core.schema.TargetNode;
2526
import reactor.core.publisher.Flux;
2627
import reactor.core.publisher.Mono;
@@ -65,7 +66,6 @@
6566
import org.springframework.core.log.LogAccessor;
6667
import org.springframework.dao.IncorrectResultSizeDataAccessException;
6768
import org.springframework.dao.OptimisticLockingFailureException;
68-
import org.springframework.data.mapping.AssociationHandler;
6969
import org.springframework.data.mapping.PersistentPropertyAccessor;
7070
import org.springframework.data.mapping.PropertyPath;
7171
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
@@ -843,7 +843,7 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
843843
List<Mono<Void>> relationshipDeleteMonos = new ArrayList<>();
844844
List<Flux<RelationshipHandler>> relationshipCreationCreations = new ArrayList<>();
845845

846-
sourceEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
846+
AssociationHandlerSupport.of(sourceEntity).doWithAssociations(association -> {
847847

848848
// create context to bundle parameters
849849
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, parentPropertyAccessor, sourceEntity);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.mapping;
17+
18+
import java.util.Map;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
21+
import org.apiguardian.api.API;
22+
import org.springframework.data.mapping.AssociationHandler;
23+
import org.springframework.data.neo4j.core.schema.TargetNode;
24+
25+
/**
26+
* <strong>Warning</strong> Internal API, might change without further notice, even in patch releases.
27+
* <p>
28+
* This class removes {@link TargetNode @TargetNode} properties again from associations.
29+
*
30+
* @author Michael J. Simons
31+
* @since 6.3
32+
*/
33+
@API(status = API.Status.INTERNAL, since = "6.3")
34+
public final class AssociationHandlerSupport {
35+
36+
private final static Map<Neo4jPersistentEntity<?>, AssociationHandlerSupport> CACHE = new ConcurrentHashMap<>();
37+
38+
public static AssociationHandlerSupport of(Neo4jPersistentEntity<?> entity) {
39+
return CACHE.computeIfAbsent(entity, AssociationHandlerSupport::new);
40+
}
41+
42+
private final Neo4jPersistentEntity<?> entity;
43+
44+
private AssociationHandlerSupport(Neo4jPersistentEntity<?> entity) {
45+
this.entity = entity;
46+
}
47+
48+
public Neo4jPersistentEntity<?> doWithAssociations(AssociationHandler<Neo4jPersistentProperty> handler) {
49+
entity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
50+
if (!association.getInverse().isAnnotationPresent(TargetNode.class)) {
51+
handler.doWithAssociation(association);
52+
}
53+
});
54+
return entity;
55+
}
56+
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public void write(Object source, Map<String, Object> parameters) {
189189
.getNodeDescription(source.getClass());
190190

191191
PersistentPropertyAccessor<Object> propertyAccessor = nodeDescription.getPropertyAccessor(source);
192-
nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> {
192+
PropertyHandlerSupport.of(nodeDescription).doWithProperties((Neo4jPersistentProperty p) -> {
193193

194194
// Skip the internal properties, we don't want them to end up stored as properties
195195
if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty() || p.isReadOnly()) {
@@ -339,14 +339,14 @@ private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEnt
339339
// Fill simple properties
340340
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
341341
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType);
342-
concreteNodeDescription.doWithProperties(handler);
342+
PropertyHandlerSupport.of(concreteNodeDescription).doWithProperties(handler);
343343
}
344344
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
345345
// reference the start again. Because it is getting still constructed, it won't be in the knownObjects
346346
// store unless we temporarily put it there.
347347
knownObjects.storeObject(internalId, mappedObject);
348348

349-
concreteNodeDescription.doWithAssociations(
349+
AssociationHandlerSupport.of(concreteNodeDescription).doWithAssociations(
350350
populateFrom(queryResult, nodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult));
351351
}
352352

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.springframework.core.annotation.AnnotatedElementUtils;
3535
import org.springframework.data.annotation.Persistent;
3636
import org.springframework.data.mapping.Association;
37-
import org.springframework.data.mapping.PropertyHandler;
3837
import org.springframework.data.mapping.model.BasicPersistentEntity;
3938
import org.springframework.data.neo4j.core.schema.DynamicLabels;
4039
import org.springframework.data.neo4j.core.schema.GeneratedValue;
@@ -222,7 +221,10 @@ private void verifyNoDuplicatedGraphProperties() {
222221

223222
Set<String> seen = new HashSet<>();
224223
Set<String> duplicates = new HashSet<>();
225-
this.doWithProperties((PropertyHandler<Neo4jPersistentProperty>) persistentProperty -> {
224+
PropertyHandlerSupport.of(this).doWithProperties(persistentProperty -> {
225+
if (persistentProperty.isEntity()) {
226+
return;
227+
}
226228
String propertyName = persistentProperty.getPropertyName();
227229
if (seen.contains(propertyName)) {
228230
duplicates.add(propertyName);
@@ -238,7 +240,7 @@ private void verifyNoDuplicatedGraphProperties() {
238240
private void verifyDynamicAssociations() {
239241

240242
Set<Class> targetEntities = new HashSet<>();
241-
this.doWithAssociations((Association<Neo4jPersistentProperty> association) -> {
243+
AssociationHandlerSupport.of(this).doWithAssociations((Association<Neo4jPersistentProperty> association) -> {
242244
Neo4jPersistentProperty inverse = association.getInverse();
243245
if (inverse.isDynamicAssociation()) {
244246
Relationship relationship = inverse.findAnnotation(Relationship.class);
@@ -272,7 +274,7 @@ private void verifyDynamicLabels() {
272274

273275
Set<String> namesOfPropertiesWithDynamicLabels = new HashSet<>();
274276

275-
this.doWithProperties((PropertyHandler<Neo4jPersistentProperty>) persistentProperty -> {
277+
PropertyHandlerSupport.of(this).doWithProperties(persistentProperty -> {
276278
if (!persistentProperty.isAnnotationPresent(DynamicLabels.class)) {
277279
return;
278280
}
@@ -449,7 +451,7 @@ private IdDescription computeIdDescription() {
449451
public Collection<RelationshipDescription> getRelationships() {
450452

451453
final List<RelationshipDescription> relationships = new ArrayList<>();
452-
this.doWithAssociations(
454+
AssociationHandlerSupport.of(this).doWithAssociations(
453455
(Association<Neo4jPersistentProperty> association) -> relationships.add((RelationshipDescription) association));
454456
return Collections.unmodifiableCollection(relationships);
455457
}
@@ -489,7 +491,7 @@ private Collection<GraphPropertyDescription> computeGraphProperties() {
489491

490492
final List<GraphPropertyDescription> computedGraphProperties = new ArrayList<>();
491493

492-
doWithProperties((PropertyHandler<Neo4jPersistentProperty>) computedGraphProperties::add);
494+
PropertyHandlerSupport.of(this).doWithProperties(computedGraphProperties::add);
493495

494496
return Collections.unmodifiableCollection(computedGraphProperties);
495497
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
9090
if (isAnnotationPresent(Relationship.class)) {
9191
return true;
9292
}
93-
return !(isWritableProperty.get() || isAnnotationPresent(TargetNode.class));
93+
return !(isWritableProperty.get());
9494
});
9595

9696
this.customConversion = Lazy.of(() -> {
@@ -229,7 +229,7 @@ public Neo4jPersistentPropertyConverter<?> getOptionalConverter() {
229229
@Nullable
230230
private String computeGraphPropertyName() {
231231

232-
if (this.isAssociation()) {
232+
if (this.isRelationship()) {
233233
return null;
234234
}
235235

@@ -270,7 +270,7 @@ public boolean isInternalIdProperty() {
270270
@Override
271271
public boolean isRelationship() {
272272

273-
return isAssociation();
273+
return isAssociation() && !isAnnotationPresent(TargetNode.class);
274274
}
275275

276276
@Override

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.springframework.data.mapping.PersistentEntity;
3434
import org.springframework.data.mapping.PersistentProperty;
3535
import org.springframework.data.mapping.PersistentPropertyAccessor;
36-
import org.springframework.data.mapping.SimplePropertyHandler;
3736
import org.springframework.data.mapping.model.ParameterValueProvider;
3837
import org.springframework.data.util.ClassTypeInformation;
3938
import org.springframework.lang.Nullable;
@@ -86,7 +85,7 @@ public Object convertDirectly(Object entityInstance) {
8685
);
8786

8887
PersistentPropertyAccessor<Object> dtoAccessor = targetEntity.getPropertyAccessor(dto);
89-
targetEntity.doWithProperties((SimplePropertyHandler) property -> {
88+
PropertyHandlerSupport.of(targetEntity).doWithProperties(property -> {
9089

9190
if (creator != null && creator.isCreatorParameter(property)) {
9291
return;

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.mapping.PersistentProperty;
3030
import org.springframework.data.mapping.PersistentPropertyAccessor;
3131
import org.springframework.data.mapping.model.ParameterValueProvider;
32+
import org.springframework.data.neo4j.core.schema.TargetNode;
3233
import org.springframework.data.util.ClassTypeInformation;
3334
import org.springframework.data.util.ReflectionUtils;
3435
import org.springframework.lang.Nullable;
@@ -118,7 +119,7 @@ Object getPropertyValueFor(PersistentProperty<?> targetProperty, PersistentEntit
118119
return ReflectionUtils.getPrimitiveDefault(targetPropertyType);
119120
}
120121

121-
if (targetProperty.isAssociation() && targetProperty.isCollectionLike()) {
122+
if (targetProperty.isAssociation() && !targetProperty.isAnnotationPresent(TargetNode.class) && targetProperty.isCollectionLike()) {
122123
EntityFromDtoInstantiatingConverter<?> nestedConverter = converterCache.computeIfAbsent(targetProperty.getComponentType(), t -> new EntityFromDtoInstantiatingConverter<>(t, context));
123124
Collection<?> source = (Collection<?>) propertyValue;
124125
if (source == null) {

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

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
* Neo4j. Both Spring Data methods {@link #doWithProperties(PropertyHandler)} and
2828
* {@link #doWithAssociations(AssociationHandler)} are aware which field of a class is meant to be mapped as a property
2929
* of a node or a relationship or if it is a relationship (in Spring Data terms: if it is an association).
30+
* <p>
31+
* <strong>Note</strong> to the outside world, we treat the {@link org.springframework.data.neo4j.core.schema.TargetNode @TargetNode}
32+
* annotated field of a {@link org.springframework.data.neo4j.core.schema.RelationshipProperties @RelationshipProperties} annotated
33+
* class as association. Internally, we treat it as a property
3034
*
3135
* @author Michael J. Simons
3236
* @param <T> type of the underlying class

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public interface Neo4jPersistentProperty extends PersistentProperty<Neo4jPersist
4444
*/
4545
default boolean isDynamicAssociation() {
4646

47-
return isAssociation() && isMap() && (getComponentType() == String.class || getComponentType().isEnum());
47+
return isRelationship() && isMap() && (getComponentType() == String.class || getComponentType().isEnum());
4848
}
4949

5050
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2011-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.mapping;
17+
18+
import java.util.Map;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
21+
import org.apiguardian.api.API;
22+
import org.springframework.data.mapping.AssociationHandler;
23+
import org.springframework.data.mapping.PropertyHandler;
24+
import org.springframework.data.neo4j.core.schema.TargetNode;
25+
26+
/**
27+
* <strong>Warning</strong> Internal API, might change without further notice, even in patch releases.
28+
* <p>
29+
* This class adds {@link TargetNode @TargetNode} properties back to properties.
30+
*
31+
* @author Michael J. Simons
32+
* @since 6.3
33+
*/
34+
@API(status = API.Status.INTERNAL, since = "6.3")
35+
public final class PropertyHandlerSupport {
36+
37+
private final static Map<Neo4jPersistentEntity<?>, PropertyHandlerSupport> CACHE = new ConcurrentHashMap<>();
38+
39+
public static PropertyHandlerSupport of(Neo4jPersistentEntity<?> entity) {
40+
return CACHE.computeIfAbsent(entity, PropertyHandlerSupport::new);
41+
}
42+
43+
private final Neo4jPersistentEntity<?> entity;
44+
45+
private PropertyHandlerSupport(Neo4jPersistentEntity<?> entity) {
46+
this.entity = entity;
47+
}
48+
49+
public Neo4jPersistentEntity<?> doWithProperties(PropertyHandler<Neo4jPersistentProperty> handler) {
50+
entity.doWithProperties(handler);
51+
entity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
52+
if (association.getInverse().isAnnotationPresent(TargetNode.class)) {
53+
handler.doWithPersistentProperty(association.getInverse());
54+
}
55+
});
56+
return entity;
57+
}
58+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apiguardian.api.API;
2626
import org.springframework.data.mapping.Association;
2727
import org.springframework.data.mapping.PropertyPath;
28+
import org.springframework.data.neo4j.core.schema.TargetNode;
2829
import org.springframework.lang.Nullable;
2930

3031
/**
@@ -79,7 +80,7 @@ private void traverseImpl(
7980
}
8081

8182
sink.accept(path, p);
82-
if (p.isAssociation() && !pathAlreadyVisited) {
83+
if (p.isAssociation() && !(pathAlreadyVisited || p.isAnnotationPresent(TargetNode.class))) {
8384
Class<?> associationTargetType = p.getAssociationTargetType();
8485
if (associationTargetType == null) {
8586
return;

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

+4
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ private ExposesRelationships<?> createRelationshipChain(ExposesRelationships<?>
180180
int cnt = 0;
181181
for (PersistentProperty<?> persistentProperty : propertyPath) {
182182

183+
if (persistentProperty.isAssociation() && persistentProperty.isAnnotationPresent(TargetNode.class)) {
184+
break;
185+
}
186+
183187
RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty.getAssociation();
184188

185189
if (relationshipDescription == null) {

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,18 @@
3434
import org.neo4j.driver.Value;
3535
import org.neo4j.driver.Values;
3636
import org.springframework.data.convert.EntityWriter;
37-
import org.springframework.data.mapping.AssociationHandler;
3837
import org.springframework.data.mapping.MappingException;
3938
import org.springframework.data.mapping.PersistentPropertyAccessor;
40-
import org.springframework.data.mapping.PropertyHandler;
4139
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
40+
import org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport;
4241
import org.springframework.data.neo4j.core.mapping.Constants;
4342
import org.springframework.data.neo4j.core.mapping.MappingSupport.RelationshipPropertiesWithEntityHolder;
4443
import org.springframework.data.neo4j.core.mapping.Neo4jEntityConverter;
4544
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
4645
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
4746
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
4847
import org.springframework.data.neo4j.core.mapping.NestedRelationshipContext;
48+
import org.springframework.data.neo4j.core.mapping.PropertyHandlerSupport;
4949
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
5050
import org.springframework.data.neo4j.core.schema.TargetNode;
5151
import org.springframework.data.util.TypeInformation;
@@ -121,7 +121,7 @@ Map<String, Object> writeImpl(@Nullable Object source, Map<String, Object> sink,
121121
if (initialObject && entity.isRelationshipPropertiesEntity()) {
122122
@SuppressWarnings("unchecked")
123123
Map<String, Object> propertyMap = (Map<String, Object>) sink.get(Constants.NAME_OF_PROPERTIES_PARAM);
124-
entity.doWithProperties((PropertyHandler<Neo4jPersistentProperty>) p -> {
124+
PropertyHandlerSupport.of(entity).doWithProperties(p -> {
125125
if (p.isAnnotationPresent(TargetNode.class)) {
126126
Map<String, Object> target = this.writeImpl(propertyAccessor.getProperty(p), new HashMap<>(), seenObjects, false);
127127
propertyMap.put("__target__", Values.value(target));
@@ -149,7 +149,7 @@ private void addRelations(Map<String, Object> sink, Neo4jPersistentEntity<?> ent
149149

150150
@SuppressWarnings("unchecked")
151151
Map<String, Object> propertyMap = (Map<String, Object>) sink.get(Constants.NAME_OF_PROPERTIES_PARAM);
152-
entity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
152+
AssociationHandlerSupport.of(entity).doWithAssociations(association -> {
153153

154154
NestedRelationshipContext context = NestedRelationshipContext.of(association, propertyAccessor, entity);
155155
RelationshipDescription description = (RelationshipDescription) association;

0 commit comments

Comments
 (0)