Skip to content

Commit 22ff284

Browse files
committed
everything works but nullaway still fails
Signed-off-by: Gerrit Meier <[email protected]>
1 parent 4144bda commit 22ff284

File tree

10 files changed

+381
-70
lines changed

10 files changed

+381
-70
lines changed

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

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.function.Function;
3434
import java.util.function.Supplier;
3535
import java.util.stream.Collectors;
36+
import java.util.stream.Stream;
3637

3738
import org.apache.commons.logging.LogFactory;
3839
import org.apiguardian.api.API;
@@ -87,7 +88,7 @@
8788
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
8889
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
8990
import org.springframework.data.neo4j.core.schema.TargetNode;
90-
import org.springframework.data.neo4j.core.support.UserDefinedChangeEvaluator;
91+
import org.springframework.data.neo4j.core.support.NeedsUpdateEvaluator;
9192
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
9293
import org.springframework.data.neo4j.repository.NoResultException;
9394
import org.springframework.data.neo4j.repository.query.QueryFragments;
@@ -157,7 +158,8 @@ public boolean isReadOnly() {
157158

158159
@Nullable
159160
private TransactionTemplate transactionTemplateReadOnly;
160-
private final Map<Class, UserDefinedChangeEvaluator> userDefinedChangeEvaluators = new HashMap<>();
161+
162+
private final Map<Class, NeedsUpdateEvaluator> needsUpdateEvaluators = new HashMap<>();
161163

162164
public Neo4jTemplate(Neo4jClient neo4jClient) {
163165
this(neo4jClient, new Neo4jMappingContext());
@@ -454,15 +456,19 @@ public <T> T save(T instance) {
454456
private <T> T saveImpl(T instance, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties,
455457
@Nullable NestedRelationshipProcessingStateMachine stateMachine) {
456458

457-
if ((stateMachine != null && stateMachine.hasProcessedValue(instance))
458-
|| (this.userDefinedChangeEvaluators.containsKey(instance.getClass()) && !this.userDefinedChangeEvaluators.get(instance.getClass()).needsUpdate(instance))) {
459+
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
459460
return instance;
460461
}
461462

462463
Neo4jPersistentEntity<?> entityMetaData = this.neo4jMappingContext
463464
.getRequiredPersistentEntity(instance.getClass());
464465
boolean isEntityNew = entityMetaData.isNew(instance);
465466

467+
if (!isEntityNew && this.needsUpdateEvaluators.containsKey(instance.getClass())
468+
&& !this.needsUpdateEvaluators.get(instance.getClass()).needsUpdate(instance)) {
469+
return instance;
470+
}
471+
466472
T entityToBeSaved = this.eventSupport.maybeCallBeforeBind(instance);
467473

468474
DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData);
@@ -612,9 +618,16 @@ class Tuple3<T> {
612618
}
613619

614620
List<Tuple3<T>> entitiesToBeSaved = entities.stream()
621+
.filter(e -> entityMetaData.isNew(e) || !this.needsUpdateEvaluators.containsKey(e.getClass())
622+
|| this.needsUpdateEvaluators.get(e.getClass()).needsUpdate(e))
615623
.map(e -> new Tuple3<>(e, entityMetaData.isNew(e), this.eventSupport.maybeCallBeforeBind(e)))
616624
.collect(Collectors.toList());
617625

626+
List<T> unprocessedEntities = entities.stream()
627+
.filter(e -> !entityMetaData.isNew(e) && (this.needsUpdateEvaluators.containsKey(e.getClass())
628+
&& !this.needsUpdateEvaluators.get(e.getClass()).needsUpdate(e)))
629+
.toList();
630+
618631
// Save roots
619632
@SuppressWarnings("unchecked") // We can safely assume here that we have a
620633
// humongous collection with only one single type
@@ -640,7 +653,7 @@ class Tuple3<T> {
640653

641654
// Save related
642655
var stateMachine = new NestedRelationshipProcessingStateMachine(this.neo4jMappingContext, null, null);
643-
return entitiesToBeSaved.stream().map(t -> {
656+
return Stream.of(entitiesToBeSaved.stream().map(t -> {
644657
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance);
645658
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
646659
Object id = TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty,
@@ -652,7 +665,7 @@ class Tuple3<T> {
652665
((includedProperties != null && !includedProperties.isEmpty()) || includeProperty != null)
653666
? pps : includedPropertiesByClass.get(t.modifiedInstance.getClass()),
654667
entityMetaData));
655-
}).collect(Collectors.toList());
668+
}).collect(Collectors.toList()), unprocessedEntities).flatMap(Collection::stream).toList();
656669
}
657670

658671
@Override
@@ -984,18 +997,18 @@ private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity,
984997
var skipUpdateOfEntity = !isNewEntity;
985998
if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder rpweh) {
986999
var relatedEntity = rpweh.getRelatedEntity();
987-
skipUpdateOfEntity &= this.userDefinedChangeEvaluators.containsKey(relatedEntity.getClass())
988-
&& !this.userDefinedChangeEvaluators.get(relatedEntity.getClass()).needsUpdate(relatedEntity);
1000+
skipUpdateOfEntity &= this.needsUpdateEvaluators.containsKey(relatedEntity.getClass())
1001+
&& !this.needsUpdateEvaluators.get(relatedEntity.getClass()).needsUpdate(relatedEntity);
9891002
}
9901003
else {
991-
skipUpdateOfEntity &= this.userDefinedChangeEvaluators.containsKey(relatedValueToStore.getClass())
992-
&& !this.userDefinedChangeEvaluators.get(relatedValueToStore.getClass()).needsUpdate(relatedValueToStore);
1004+
skipUpdateOfEntity &= this.needsUpdateEvaluators.containsKey(relatedValueToStore.getClass())
1005+
&& !this.needsUpdateEvaluators.get(relatedValueToStore.getClass())
1006+
.needsUpdate(relatedValueToStore);
9931007
}
9941008
Object newRelatedObject = stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied)
9951009
? stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied)
996-
: skipUpdateOfEntity
997-
? relatedObjectBeforeCallbacksApplied
998-
: this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied);
1010+
: skipUpdateOfEntity ? relatedObjectBeforeCallbacksApplied
1011+
: this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied);
9991012

10001013
Object relatedInternalId;
10011014
Entity savedEntity = null;
@@ -1004,7 +1017,7 @@ private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity,
10041017
relatedInternalId = stateMachine.getObjectId(relatedValueToStore);
10051018
}
10061019
else {
1007-
if ((isNewEntity || relationshipDescription.cascadeUpdates()) && !skipUpdateOfEntity) {
1020+
if (isNewEntity || (relationshipDescription.cascadeUpdates() && !skipUpdateOfEntity)) {
10081021
savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty,
10091022
currentPropertyPath);
10101023
}
@@ -1341,8 +1354,9 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
13411354
}
13421355
}
13431356
setTransactionManager(transactionManager);
1344-
this.userDefinedChangeEvaluators.putAll(beanFactory.getBeanProvider(UserDefinedChangeEvaluator.class).stream()
1345-
.collect(Collectors.toMap(e -> e.getEvaluatingClass(), e -> e)));
1357+
this.needsUpdateEvaluators.putAll(beanFactory.getBeanProvider(NeedsUpdateEvaluator.class)
1358+
.stream()
1359+
.collect(Collectors.toMap(e -> e.getEvaluatingClass(), e -> e)));
13461360
}
13471361

13481362
// only used for the CDI configuration

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
9393
import org.springframework.data.neo4j.core.mapping.callback.ReactiveEventSupport;
9494
import org.springframework.data.neo4j.core.schema.TargetNode;
95+
import org.springframework.data.neo4j.core.support.NeedsUpdateEvaluator;
9596
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
9697
import org.springframework.data.neo4j.repository.query.QueryFragments;
9798
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
@@ -158,6 +159,8 @@ public boolean isReadOnly() {
158159

159160
private Function<Named, FunctionInvocation> elementIdOrIdFunction;
160161

162+
private final Map<Class, NeedsUpdateEvaluator> needsUpdateEvaluators = new HashMap<>();
163+
161164
public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) {
162165
this(neo4jClient, neo4jMappingContext, null);
163166
}
@@ -651,31 +654,45 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances,
651654
: Objects.requireNonNullElseGet(includedProperties, List::of);
652655

653656
Neo4jPersistentEntity<?> entityMetaData = this.neo4jMappingContext.getRequiredPersistentEntity(domainClass);
657+
658+
List<T> entitiesToProcess = entities.stream()
659+
.filter(e -> this.neo4jMappingContext.getRequiredPersistentEntity(e.getClass()).isNew(e)
660+
|| !this.needsUpdateEvaluators.containsKey(e.getClass())
661+
|| this.needsUpdateEvaluators.get(e.getClass()).needsUpdate(e))
662+
.collect(Collectors.toList());
663+
664+
List<T> unprocessedEntities = entities.stream()
665+
.filter(e -> !this.neo4jMappingContext.getRequiredPersistentEntity(e.getClass()).isNew(e)
666+
&& (this.needsUpdateEvaluators.containsKey(e.getClass())
667+
&& !this.needsUpdateEvaluators.get(e.getClass()).needsUpdate(e)))
668+
.toList();
669+
654670
if (heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()
655671
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
656672
log.debug("Saving entities using single statements.");
657673

658674
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(
659675
this.neo4jMappingContext);
660676

661-
return Flux.fromIterable(entities)
677+
return Flux.fromIterable(entitiesToProcess)
662678
.concatMap(e -> this.saveImpl(e,
663679
((includedProperties != null && !includedProperties.isEmpty()) || includeProperty != null) ? pps
664680
: includedPropertiesByClass.get(e.getClass()),
665-
stateMachine));
681+
stateMachine))
682+
.concatWith(Flux.fromIterable(unprocessedEntities));
666683
}
667684

668685
@SuppressWarnings("unchecked") // We can safely assume here that we have a
669686
// humongous collection with only one single type
670687
// being either T or extending it
671688
Function<T, Map<String, Object>> binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps,
672689
entityMetaData, this.neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass));
673-
return (Flux<T>) Flux.deferContextual((ctx) -> Flux.fromIterable(entities)
690+
return (Flux<T>) Flux.deferContextual((ctx) -> Flux.fromIterable(entitiesToProcess)
674691
// Map all entities into a tuple <Original, OriginalWasNew>
675692
.map(e -> Tuples.of(e, entityMetaData.isNew(e)))
676693
// Map that tuple into a tuple <<Original, OriginalWasNew>,
677694
// PotentiallyModified>
678-
.zipWith(Flux.fromIterable(entities).flatMapSequential(this.eventSupport::maybeCallBeforeBind))
695+
.zipWith(Flux.fromIterable(entitiesToProcess).flatMapSequential(this.eventSupport::maybeCallBeforeBind))
679696
// And for my own sanity, back into a flat Tuple3
680697
.map(nested -> Tuples.of(nested.getT1().getT1(), nested.getT1().getT2(), nested.getT2()))
681698
.collectList()
@@ -708,7 +725,8 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances,
708725
}))))
709726
.contextWrite(ctx -> ctx
710727
.put("stateMachine", new NestedRelationshipProcessingStateMachine(this.neo4jMappingContext, null, null))
711-
.put("knownRelIds", new HashSet<>()));
728+
.put("knownRelIds", new HashSet<>()))
729+
.concatWith(Flux.fromIterable(unprocessedEntities));
712730
}
713731

714732
@Override
@@ -1114,12 +1132,17 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
11141132
.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
11151133
boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied);
11161134

1135+
var related = (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder rpweh)
1136+
? rpweh.getRelatedEntity() : relatedValueToStore;
1137+
1138+
var skipUpdateOfEntity = !isNewEntity && this.needsUpdateEvaluators.containsKey(related.getClass())
1139+
&& !this.needsUpdateEvaluators.get(related.getClass()).needsUpdate(related);
11171140
return Mono.deferContextual(ctx ->
11181141

11191142
(stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied)
11201143
? Mono.just(stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied))
1121-
: this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied))
1122-
1144+
: skipUpdateOfEntity ? Mono.just(relatedObjectBeforeCallbacksApplied)
1145+
: this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied))
11231146
.flatMap(newRelatedObject -> {
11241147

11251148
Mono<Tuple2<AtomicReference<Object>, AtomicReference<Entity>>> queryOrSave;
@@ -1133,7 +1156,7 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
11331156
}
11341157
else {
11351158
Mono<Entity> savedEntity;
1136-
if (isNewEntity || relationshipDescription.cascadeUpdates()) {
1159+
if (isNewEntity || (relationshipDescription.cascadeUpdates() && !skipUpdateOfEntity)) {
11371160
savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty,
11381161
currentPropertyPath);
11391162
}
@@ -1467,6 +1490,9 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
14671490
}
14681491
}
14691492
setTransactionManager(reactiveTransactionManager);
1493+
this.needsUpdateEvaluators.putAll(beanFactory.getBeanProvider(NeedsUpdateEvaluator.class)
1494+
.stream()
1495+
.collect(Collectors.toMap(e -> e.getEvaluatingClass(), e -> e)));
14701496
}
14711497

14721498
private void setTransactionManager(@Nullable ReactiveTransactionManager reactiveTransactionManager) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2011-2025 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.core.support;
17+
18+
/**
19+
* Interface to implement for a concrete implementation (not an inherited class or
20+
* interface). Returns if an entity and its relationships needs to be processed for
21+
* updates or not.
22+
*
23+
* @param <T> type to implement the needs update logic for
24+
* @author Gerrit Meier
25+
*/
26+
public interface NeedsUpdateEvaluator<T> {
27+
28+
/**
29+
* Report if this entity needs to be considered for an update. This includes possible
30+
* relationships.
31+
* @param instance instance of type `T` to check
32+
* @return true, if it should be processed
33+
*/
34+
default boolean needsUpdate(T instance) {
35+
return true;
36+
}
37+
38+
Class<T> getEvaluatingClass();
39+
40+
}

src/main/java/org/springframework/data/neo4j/core/support/UserDefinedChangeEvaluator.java

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)