Skip to content

Commit 3acc5ec

Browse files
committed
everything works but nullaway still fails
Signed-off-by: Gerrit Meier <[email protected]>
1 parent 3ae1968 commit 3acc5ec

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
}
@@ -652,31 +655,45 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances,
652655
: Objects.requireNonNullElseGet(includedProperties, List::of);
653656

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

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

662-
return Flux.fromIterable(entities)
678+
return Flux.fromIterable(entitiesToProcess)
663679
.concatMap(e -> this.saveImpl(e,
664680
((includedProperties != null && !includedProperties.isEmpty()) || includeProperty != null) ? pps
665681
: includedPropertiesByClass.get(e.getClass()),
666-
stateMachine));
682+
stateMachine))
683+
.concatWith(Flux.fromIterable(unprocessedEntities));
667684
}
668685

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

715733
@Override
@@ -1115,12 +1133,17 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
11151133
.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
11161134
boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied);
11171135

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

11201143
(stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied)
11211144
? Mono.just(stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied))
1122-
: this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied))
1123-
1145+
: skipUpdateOfEntity ? Mono.just(relatedObjectBeforeCallbacksApplied)
1146+
: this.eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied))
11241147
.flatMap(newRelatedObject -> {
11251148

11261149
Mono<Tuple2<AtomicReference<Object>, AtomicReference<Entity>>> queryOrSave;
@@ -1134,7 +1157,7 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
11341157
}
11351158
else {
11361159
Mono<Entity> savedEntity;
1137-
if (isNewEntity || relationshipDescription.cascadeUpdates()) {
1160+
if (isNewEntity || (relationshipDescription.cascadeUpdates() && !skipUpdateOfEntity)) {
11381161
savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty,
11391162
currentPropertyPath);
11401163
}
@@ -1468,6 +1491,9 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
14681491
}
14691492
}
14701493
setTransactionManager(reactiveTransactionManager);
1494+
this.needsUpdateEvaluators.putAll(beanFactory.getBeanProvider(NeedsUpdateEvaluator.class)
1495+
.stream()
1496+
.collect(Collectors.toMap(e -> e.getEvaluatingClass(), e -> e)));
14711497
}
14721498

14731499
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)