Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5ad977c

Browse files
Add aggregate boundaries for simpified projection (#3023)
Introduce a parameter to the `@Node` annotation (`aggregateBoundary`) which allows users to express that an entity should only report its `@Id` if it gets loaded from a certain different class/entity. In all other cases it will behave as a usual entity and SDN will hydrate all properties and follow all relationships. This works for reading and writing entities. Signed-off-by: Gerrit Meier <meistermeier@gmail.com>
1 parent a53a27b commit 5ad977c

File tree

14 files changed

+1354
-29
lines changed

14 files changed

+1354
-29
lines changed

‎src/main/antora/modules/ROOT/pages/projections/sdn-projections.adoc‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ additional properties - via accessors or fields - Spring Data Neo4j looks in the
2828
Properties must match exactly by name and can be of simple types (as defined in `org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes`)
2929
or of known persistent entities. Collections of those are supported, but maps are not.
3030

31+
There is also an additional mechanism built into Spring Data Neo4j which allows defining loading and persisting boundaries on the entity definition level.
32+
Read more about this in the <<projections.sdn.aggregate-boundaries>> section.
33+
3134
[[projections.sdn.multi-level]]
3235
== Multi-level projections
3336

@@ -212,3 +215,13 @@ The key to a dynamic projection is to specify the desired projection type as the
212215
in a repository like this: `<T> Collection<T> findByName(String name, Class<T> type)`. This is a declaration that
213216
could be added to the `TestRepository` above and allow for different projections retrieved by the same method, without
214217
to repeat a possible `@Query` annotation on several methods.
218+
219+
[[projections.sdn.aggregate-boundaries]]
220+
== Aggregate boundaries
221+
222+
Reflecting multiple levels of relationships by introducing multiple projections can be cumbersome.
223+
To simplify this already on the entity level, it's possible to add an additional parameter `aggregateBoundary` and supply 1..n classes.
224+
With this the parameterized entity will only report its `@Id` field back and SDN won't follow its relationships or fetch other properties.
225+
226+
It's still possible to use interface-based projections for those entities.
227+
Those projections can be even broader as the declared aggregate boundaries and e.g. include properties or relationships.

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ public <T> List<T> findAll(Class<T> domainType) {
254254
private <T> List<T> doFindAll(Class<T> domainType, @Nullable Class<?> resultType) {
255255
return executeReadOnly(tx -> {
256256
Neo4jPersistentEntity<?> entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType);
257-
return createExecutableQuery(domainType, resultType,QueryFragmentsAndParameters.forFindAll(entityMetaData),
258-
true)
257+
return createExecutableQuery(domainType, resultType,
258+
QueryFragmentsAndParameters.forFindAll(entityMetaData, this.neo4jMappingContext), true)
259259
.getResults();
260260
});
261261
}
@@ -367,8 +367,10 @@ public <T> Optional<T> findById(Object id, Class<T> domainType) {
367367
Neo4jPersistentEntity<?> entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType);
368368

369369
return createExecutableQuery(domainType, null,
370-
QueryFragmentsAndParameters.forFindById(entityMetaData, TemplateSupport
371-
.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)),
370+
QueryFragmentsAndParameters.forFindById(entityMetaData,
371+
TemplateSupport.convertIdValues(this.neo4jMappingContext,
372+
entityMetaData.getRequiredIdProperty(), id),
373+
this.neo4jMappingContext),
372374
true)
373375
.getSingleResult();
374376
});
@@ -380,17 +382,20 @@ public <T> List<T> findAllById(Iterable<?> ids, Class<T> domainType) {
380382
Neo4jPersistentEntity<?> entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainType);
381383

382384
return createExecutableQuery(domainType, null,
383-
QueryFragmentsAndParameters.forFindByAllId(entityMetaData, TemplateSupport
384-
.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)),
385+
QueryFragmentsAndParameters.forFindByAllId(entityMetaData,
386+
TemplateSupport.convertIdValues(this.neo4jMappingContext,
387+
entityMetaData.getRequiredIdProperty(), ids),
388+
this.neo4jMappingContext),
385389
true)
386390
.getResults();
387391
});
388392
}
389393

390394
@Override
391395
public <T> T save(T instance) {
392-
393-
return execute(tx -> saveImpl(instance, Collections.emptySet(), null));
396+
Collection<PropertyFilter.ProjectedPath> pps = PropertyFilterSupport
397+
.getInputPropertiesForAggregateBoundary(instance.getClass(), this.neo4jMappingContext);
398+
return execute(tx -> saveImpl(instance, pps, null));
394399

395400
}
396401

@@ -550,9 +555,12 @@ private <T> List<T> saveAllImpl(Iterable<T> instances,
550555

551556
Set<Class<?>> types = new HashSet<>();
552557
List<T> entities = new ArrayList<>();
558+
Map<Class<?>, Collection<PropertyFilter.ProjectedPath>> includedPropertiesByClass = new HashMap<>();
553559
instances.forEach(instance -> {
554560
entities.add(instance);
555561
types.add(instance.getClass());
562+
includedPropertiesByClass.put(instance.getClass(), PropertyFilterSupport
563+
.getInputPropertiesForAggregateBoundary(instance.getClass(), this.neo4jMappingContext));
556564
});
557565

558566
if (entities.isEmpty()) {
@@ -573,7 +581,12 @@ private <T> List<T> saveAllImpl(Iterable<T> instances,
573581

574582
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(
575583
this.neo4jMappingContext);
576-
return entities.stream().map(e -> saveImpl(e, pps, stateMachine)).collect(Collectors.toList());
584+
return entities.stream()
585+
.map(e -> saveImpl(e,
586+
((includedProperties != null && !includedProperties.isEmpty()) || includeProperty != null) ? pps
587+
: includedPropertiesByClass.get(e.getClass()),
588+
stateMachine))
589+
.collect(Collectors.toList());
577590
}
578591

579592
class Tuple3<T> {
@@ -628,7 +641,10 @@ class Tuple3<T> {
628641
String internalId = Objects.requireNonNull(idToInternalIdMapping.get(id));
629642
stateMachine.registerInitialObject(t.originalInstance, internalId);
630643
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, stateMachine,
631-
TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
644+
TemplateSupport.computeIncludePropertyPredicate(
645+
((includedProperties != null && !includedProperties.isEmpty()) || includeProperty != null)
646+
? pps : includedPropertiesByClass.get(t.modifiedInstance.getClass()),
647+
entityMetaData));
632648
}).collect(Collectors.toList());
633649
}
634650

@@ -1182,9 +1198,11 @@ private Optional<Object> getRelationshipId(Statement statement, @Nullable Neo4jP
11821198
private Entity loadRelatedNode(NodeDescription<?> targetNodeDescription, @Nullable Object relatedInternalId) {
11831199

11841200
var targetPersistentEntity = (Neo4jPersistentEntity<?>) targetNodeDescription;
1185-
var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity,
1186-
TemplateSupport.convertIdValues(this.neo4jMappingContext,
1187-
targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
1201+
var queryFragmentsAndParameters = QueryFragmentsAndParameters
1202+
.forFindById(targetPersistentEntity,
1203+
TemplateSupport.convertIdValues(this.neo4jMappingContext,
1204+
targetPersistentEntity.getRequiredIdProperty(), relatedInternalId),
1205+
this.neo4jMappingContext);
11881206
var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue();
11891207

11901208
return this.neo4jClient

‎src/main/java/org/springframework/data/neo4j/core/PropertyFilterSupport.java‎

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
package org.springframework.data.neo4j.core;
1717

1818
import java.beans.PropertyDescriptor;
19+
import java.util.ArrayList;
1920
import java.util.Collection;
2021
import java.util.Collections;
2122
import java.util.HashSet;
2223
import java.util.Objects;
2324
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.concurrent.locks.ReentrantLock;
27+
import java.util.function.Predicate;
2428

2529
import org.apiguardian.api.API;
2630

@@ -29,6 +33,7 @@
2933
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
3034
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
3135
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
36+
import org.springframework.data.neo4j.core.mapping.NodeDescription;
3237
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
3338
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
3439
import org.springframework.data.projection.ProjectionFactory;
@@ -47,6 +52,9 @@
4752
@API(status = API.Status.INTERNAL, since = "6.1.3")
4853
public final class PropertyFilterSupport {
4954

55+
// A cache to look up if there are aggregate boundaries between two entities.
56+
private static final AggregateBoundaries AGGREGATE_BOUNDARIES = new AggregateBoundaries();
57+
5058
private PropertyFilterSupport() {
5159
}
5260

@@ -61,6 +69,11 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
6169

6270
boolean isProjecting = returnedType.isProjecting();
6371
boolean isClosedProjection = factory.getProjectionInformation(potentiallyProjectedType).isClosed();
72+
if (!isProjecting && containsAggregateBoundary(domainType, mappingContext)) {
73+
Collection<PropertyFilter.ProjectedPath> listForAggregate = createListForAggregate(domainType,
74+
mappingContext);
75+
return listForAggregate;
76+
}
6477

6578
if (!isProjecting || !isClosedProjection) {
6679
return Collections.emptySet();
@@ -84,6 +97,160 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
8497
return filteredProperties;
8598
}
8699

100+
public static Collection<PropertyFilter.ProjectedPath> getInputPropertiesForAggregateBoundary(Class<?> domainType,
101+
Neo4jMappingContext mappingContext) {
102+
if (!containsAggregateBoundary(domainType, mappingContext)) {
103+
return Collections.emptySet();
104+
}
105+
Collection<PropertyFilter.ProjectedPath> listForAggregate = createListForAggregate(domainType, mappingContext);
106+
return listForAggregate;
107+
}
108+
109+
public static Predicate<PropertyFilter.RelaxedPropertyPath> createRelaxedPropertyPathFilter(Class<?> domainType,
110+
Neo4jMappingContext mappingContext) {
111+
if (!containsAggregateBoundary(domainType, mappingContext)) {
112+
return PropertyFilter.NO_FILTER;
113+
}
114+
Collection<PropertyFilter.RelaxedPropertyPath> relaxedPropertyPathFilter = createRelaxedPropertyPathFilter(
115+
domainType, mappingContext, new HashSet<RelationshipDescription>());
116+
return (rpp) -> {
117+
return relaxedPropertyPathFilter.contains(rpp);
118+
};
119+
}
120+
121+
private static Collection<PropertyFilter.RelaxedPropertyPath> createRelaxedPropertyPathFilter(Class<?> domainType,
122+
Neo4jMappingContext neo4jMappingContext, Set<RelationshipDescription> processedRelationships) {
123+
var relaxedPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(domainType);
124+
var relaxedPropertyPaths = new ArrayList<PropertyFilter.RelaxedPropertyPath>();
125+
relaxedPropertyPaths.add(relaxedPropertyPath);
126+
Neo4jPersistentEntity<?> domainEntity = neo4jMappingContext.getRequiredPersistentEntity(domainType);
127+
domainEntity.getGraphProperties().stream().forEach(property -> {
128+
relaxedPropertyPaths.add(relaxedPropertyPath.append(property.getFieldName()));
129+
});
130+
for (RelationshipDescription relationshipDescription : domainEntity.getRelationshipsInHierarchy(any -> true)) {
131+
var target = relationshipDescription.getTarget();
132+
PropertyFilter.RelaxedPropertyPath relationshipPath = relaxedPropertyPath
133+
.append(relationshipDescription.getFieldName());
134+
relaxedPropertyPaths.add(relationshipPath);
135+
processedRelationships.add(relationshipDescription);
136+
createRelaxedPropertyPathFilter(domainType, target, relationshipPath, relaxedPropertyPaths,
137+
processedRelationships);
138+
}
139+
return relaxedPropertyPaths;
140+
}
141+
142+
private static Collection<PropertyFilter.RelaxedPropertyPath> createRelaxedPropertyPathFilter(Class<?> domainType,
143+
NodeDescription<?> nodeDescription, PropertyFilter.RelaxedPropertyPath relaxedPropertyPath,
144+
Collection<PropertyFilter.RelaxedPropertyPath> relaxedPropertyPaths,
145+
Set<RelationshipDescription> processedRelationships) {
146+
// always add the related entity itself
147+
relaxedPropertyPaths.add(relaxedPropertyPath);
148+
if (nodeDescription.hasAggregateBoundaries(domainType)) {
149+
relaxedPropertyPaths.add(relaxedPropertyPath
150+
.append(((Neo4jPersistentEntity<?>) nodeDescription).getRequiredIdProperty().getFieldName()));
151+
152+
return relaxedPropertyPaths;
153+
}
154+
nodeDescription.getGraphProperties().stream().forEach(property -> {
155+
relaxedPropertyPaths.add(relaxedPropertyPath.append(property.getFieldName()));
156+
});
157+
for (RelationshipDescription relationshipDescription : nodeDescription
158+
.getRelationshipsInHierarchy(any -> true)) {
159+
if (processedRelationships.contains(relationshipDescription)) {
160+
continue;
161+
}
162+
var target = relationshipDescription.getTarget();
163+
PropertyFilter.RelaxedPropertyPath relationshipPath = relaxedPropertyPath
164+
.append(relationshipDescription.getFieldName());
165+
relaxedPropertyPaths.add(relationshipPath);
166+
processedRelationships.add(relationshipDescription);
167+
createRelaxedPropertyPathFilter(domainType, target, relationshipPath, relaxedPropertyPaths,
168+
processedRelationships);
169+
}
170+
return relaxedPropertyPaths;
171+
}
172+
173+
private static Collection<PropertyFilter.ProjectedPath> createListForAggregate(Class<?> domainType,
174+
Neo4jMappingContext neo4jMappingContext) {
175+
var relaxedPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(domainType);
176+
var filteredProperties = new ArrayList<PropertyFilter.ProjectedPath>();
177+
Neo4jPersistentEntity<?> domainEntity = neo4jMappingContext.getRequiredPersistentEntity(domainType);
178+
domainEntity.getGraphProperties().stream().forEach(property -> {
179+
filteredProperties
180+
.add(new PropertyFilter.ProjectedPath(relaxedPropertyPath.append(property.getFieldName()), false));
181+
});
182+
for (RelationshipDescription relationshipDescription : domainEntity.getRelationshipsInHierarchy(any -> true)) {
183+
var target = relationshipDescription.getTarget();
184+
filteredProperties.addAll(createListForAggregate(domainType, target,
185+
relaxedPropertyPath.append(relationshipDescription.getFieldName())));
186+
}
187+
return filteredProperties;
188+
}
189+
190+
private static Collection<PropertyFilter.ProjectedPath> createListForAggregate(Class<?> domainType,
191+
NodeDescription<?> nodeDescription, PropertyFilter.RelaxedPropertyPath relaxedPropertyPath) {
192+
var filteredProperties = new ArrayList<PropertyFilter.ProjectedPath>();
193+
// always add the related entity itself
194+
filteredProperties.add(new PropertyFilter.ProjectedPath(relaxedPropertyPath, false));
195+
if (nodeDescription.hasAggregateBoundaries(domainType)) {
196+
filteredProperties.add(new PropertyFilter.ProjectedPath(
197+
relaxedPropertyPath
198+
.append(((Neo4jPersistentEntity<?>) nodeDescription).getRequiredIdProperty().getFieldName()),
199+
false));
200+
return filteredProperties;
201+
}
202+
nodeDescription.getGraphProperties().stream().forEach(property -> {
203+
filteredProperties
204+
.add(new PropertyFilter.ProjectedPath(relaxedPropertyPath.append(property.getFieldName()), false));
205+
});
206+
for (RelationshipDescription relationshipDescription : nodeDescription
207+
.getRelationshipsInHierarchy(any -> true)) {
208+
var target = relationshipDescription.getTarget();
209+
filteredProperties.addAll(createListForAggregate(domainType, target,
210+
relaxedPropertyPath.append(relationshipDescription.getFieldName())));
211+
}
212+
return filteredProperties;
213+
}
214+
215+
private static boolean containsAggregateBoundary(Class<?> domainType, Neo4jMappingContext neo4jMappingContext) {
216+
var processedRelationships = new HashSet<RelationshipDescription>();
217+
Neo4jPersistentEntity<?> domainEntity = neo4jMappingContext.getRequiredPersistentEntity(domainType);
218+
if (AGGREGATE_BOUNDARIES.hasEntry(domainEntity, domainType)) {
219+
return AGGREGATE_BOUNDARIES.getCachedStatus(domainEntity, domainType);
220+
}
221+
for (RelationshipDescription relationshipDescription : domainEntity.getRelationshipsInHierarchy(any -> true)) {
222+
var target = relationshipDescription.getTarget();
223+
if (target.hasAggregateBoundaries(domainType)) {
224+
AGGREGATE_BOUNDARIES.add(domainEntity, domainType, true);
225+
return true;
226+
}
227+
processedRelationships.add(relationshipDescription);
228+
boolean containsAggregateBoundary = containsAggregateBoundary(domainType, target, processedRelationships);
229+
AGGREGATE_BOUNDARIES.add(domainEntity, domainType, containsAggregateBoundary);
230+
return containsAggregateBoundary;
231+
}
232+
AGGREGATE_BOUNDARIES.add(domainEntity, domainType, false);
233+
return false;
234+
}
235+
236+
private static boolean containsAggregateBoundary(Class<?> domainType, NodeDescription<?> nodeDescription,
237+
Set<RelationshipDescription> processedRelationships) {
238+
for (RelationshipDescription relationshipDescription : nodeDescription
239+
.getRelationshipsInHierarchy(any -> true)) {
240+
var target = relationshipDescription.getTarget();
241+
Class<?> underlyingClass = nodeDescription.getUnderlyingClass();
242+
if (processedRelationships.contains(relationshipDescription)) {
243+
continue;
244+
}
245+
if (target.hasAggregateBoundaries(domainType)) {
246+
return true;
247+
}
248+
processedRelationships.add(relationshipDescription);
249+
return containsAggregateBoundary(domainType, target, processedRelationships);
250+
}
251+
return false;
252+
}
253+
87254
static Collection<PropertyFilter.ProjectedPath> addPropertiesFrom(Class<?> domainType, Class<?> returnType,
88255
ProjectionFactory projectionFactory, Neo4jMappingContext neo4jMappingContext) {
89256

@@ -258,4 +425,65 @@ boolean isChildLevel() {
258425

259426
}
260427

428+
record AggregateBoundary(Neo4jPersistentEntity<?> entity, Class<?> domainType, boolean status) {
429+
430+
}
431+
432+
private static final class AggregateBoundaries {
433+
434+
private final Set<AggregateBoundary> aggregateBoundaries = new HashSet<>();
435+
436+
private final ReentrantLock lock = new ReentrantLock();
437+
438+
void add(Neo4jPersistentEntity<?> entity, Class<?> domainType, boolean status) {
439+
try {
440+
this.lock.lock();
441+
for (AggregateBoundary aggregateBoundary : this.aggregateBoundaries) {
442+
if (aggregateBoundary.domainType().equals(domainType) && aggregateBoundary.entity().equals(entity)
443+
&& aggregateBoundary.status() != status) {
444+
throw new IllegalStateException("%s cannot have a different status to %s. Was %s now %s"
445+
.formatted(entity.getUnderlyingClass(), domainType, aggregateBoundary.status(), status));
446+
}
447+
}
448+
this.aggregateBoundaries.add(new AggregateBoundary(entity, domainType, status));
449+
}
450+
finally {
451+
this.lock.unlock();
452+
}
453+
}
454+
455+
boolean hasEntry(Neo4jPersistentEntity<?> entity, Class<?> domainType) {
456+
try {
457+
this.lock.lock();
458+
for (AggregateBoundary aggregateBoundary : this.aggregateBoundaries) {
459+
if (aggregateBoundary.domainType().equals(domainType)
460+
&& aggregateBoundary.entity().equals(entity)) {
461+
return true;
462+
}
463+
}
464+
return false;
465+
}
466+
finally {
467+
this.lock.unlock();
468+
}
469+
}
470+
471+
boolean getCachedStatus(Neo4jPersistentEntity<?> entity, Class<?> domainType) {
472+
try {
473+
this.lock.lock();
474+
for (AggregateBoundary aggregateBoundary : this.aggregateBoundaries) {
475+
if (aggregateBoundary.domainType().equals(domainType)
476+
&& aggregateBoundary.entity().equals(entity)) {
477+
return aggregateBoundary.status();
478+
}
479+
}
480+
return false;
481+
}
482+
finally {
483+
this.lock.unlock();
484+
}
485+
}
486+
487+
}
488+
261489
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /