Skip to content

Commit 23e6f18

Browse files
michael-simonsmeistermeier
authored andcommitted
GH-2536 - Provide saveAs overloads with a predicate for selecting projected attributes.
Closes #2536.
1 parent 6c91ee6 commit 23e6f18

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
@@ -41,6 +41,7 @@
4141
import java.util.concurrent.ConcurrentHashMap;
4242
import java.util.concurrent.atomic.AtomicReference;
4343
import java.util.function.BiFunction;
44+
import java.util.function.BiPredicate;
4445
import java.util.function.Function;
4546
import java.util.function.Supplier;
4647
import java.util.stream.Collectors;
@@ -328,6 +329,16 @@ public <T> Mono<T> save(T instance) {
328329
return saveImpl(instance, Collections.emptyMap(), null);
329330
}
330331

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

@@ -464,7 +475,13 @@ private <T> Mono<Tuple2<T, DynamicLabels>> determineDynamicLabels(T entityToBeSa
464475

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

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

485-
Flux<T> savedInstances = saveAllImpl(instances, pps);
502+
Flux<T> savedInstances = saveAllImpl(instances, pps, null);
486503
if (projectionInformation.isClosed()) {
487504
return savedInstances.map(instance -> projectionFactory.createProjection(resultType, instance));
488505
}
@@ -496,7 +513,7 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
496513
}).map(instance -> projectionFactory.createProjection(resultType, instance));
497514
}
498515

499-
private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties) {
516+
private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
500517

501518
Set<Class<?>> types = new HashSet<>();
502519
List<T> entities = new ArrayList<>();
@@ -511,18 +528,24 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat
511528

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

519542
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
520-
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, includedProperties, stateMachine));
543+
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, pps, stateMachine));
521544
}
522545

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

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)