Skip to content

Commit 1916720

Browse files
michael-simonsmeistermeier
authored andcommitted
GH-2420 - Provide saveAs overloads with a predicate for selecting projected attributes.
Closes #2420.
1 parent 78bb073 commit 1916720

File tree

9 files changed

+561
-12
lines changed

9 files changed

+561
-12
lines changed

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

+38
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
import java.util.List;
1919
import java.util.Map;
2020
import java.util.Optional;
21+
import java.util.function.BiPredicate;
2122

2223
import org.apiguardian.api.API;
2324
import org.neo4j.cypherdsl.core.Statement;
2425
import org.springframework.dao.IncorrectResultSizeDataAccessException;
26+
import org.springframework.data.mapping.PropertyPath;
2527
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
2628
import org.springframework.data.neo4j.repository.NoResultException;
2729
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
@@ -191,6 +193,24 @@ public interface Neo4jOperations {
191193
*/
192194
<T> T save(T instance);
193195

196+
/**
197+
* Saves an instance of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
198+
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
199+
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
200+
* <p>
201+
* Be careful when reusing the returned instance for further persistence operations, as it will most likely not be
202+
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
203+
*
204+
* @param instance the entity to be saved. Must not be {@code null}.
205+
* @param includeProperty A predicate to determine the properties to save.
206+
* @param <T> the type of the entity.
207+
* @return the saved instance.
208+
* @since 6.3
209+
*/
210+
default <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
211+
throw new UnsupportedOperationException();
212+
}
213+
194214
/**
195215
* Saves an instance of an entity, including the properties and relationship defined by the projected {@code resultType}.
196216
*
@@ -213,6 +233,24 @@ default <T, R> R saveAs(T instance, Class<R> resultType) {
213233
*/
214234
<T> List<T> saveAll(Iterable<T> instances);
215235

236+
/**
237+
* Saves several instances of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
238+
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
239+
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
240+
* <p>
241+
* Be careful when reusing the returned instances for further persistence operations, as they will most likely not be
242+
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
243+
*
244+
* @param instances the instances to be saved. Must not be {@code null}.
245+
* @param includeProperty A predicate to determine the properties to save.
246+
* @param <T> the type of the entity.
247+
* @return the saved instances.
248+
* @since 6.3
249+
*/
250+
default <T> List<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
251+
throw new UnsupportedOperationException();
252+
}
253+
216254
/**
217255
* Saves an instance of an entity, including the properties and relationship defined by the project {@code resultType}.
218256
*

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

+29-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Optional;
3333
import java.util.Set;
3434
import java.util.function.BiFunction;
35+
import java.util.function.BiPredicate;
3536
import java.util.function.Consumer;
3637
import java.util.function.Function;
3738
import java.util.function.Supplier;
@@ -343,6 +344,16 @@ public <T> T save(T instance) {
343344
return saveImpl(instance, Collections.emptyMap(), null);
344345
}
345346

347+
@Override
348+
public <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
349+
350+
if (instance == null) {
351+
return null;
352+
}
353+
354+
return saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null);
355+
}
356+
346357
@Override
347358
public <T, R> R saveAs(T instance, Class<R> resultType) {
348359

@@ -451,10 +462,10 @@ private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersist
451462

452463
@Override
453464
public <T> List<T> saveAll(Iterable<T> instances) {
454-
return saveAllImpl(instances, Collections.emptyMap());
465+
return saveAllImpl(instances, Collections.emptyMap(), null);
455466
}
456467

457-
private <T> List<T> saveAllImpl(Iterable<T> instances, Map<PropertyPath, Boolean> includedProperties) {
468+
private <T> List<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
458469

459470
Set<Class<?>> types = new HashSet<>();
460471
List<T> entities = new ArrayList<>();
@@ -469,13 +480,19 @@ private <T> List<T> saveAllImpl(Iterable<T> instances, Map<PropertyPath, Boolean
469480

470481
boolean heterogeneousCollection = types.size() > 1;
471482
Class<?> domainClass = types.iterator().next();
483+
484+
Map<PropertyPath, Boolean> pps = includeProperty == null ?
485+
includedProperties :
486+
TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass,
487+
includeProperty);
488+
472489
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainClass);
473490
if (heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()
474491
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
475492
log.debug("Saving entities using single statements.");
476493

477494
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
478-
return entities.stream().map(e -> saveImpl(e, includedProperties, stateMachine)).collect(Collectors.toList());
495+
return entities.stream().map(e -> saveImpl(e, pps, stateMachine)).collect(Collectors.toList());
479496
}
480497

481498
class Tuple3<T> {
@@ -497,7 +514,7 @@ class Tuple3<T> {
497514
// Save roots
498515
@SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it
499516
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass);
500-
binderFunction = TemplateSupport.createAndApplyPropertyFilter(includedProperties, entityMetaData, binderFunction);
517+
binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps, entityMetaData, binderFunction);
501518
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
502519
.collect(Collectors.toList());
503520
Map<Value, Long> idToInternalIdMapping = neo4jClient
@@ -515,10 +532,16 @@ class Tuple3<T> {
515532
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
516533
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
517534
Long internalId = idToInternalIdMapping.get(id);
518-
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
535+
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
519536
}).collect(Collectors.toList());
520537
}
521538

539+
@Override
540+
public <T> List<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
541+
542+
return saveAllImpl(instances, null, includeProperty);
543+
}
544+
522545
@Override
523546
public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
524547

@@ -537,7 +560,7 @@ public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
537560
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
538561
projectionFactory, neo4jMappingContext);
539562

540-
List<T> savedInstances = saveAllImpl(instances, pps);
563+
List<T> savedInstances = saveAllImpl(instances, pps, null);
541564

542565
if (projectionInformation.isClosed()) {
543566
return savedInstances.stream().map(instance -> projectionFactory.createProjection(resultType, instance))

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

+38
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
*/
1616
package org.springframework.data.neo4j.core;
1717

18+
import org.springframework.data.mapping.PropertyPath;
1819
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
1920
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
2021
import reactor.core.publisher.Flux;
2122
import reactor.core.publisher.Mono;
2223

2324
import java.util.Map;
25+
import java.util.function.BiPredicate;
2426

2527
import org.apiguardian.api.API;
2628
import org.neo4j.cypherdsl.core.Statement;
@@ -190,6 +192,24 @@ public interface ReactiveNeo4jOperations {
190192
*/
191193
<T> Mono<T> save(T instance);
192194

195+
/**
196+
* Saves an instance of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
197+
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
198+
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
199+
* <p>
200+
* Be careful when reusing the returned instance for further persistence operations, as it will most likely not be
201+
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
202+
*
203+
* @param instance the entity to be saved. Must not be {@code null}.
204+
* @param includeProperty A predicate to determine the properties to save.
205+
* @param <T> the type of the entity.
206+
* @return the saved instance.
207+
* @since 6.3
208+
*/
209+
default <T> Mono<T> saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
210+
throw new UnsupportedOperationException();
211+
}
212+
193213
/**
194214
* Saves an instance of an entity, including the properties and relationship defined by the projected {@code resultType}.
195215
*
@@ -212,6 +232,24 @@ default <T, R> Mono<R> saveAs(T instance, Class<R> resultType) {
212232
*/
213233
<T> Flux<T> saveAll(Iterable<T> instances);
214234

235+
/**
236+
* Saves several instances of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
237+
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
238+
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
239+
* <p>
240+
* Be careful when reusing the returned instances for further persistence operations, as they will most likely not be
241+
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
242+
*
243+
* @param instances the instances to be saved. Must not be {@code null}.
244+
* @param includeProperty A predicate to determine the properties to save.
245+
* @param <T> the type of the entity.
246+
* @return the saved instances.
247+
* @since 6.3
248+
*/
249+
default <T> Flux<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
250+
throw new UnsupportedOperationException();
251+
}
252+
215253
/**
216254
* Saves several instances of an entity, including the properties and relationship defined by the project {@code resultType}.
217255
*

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

+29-6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.concurrent.ConcurrentHashMap;
4141
import java.util.concurrent.atomic.AtomicReference;
4242
import java.util.function.BiFunction;
43+
import java.util.function.BiPredicate;
4344
import java.util.function.Function;
4445
import java.util.function.Supplier;
4546
import java.util.stream.Collectors;
@@ -327,6 +328,16 @@ public <T> Mono<T> save(T instance) {
327328
return saveImpl(instance, Collections.emptyMap(), null);
328329
}
329330

331+
@Override
332+
public <T> Mono<T> saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
333+
334+
if (instance == null) {
335+
return null;
336+
}
337+
338+
return saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null);
339+
}
340+
330341
@Override
331342
public <T, R> Mono<R> saveAs(T instance, Class<R> resultType) {
332343

@@ -463,7 +474,13 @@ private <T> Mono<Tuple2<T, DynamicLabels>> determineDynamicLabels(T entityToBeSa
463474

464475
@Override
465476
public <T> Flux<T> saveAll(Iterable<T> instances) {
466-
return saveAllImpl(instances, Collections.emptyMap());
477+
return saveAllImpl(instances, Collections.emptyMap(), null);
478+
}
479+
480+
@Override
481+
public <T> Flux<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
482+
483+
return saveAllImpl(instances, null, includeProperty);
467484
}
468485

469486
@Override
@@ -481,7 +498,7 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
481498
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
482499
projectionFactory, neo4jMappingContext);
483500

484-
Flux<T> savedInstances = saveAllImpl(instances, pps);
501+
Flux<T> savedInstances = saveAllImpl(instances, pps, null);
485502
if (projectionInformation.isClosed()) {
486503
return savedInstances.map(instance -> projectionFactory.createProjection(resultType, instance));
487504
}
@@ -495,7 +512,7 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
495512
}).map(instance -> projectionFactory.createProjection(resultType, instance));
496513
}
497514

498-
private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties) {
515+
private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
499516

500517
Set<Class<?>> types = new HashSet<>();
501518
List<T> entities = new ArrayList<>();
@@ -510,18 +527,24 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat
510527

511528
boolean heterogeneousCollection = types.size() > 1;
512529
Class<?> domainClass = types.iterator().next();
530+
531+
Map<PropertyPath, Boolean> pps = includeProperty == null ?
532+
includedProperties :
533+
TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass,
534+
includeProperty);
535+
513536
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainClass);
514537
if (heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()
515538
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
516539
log.debug("Saving entities using single statements.");
517540

518541
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
519-
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, includedProperties, stateMachine));
542+
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, pps, stateMachine));
520543
}
521544

522545
@SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it
523546
Function<T, Map<String, Object>> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
524-
includedProperties, entityMetaData,
547+
pps, entityMetaData,
525548
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass));
526549
return Flux.fromIterable(entities)
527550
// Map all entities into a tuple <Original, OriginalWasNew>
@@ -550,7 +573,7 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat
550573
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
551574
Long internalId = idToInternalIdMapping.get(id);
552575
return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId),
553-
TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
576+
TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
554577
}))
555578
);
556579
}

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

+24
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Set;
2626
import java.util.function.BiFunction;
27+
import java.util.function.BiPredicate;
2728
import java.util.function.Function;
2829
import java.util.function.Predicate;
2930
import java.util.function.Supplier;
@@ -45,8 +46,10 @@
4546
import org.springframework.data.neo4j.core.mapping.EntityInstanceWithSource;
4647
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
4748
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
49+
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
4850
import org.springframework.data.neo4j.core.mapping.NodeDescription;
4951
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
52+
import org.springframework.data.neo4j.core.mapping.PropertyTraverser;
5053
import org.springframework.data.neo4j.repository.query.QueryFragments;
5154
import org.springframework.lang.Nullable;
5255
import org.springframework.util.Assert;
@@ -272,6 +275,27 @@ static <T> FilteredBinderFunction<T> createAndApplyPropertyFilter(
272275
}));
273276
}
274277

278+
/**
279+
* Helper function that computes the map of included properties for a dynamic projection as expected in 6.2, but
280+
* for fully dynamic projection
281+
*
282+
* @param mappingContext The context to work on
283+
* @param domainType The projected domain type
284+
* @param predicate The predicate to compute the included columns
285+
* @param <T> Type of the domain type
286+
* @return A map as expected by the property filter.
287+
*/
288+
static <T> Map<PropertyPath, Boolean> computeIncludedPropertiesFromPredicate(Neo4jMappingContext mappingContext,
289+
Class<T> domainType, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> predicate) {
290+
if (predicate == null) {
291+
return Collections.emptyMap();
292+
}
293+
Map<PropertyPath, Boolean> pps = new HashMap<>();
294+
PropertyTraverser traverser = new PropertyTraverser(mappingContext);
295+
traverser.traverse(domainType, predicate, (path, property) -> pps.put(path, false));
296+
return Collections.unmodifiableMap(pps);
297+
}
298+
275299
/**
276300
* A wrapper around a {@link Function} from entity to {@link Map} which is filtered the {@link PropertyFilter} included as well.
277301
*

0 commit comments

Comments
 (0)