Skip to content

Commit 6ee61d2

Browse files
GH-2407 - Apply property filter to binder function in case of single save all statement, too.
This fixes #2407.
1 parent e599f93 commit 6ee61d2

File tree

7 files changed

+302
-41
lines changed

7 files changed

+302
-41
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
* helpful when you received them via {@link FluentFindOperation fluent find operations} as they won't be modifiable.
2929
*
3030
* @author Michael J. Simons
31-
* @since TBA
31+
* @author Gerrit Meier
32+
* @since 6.1.3
3233
*/
33-
@API(status = API.Status.STABLE, since = "TBA")
34+
@API(status = API.Status.STABLE, since = "6.1.3")
3435
public interface FluentSaveOperation {
3536

3637
/**

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

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -370,20 +370,11 @@ private <T> T saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> included
370370

371371
DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData);
372372

373-
@SuppressWarnings("unchecked")
374-
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext
375-
.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass());
376-
377-
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData);
378-
binderFunction = binderFunction.andThen(tree -> {
379-
@SuppressWarnings("unchecked")
380-
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
381-
382-
if (!includeProperty.isNotFiltering()) {
383-
properties.entrySet().removeIf(e -> !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()));
384-
}
385-
return tree;
386-
});
373+
@SuppressWarnings("unchecked") // Applies to retrieving the meta data
374+
TemplateSupport.FilteredBinderFunction<T> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
375+
includedProperties, entityMetaData,
376+
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass())
377+
);
387378
Optional<Entity> newOrUpdatedNode = neo4jClient
388379
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
389380
.bind(entityToBeSaved)
@@ -412,7 +403,7 @@ private <T> T saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> included
412403
}
413404

414405
stateMachine.markValueAsProcessed(instance, internalId);
415-
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, includeProperty);
406+
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, binderFunction.filter);
416407

417408
T bean = propertyAccessor.getBean();
418409
stateMachine.markValueAsProcessedAs(instance, bean);
@@ -489,8 +480,9 @@ class Tuple3<T> {
489480
.collect(Collectors.toList());
490481

491482
// Save roots
492-
@SuppressWarnings("unchecked") // We can safely assume here that we have a homongous collection with only one single type being either T or extending it
483+
@SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it
493484
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass);
485+
binderFunction = TemplateSupport.createAndApplyPropertyFilter(includedProperties, entityMetaData, binderFunction);
494486
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
495487
.collect(Collectors.toList());
496488
Map<Value, Long> idToInternalIdMapping = neo4jClient

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

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.neo4j.cypherdsl.core.Cypher.parameter;
2121

2222
import org.springframework.data.mapping.Association;
23+
import org.springframework.data.neo4j.core.TemplateSupport.FilteredBinderFunction;
2324
import org.springframework.data.neo4j.core.schema.TargetNode;
2425
import reactor.core.publisher.Flux;
2526
import reactor.core.publisher.Mono;
@@ -389,18 +390,9 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> in
389390
DynamicLabels dynamicLabels = t.getT2();
390391

391392
@SuppressWarnings("unchecked")
392-
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext
393-
.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass());
394-
395-
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData);
396-
binderFunction = binderFunction.andThen(tree -> {
397-
@SuppressWarnings("unchecked")
398-
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
399-
if (!includeProperty.isNotFiltering()) {
400-
properties.entrySet().removeIf(e -> !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()));
401-
}
402-
return tree;
403-
});
393+
FilteredBinderFunction<T> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
394+
includedProperties, entityMetaData,
395+
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()));
404396

405397
Mono<Entity> idMono = this.neo4jClient.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
406398
.bind(entityToBeSaved)
@@ -422,7 +414,7 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> in
422414
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode);
423415
finalStateMachine.markValueAsProcessed(instance, newOrUpdatedNode.id());
424416
}).map(Entity::id)
425-
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, includeProperty));
417+
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, binderFunction.filter));
426418
});
427419
}
428420

@@ -512,7 +504,9 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat
512504
}
513505

514506
@SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it
515-
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass);
507+
Function<T, Map<String, Object>> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
508+
includedProperties, entityMetaData,
509+
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass));
516510
return Flux.fromIterable(entities)
517511
// Map all entities into a tuple <Original, OriginalWasNew>
518512
.map(e -> Tuples.of(e, entityMetaData.isNew(e)))

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

Lines changed: 50 additions & 1 deletion
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.Function;
2728
import java.util.function.Predicate;
2829
import java.util.stream.Collectors;
2930
import java.util.stream.StreamSupport;
@@ -38,14 +39,14 @@
3839
import org.neo4j.driver.types.MapAccessor;
3940
import org.neo4j.driver.types.TypeSystem;
4041
import org.springframework.data.mapping.PersistentPropertyAccessor;
42+
import org.springframework.data.mapping.PropertyPath;
4143
import org.springframework.data.neo4j.core.mapping.Constants;
4244
import org.springframework.data.neo4j.core.mapping.EntityInstanceWithSource;
4345
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
4446
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
4547
import org.springframework.data.neo4j.core.mapping.NodeDescription;
4648
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
4749
import org.springframework.data.neo4j.repository.query.QueryFragments;
48-
import org.springframework.data.mapping.PropertyPath;
4950
import org.springframework.lang.Nullable;
5051
import org.springframework.util.Assert;
5152

@@ -241,6 +242,54 @@ Statement toStatement(NodeDescription<?> nodeDescription) {
241242
return mappingFunction;
242243
}
243244

245+
/**
246+
* Computes a {@link PropertyFilter} from a set of included properties based on an entities meta data and applies it
247+
* to a given binder function.
248+
*
249+
* @param includedProperties The set of included properties
250+
* @param entityMetaData The metadata of the entity in question
251+
* @param binderFunction The original binder function for persisting the entity.
252+
* @param <T> The type of the entity
253+
* @return A new binder function that only works on the included properties.
254+
*/
255+
static <T> FilteredBinderFunction<T> createAndApplyPropertyFilter(
256+
Map<PropertyPath, Boolean> includedProperties, Neo4jPersistentEntity<?> entityMetaData,
257+
Function<T, Map<String, Object>> binderFunction) {
258+
259+
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData);
260+
return new FilteredBinderFunction<>(includeProperty, binderFunction.andThen(tree -> {
261+
@SuppressWarnings("unchecked")
262+
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
263+
264+
if (!includeProperty.isNotFiltering()) {
265+
properties.entrySet()
266+
.removeIf(e -> !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()));
267+
}
268+
return tree;
269+
}));
270+
}
271+
272+
/**
273+
* A wrapper around a {@link Function} from entity to {@link Map} which is filtered the {@link PropertyFilter} included as well.
274+
*
275+
* @param <T> Type of the entity
276+
*/
277+
static class FilteredBinderFunction<T> implements Function<T, Map<String, Object>> {
278+
final PropertyFilter filter;
279+
280+
final Function<T, Map<String, Object>> binderFunction;
281+
282+
FilteredBinderFunction(PropertyFilter filter, Function<T, Map<String, Object>> binderFunction) {
283+
this.filter = filter;
284+
this.binderFunction = binderFunction;
285+
}
286+
287+
@Override
288+
public Map<String, Object> apply(T t) {
289+
return binderFunction.apply(t);
290+
}
291+
}
292+
244293
private TemplateSupport() {
245294
}
246295
}

src/test/java/org/springframework/data/neo4j/integration/imperative/Neo4jTemplateIT.java

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
5454
import org.springframework.data.neo4j.integration.shared.common.Person;
5555
import org.springframework.data.neo4j.integration.shared.common.PersonWithAllConstructor;
56+
import org.springframework.data.neo4j.integration.shared.common.PersonWithAssignedId;
5657
import org.springframework.data.neo4j.integration.shared.common.ThingWithGeneratedId;
5758
import org.springframework.data.neo4j.test.BookmarkCapture;
5859
import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport;
@@ -101,14 +102,14 @@ void setupData() {
101102
Values.parameters("name", TEST_PERSON2_NAME)).single().get("id").asLong();
102103

103104
transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})");
104-
transaction
105-
.run("CREATE (p:Person{firstName: 'Michael', lastName: 'Siemons'})" +
105+
transaction.run("CREATE (p:Person{firstName: 'Michael', lastName: 'Siemons'})" +
106106
" -[:LIVES_AT]-> (a:Address {city: 'Aachen'})" +
107107
" -[:BASED_IN]->(c:YetAnotherCountryEntity{name: 'Gemany', countryCode: 'DE'})" +
108108
" RETURN id(p)");
109-
transaction
110-
.run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schnitzel'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'}) RETURN id(p)");
109+
transaction.run(
110+
"CREATE (p:Person{firstName: 'Helge', lastName: 'Schnitzel'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'}) RETURN id(p)");
111111
transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})");
112+
transaction.run("CREATE (p:PersonWithAssignedId{id: 'x', firstName: 'John', lastName: 'Doe'})");
112113

113114
transaction.commit();
114115
bookmarkCapture.seedWith(session.lastBookmark());
@@ -438,9 +439,9 @@ void saveAllAsWithOpenProjectionShouldWork() {
438439
p2.setFirstName("Helga");
439440
p2.setLastName("Schneider");
440441

441-
List<OpenProjection> openProjection = neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), OpenProjection.class);
442+
List<OpenProjection> openProjections = neo4jTemplate.saveAllAs(Arrays.asList(p1, p2), OpenProjection.class);
442443

443-
assertThat(openProjection).extracting(OpenProjection::getFullName)
444+
assertThat(openProjections).extracting(OpenProjection::getFullName)
444445
.containsExactlyInAnyOrder("Michael Simons", "Helge Schneider");
445446

446447
List<Person> people = neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class);
@@ -529,6 +530,105 @@ void saveAsWithClosedProjectionOnSecondLevelShouldWork() {
529530
assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail");
530531
}
531532

533+
@Test // GH-2407
534+
void saveAllAsWithClosedProjectionOnSecondLevelShouldWork() {
535+
536+
Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address) RETURN p, collect(r), collect(a)",
537+
Collections.singletonMap("lastName", "Siemons"), Person.class).get();
538+
539+
p.setFirstName("Klaus");
540+
p.setLastName("Simons");
541+
p.getAddress().setCity("Braunschweig");
542+
p.getAddress().setStreet("Single Trail");
543+
List<ClosedProjectionWithEmbeddedProjection> projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), ClosedProjectionWithEmbeddedProjection.class);
544+
545+
assertThat(projections)
546+
.hasSize(1).first()
547+
.satisfies(projection -> assertThat(projection.getAddress().getStreet()).isEqualTo("Single Trail"));
548+
549+
p = neo4jTemplate.findById(p.getId(), Person.class).get();
550+
assertThat(p.getFirstName()).isEqualTo("Michael");
551+
assertThat(p.getLastName()).isEqualTo("Simons");
552+
assertThat(p.getAddress().getCity()).isEqualTo("Aachen");
553+
assertThat(p.getAddress().getStreet()).isEqualTo("Single Trail");
554+
}
555+
556+
@Test // GH-2407
557+
void shouldSaveNewProjectedThing() {
558+
559+
Person p = new Person();
560+
p.setFirstName("John");
561+
p.setLastName("Doe");
562+
563+
ClosedProjection projection = neo4jTemplate.saveAs(p, ClosedProjection.class);
564+
List<Person> people = neo4jTemplate.findAll("MATCH (p:Person {lastName: $lastName}) RETURN p",
565+
Collections.singletonMap("lastName", "Doe"), Person.class);
566+
assertThat(people).hasSize(1)
567+
.first().satisfies(person -> {
568+
assertThat(person.getFirstName()).isNull();
569+
assertThat(person.getLastName()).isEqualTo(projection.getLastName());
570+
});
571+
}
572+
573+
@Test // GH-2407
574+
void shouldSaveAllNewProjectedThings() {
575+
576+
Person p = new Person();
577+
p.setFirstName("John");
578+
p.setLastName("Doe");
579+
580+
List<ClosedProjection> projections = neo4jTemplate.saveAllAs(Collections.singletonList(p),
581+
ClosedProjection.class);
582+
assertThat(projections).hasSize(1);
583+
584+
ClosedProjection projection = projections.get(0);
585+
List<Person> people = neo4jTemplate.findAll("MATCH (p:Person {lastName: $lastName}) RETURN p",
586+
Collections.singletonMap("lastName", "Doe"), Person.class);
587+
assertThat(people).hasSize(1)
588+
.first().satisfies(person -> {
589+
assertThat(person.getFirstName()).isNull();
590+
assertThat(person.getLastName()).isEqualTo(projection.getLastName());
591+
});
592+
}
593+
594+
@Test // GH-2407
595+
void shouldSaveAllAsWithAssignedIdProjected() {
596+
597+
PersonWithAssignedId p = neo4jTemplate.findById("x", PersonWithAssignedId.class).get();
598+
p.setLastName("modifiedLast");
599+
p.setFirstName("modifiedFirst");
600+
601+
List<ClosedProjection> projections = neo4jTemplate.saveAllAs(Collections.singletonList(p),
602+
ClosedProjection.class);
603+
assertThat(projections).hasSize(1);
604+
605+
ClosedProjection projection = projections.get(0);
606+
List<PersonWithAssignedId> people = neo4jTemplate.findAll("MATCH (p:PersonWithAssignedId {id: $id}) RETURN p",
607+
Collections.singletonMap("id", "x"), PersonWithAssignedId.class);
608+
assertThat(people).hasSize(1)
609+
.first().satisfies(person -> {
610+
assertThat(person.getFirstName()).isEqualTo("John");
611+
assertThat(person.getLastName()).isEqualTo(projection.getLastName());
612+
});
613+
}
614+
615+
@Test // GH-2407
616+
void shouldSaveAsWithAssignedIdProjected() {
617+
618+
PersonWithAssignedId p = neo4jTemplate.findById("x", PersonWithAssignedId.class).get();
619+
p.setLastName("modifiedLast");
620+
p.setFirstName("modifiedFirst");
621+
622+
ClosedProjection projection = neo4jTemplate.saveAs(p, ClosedProjection.class);
623+
List<PersonWithAssignedId> people = neo4jTemplate.findAll("MATCH (p:PersonWithAssignedId {id: $id}) RETURN p",
624+
Collections.singletonMap("id", "x"), PersonWithAssignedId.class);
625+
assertThat(people).hasSize(1)
626+
.first().satisfies(person -> {
627+
assertThat(person.getFirstName()).isEqualTo("John");
628+
assertThat(person.getLastName()).isEqualTo(projection.getLastName());
629+
});
630+
}
631+
532632
@Test
533633
void saveAsWithClosedProjectionOnThreeLevelShouldWork() {
534634

@@ -548,6 +648,28 @@ void saveAsWithClosedProjectionOnThreeLevelShouldWork() {
548648
assertThat(savedCountry.getName()).isEqualTo("Germany");
549649
}
550650

651+
@Test // GH-2407
652+
void saveAllAsWithClosedProjectionOnThreeLevelShouldWork() {
653+
654+
Person p = neo4jTemplate.findOne("MATCH (p:Person {lastName: $lastName})-[r:LIVES_AT]-(a:Address)-[r2:BASED_IN]->(c:YetAnotherCountryEntity) RETURN p, collect(r), collect(r2), collect(a), collect(c)",
655+
Collections.singletonMap("lastName", "Siemons"), Person.class).get();
656+
657+
Person.Address.Country country = p.getAddress().getCountry();
658+
country.setName("Germany");
659+
country.setCountryCode("AT");
660+
661+
List<ClosedProjectionWithEmbeddedProjection> projections = neo4jTemplate.saveAllAs(Collections.singletonList(p), ClosedProjectionWithEmbeddedProjection.class);
662+
663+
assertThat(projections)
664+
.hasSize(1).first()
665+
.satisfies(projection -> assertThat(projection.getAddress().getCountry().getName()).isEqualTo("Germany"));
666+
667+
p = neo4jTemplate.findById(p.getId(), Person.class).get();
668+
Person.Address.Country savedCountry = p.getAddress().getCountry();
669+
assertThat(savedCountry.getCountryCode()).isEqualTo("DE");
670+
assertThat(savedCountry.getName()).isEqualTo("Germany");
671+
}
672+
551673
@Test
552674
void saveAllAsWithClosedProjectionShouldWork() {
553675

@@ -563,10 +685,10 @@ void saveAllAsWithClosedProjectionShouldWork() {
563685
p2.setFirstName("Helga");
564686
p2.setLastName("Schneider");
565687

566-
List<ClosedProjection> openProjection = neo4jTemplate
688+
List<ClosedProjection> closedProjections = neo4jTemplate
567689
.saveAllAs(Arrays.asList(p1, p2), ClosedProjection.class);
568690

569-
assertThat(openProjection).extracting(ClosedProjection::getLastName)
691+
assertThat(closedProjections).extracting(ClosedProjection::getLastName)
570692
.containsExactlyInAnyOrder("Simons", "Schneider");
571693

572694
List<Person> people = neo4jTemplate.findAllById(Arrays.asList(p1.getId(), p2.getId()), Person.class);

0 commit comments

Comments
 (0)