Skip to content

Commit 167075d

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

File tree

7 files changed

+300
-37
lines changed

7 files changed

+300
-37
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: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -363,18 +363,10 @@ private <T> T saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> included
363363

364364
DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData);
365365

366-
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext
367-
.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass());
368-
369-
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData);
370-
binderFunction = binderFunction.andThen(tree -> {
371-
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
372-
373-
if (!includeProperty.isNotFiltering()) {
374-
properties.entrySet().removeIf(e -> !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()));
375-
}
376-
return tree;
377-
});
366+
TemplateSupport.FilteredBinderFunction<T> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
367+
includedProperties, entityMetaData,
368+
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass())
369+
);
378370
Optional<Entity> newOrUpdatedNode = neo4jClient
379371
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
380372
.bind(entityToBeSaved)
@@ -403,7 +395,7 @@ private <T> T saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> included
403395
}
404396

405397
stateMachine.markValueAsProcessed(instance, internalId);
406-
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, includeProperty);
398+
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, binderFunction.filter);
407399

408400
T bean = propertyAccessor.getBean();
409401
stateMachine.markValueAsProcessedAs(instance, bean);
@@ -480,6 +472,7 @@ class Tuple3<T> {
480472

481473
// Save roots
482474
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
475+
binderFunction = TemplateSupport.createAndApplyPropertyFilter(includedProperties, entityMetaData, binderFunction);
483476
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
484477
.collect(Collectors.toList());
485478
Map<Value, Long> idToInternalIdMapping = neo4jClient

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.neo4j.core.mapping.EntityFromDtoInstantiatingConverter;
2525
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
2626
import org.springframework.data.mapping.Association;
27+
import org.springframework.data.neo4j.core.TemplateSupport.FilteredBinderFunction;
2728
import org.springframework.data.neo4j.core.schema.TargetNode;
2829
import reactor.core.publisher.Flux;
2930
import reactor.core.publisher.Mono;
@@ -380,17 +381,9 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> in
380381

381382
DynamicLabels dynamicLabels = t.getT2();
382383

383-
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext
384-
.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass());
385-
386-
PropertyFilter includeProperty = TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData);
387-
binderFunction = binderFunction.andThen(tree -> {
388-
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
389-
if (!includeProperty.isNotFiltering()) {
390-
properties.entrySet().removeIf(e -> !includeProperty.contains(e.getKey(), entityMetaData.getUnderlyingClass()));
391-
}
392-
return tree;
393-
});
384+
FilteredBinderFunction<T> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
385+
includedProperties, entityMetaData,
386+
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()));
394387

395388
Mono<Entity> idMono = this.neo4jClient.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
396389
.bind(entityToBeSaved)
@@ -412,7 +405,7 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Map<PropertyPath, Boolean> in
412405
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode);
413406
finalStateMachine.markValueAsProcessed(instance, newOrUpdatedNode.id());
414407
}).map(Entity::id)
415-
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, includeProperty));
408+
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, binderFunction.filter));
416409
});
417410
}
418411

@@ -500,7 +493,9 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat
500493
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, includedProperties, stateMachine));
501494
}
502495

503-
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
496+
Function<T, Map<String, Object>> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
497+
includedProperties, entityMetaData,
498+
neo4jMappingContext.getRequiredBinderFunctionFor(domainClass));
504499
return Flux.fromIterable(entities)
505500
// Map all entities into a tuple <Original, OriginalWasNew>
506501
.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

@@ -238,6 +239,54 @@ Statement toStatement() {
238239
return mappingFunction;
239240
}
240241

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

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)