Skip to content

Commit 9ed7557

Browse files
GH-2347 - Track state of objects with assigned ids correctly.
This change makes the Cypher generator create a statement for persisting multiple instances with assigned ids that not only returns the assigned ID, but also the internally generated id. This id is needed for tracking the root objects state in the `NestedRelationshipProcessingStateMachine`. Those ids must of course be retrieved. By doing so we currently loos the ability to debug log the number of nodes created. While this removal of a log statement can be considered a breaking change, it is necessary. To lighten that change: Storing multiple instances with internally generated did not debug log these informations at all. This fixes #2347.
1 parent 0ec3b9c commit 9ed7557

File tree

9 files changed

+404
-48
lines changed

9 files changed

+404
-48
lines changed

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

+15-21
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
2020
import static org.neo4j.cypherdsl.core.Cypher.parameter;
2121

22+
import java.util.AbstractMap;
2223
import java.util.ArrayList;
2324
import java.util.Collection;
2425
import java.util.Collections;
@@ -42,9 +43,9 @@
4243
import org.neo4j.cypherdsl.core.Node;
4344
import org.neo4j.cypherdsl.core.Statement;
4445
import org.neo4j.cypherdsl.core.renderer.Renderer;
46+
import org.neo4j.driver.Value;
4547
import org.neo4j.driver.exceptions.NoSuchRecordException;
4648
import org.neo4j.driver.summary.ResultSummary;
47-
import org.neo4j.driver.summary.SummaryCounters;
4849
import org.neo4j.driver.types.Entity;
4950
import org.neo4j.driver.types.MapAccessor;
5051
import org.neo4j.driver.types.TypeSystem;
@@ -466,20 +467,22 @@ class Tuple3<T> {
466467
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
467468
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
468469
.collect(Collectors.toList());
469-
ResultSummary resultSummary = neo4jClient
470+
Map<Value, Long> idToInternalIdMapping = neo4jClient
470471
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
471-
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run();
472-
473-
SummaryCounters counters = resultSummary.counters();
474-
log.debug(() -> String.format(
475-
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
476-
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
477-
counters.relationshipsDeleted(), counters.propertiesSet()));
472+
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
473+
.fetchAs(Map.Entry.class)
474+
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_INTERNAL_ID).asLong()))
475+
.all()
476+
.stream()
477+
.collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (Long) m.getValue()));
478478

479479
// Save related
480480
return entitiesToBeSaved.stream().map(t -> {
481481
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance);
482-
return processRelations(entityMetaData, t.originalInstance, propertyAccessor, t.wasNew, TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
482+
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
483+
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
484+
Long internalId = idToInternalIdMapping.get(id);
485+
return processRelations(entityMetaData, t.originalInstance, internalId, propertyAccessor, t.wasNew, TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
483486
}).collect(Collectors.toList());
484487
}
485488

@@ -632,22 +635,13 @@ private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, @Nulla
632635
* @param isParentObjectNew A flag if the parent was new
633636
* @param includeProperty A predicate telling to include a relationship property or not
634637
*/
635-
private <T> void processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance, Long internalId,
638+
private <T> T processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance, Long internalId,
636639
PersistentPropertyAccessor<?> parentPropertyAccessor,
637640
boolean isParentObjectNew, PropertyFilter includeProperty) {
638641

639-
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
640-
processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
641-
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
642-
}
643-
644-
private <T> T processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance,
645-
PersistentPropertyAccessor<?> parentPropertyAccessor,
646-
boolean isParentObjectNew, PropertyFilter includeProperty) {
647-
648642
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
649643
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
650-
new NestedRelationshipProcessingStateMachine(originalInstance), includeProperty, startingPropertyPath);
644+
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
651645
}
652646

653647
private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> propertyAccessor,

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

+18-22
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
2020
import static org.neo4j.cypherdsl.core.Cypher.parameter;
2121

22+
import org.neo4j.driver.Value;
2223
import org.springframework.data.mapping.PropertyPath;
2324
import org.springframework.data.neo4j.core.mapping.EntityFromDtoInstantiatingConverter;
2425
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
@@ -49,7 +50,6 @@
4950
import org.neo4j.cypherdsl.core.Node;
5051
import org.neo4j.cypherdsl.core.Statement;
5152
import org.neo4j.cypherdsl.core.renderer.Renderer;
52-
import org.neo4j.driver.summary.SummaryCounters;
5353
import org.neo4j.driver.types.Entity;
5454
import org.neo4j.driver.types.MapAccessor;
5555
import org.neo4j.driver.types.TypeSystem;
@@ -499,18 +499,23 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable List<PropertyPa
499499
.map(binderFunction).collect(Collectors.toList());
500500
return neo4jClient
501501
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
502-
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run();
503-
}).doOnNext(resultSummary -> {
504-
SummaryCounters counters = resultSummary.counters();
505-
log.debug(() -> String.format(
506-
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
507-
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
508-
counters.relationshipsDeleted(), counters.propertiesSet()));
509-
}).thenMany(Flux.fromIterable(entitiesToBeSaved)
510-
.flatMap(t -> processRelations(entityMetaData, t.getT1(),
511-
entityMetaData.getPropertyAccessor(t.getT3()), t.getT2(),
512-
TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData)))
513-
));
502+
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
503+
.fetchAs(Tuple2.class)
504+
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_INTERNAL_ID).asLong()))
505+
.all()
506+
.collectMap(m -> (Value) m.getT1(), m -> (Long) m.getT2());
507+
}).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved)
508+
.flatMap(t -> {
509+
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3());
510+
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
511+
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
512+
Long internalId = idToInternalIdMapping.get(id);
513+
return processRelations(entityMetaData, t.getT1(), internalId,
514+
propertyAccessor, t.getT2(),
515+
TemplateSupport.computeIncludePropertyPredicate(includedProperties,
516+
entityMetaData));
517+
}))
518+
);
514519
}
515520

516521
@Override
@@ -741,15 +746,6 @@ private <T> Mono<T> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEnt
741746
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
742747
}
743748

744-
private <T> Mono<T> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance,
745-
PersistentPropertyAccessor<?> parentPropertyAccessor,
746-
boolean isParentObjectNew, PropertyFilter includeProperty) {
747-
748-
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
749-
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
750-
new NestedRelationshipProcessingStateMachine(originalInstance), includeProperty, startingPropertyPath);
751-
}
752-
753749
private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> parentPropertyAccessor,
754750
boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) {
755751

src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,8 @@ public Statement prepareSaveOfMultipleInstancesOf(NodeDescription<?> nodeDescrip
366366
return Cypher.unwind(parameter(Constants.NAME_OF_ENTITY_LIST_PARAM)).as(row)
367367
.merge(rootNode.withProperties(nameOfIdProperty, Cypher.property(row, Constants.NAME_OF_ID)))
368368
.mutate(rootNode, Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM))
369-
.returning(Functions.collect(rootNode.property(nameOfIdProperty)).as(Constants.NAME_OF_IDS)).build();
369+
.returning(rootNode.internalId().as(Constants.NAME_OF_INTERNAL_ID), rootNode.property(nameOfIdProperty).as(Constants.NAME_OF_ID))
370+
.build();
370371
}
371372

372373
@NonNull

src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apiguardian.api.API;
2828
import org.springframework.lang.NonNull;
2929
import org.springframework.lang.Nullable;
30+
import org.springframework.util.Assert;
3031

3132
/**
3233
* This stores all processed nested relations and objects during save of objects so that the recursive descent can be
@@ -72,12 +73,12 @@ public enum ProcessState {
7273
private final Map<Object, Long> processedObjectsIds = new HashMap<>();
7374

7475
public NestedRelationshipProcessingStateMachine(Object initialObject, Long internalId) {
75-
this(initialObject);
76-
processedObjectsIds.put(initialObject, internalId);
77-
}
7876

79-
public NestedRelationshipProcessingStateMachine(Object initialObject) {
77+
Assert.notNull(initialObject, "Initial object must not be null.");
78+
Assert.notNull(internalId, "The initial objects internal ID must not be null.");
79+
8080
processedObjects.add(initialObject);
81+
processedObjectsIds.put(initialObject, internalId);
8182
}
8283

8384
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2011-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2347;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
import org.springframework.data.neo4j.core.schema.Id;
22+
import org.springframework.data.neo4j.core.schema.Node;
23+
import org.springframework.data.neo4j.core.schema.Relationship;
24+
25+
/**
26+
* @author Michael J. Simons
27+
*/
28+
@Node
29+
public class Application {
30+
31+
@Id
32+
private String id;
33+
34+
@Relationship(direction = Relationship.Direction.OUTGOING, type = "CONTAINS_WORKFLOW")
35+
private List<Workflow> workflows = new ArrayList<>();
36+
37+
public Application(String id) {
38+
this.id = id;
39+
}
40+
41+
public String getId() {
42+
return id;
43+
}
44+
45+
public List<Workflow> getWorkflows() {
46+
return workflows;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2011-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2347;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.neo4j.driver.Driver;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
29+
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
30+
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
31+
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
32+
import org.springframework.data.neo4j.repository.Neo4jRepository;
33+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
34+
import org.springframework.data.neo4j.test.BookmarkCapture;
35+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
36+
import org.springframework.transaction.PlatformTransactionManager;
37+
import org.springframework.transaction.annotation.EnableTransactionManagement;
38+
39+
/**
40+
* @author Michael J. Simons
41+
*/
42+
@Neo4jIntegrationTest
43+
class GH2347IT extends TestBase {
44+
45+
@Test
46+
void entitiesWithAssignedIdsSavedInBatchMustBeIdentifiableWithTheirInternalIds(
47+
@Autowired ApplicationRepository applicationRepository,
48+
@Autowired Driver driver,
49+
@Autowired BookmarkCapture bookmarkCapture
50+
) {
51+
List<Application> savedApplications = applicationRepository.saveAll(Collections.singletonList(createData()));
52+
53+
assertThat(savedApplications).hasSize(1);
54+
assertDatabase(driver, bookmarkCapture);
55+
}
56+
57+
@Test
58+
void entitiesWithAssignedIdsMustBeIdentifiableWithTheirInternalIds(
59+
@Autowired ApplicationRepository applicationRepository,
60+
@Autowired Driver driver,
61+
@Autowired BookmarkCapture bookmarkCapture
62+
) {
63+
applicationRepository.save(createData());
64+
assertDatabase(driver, bookmarkCapture);
65+
}
66+
67+
interface ApplicationRepository extends Neo4jRepository<Application, String> {
68+
}
69+
70+
@Configuration
71+
@EnableTransactionManagement
72+
@EnableNeo4jRepositories(considerNestedRepositories = true)
73+
static class Config extends AbstractNeo4jConfig {
74+
75+
@Bean
76+
public BookmarkCapture bookmarkCapture() {
77+
return new BookmarkCapture();
78+
}
79+
80+
@Override
81+
public PlatformTransactionManager transactionManager(Driver driver,
82+
DatabaseSelectionProvider databaseNameProvider) {
83+
84+
BookmarkCapture bookmarkCapture = bookmarkCapture();
85+
return new Neo4jTransactionManager(driver, databaseNameProvider,
86+
Neo4jBookmarkManager.create(bookmarkCapture));
87+
}
88+
89+
@Bean
90+
public Driver driver() {
91+
92+
return neo4jConnectionSupport.getDriver();
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)