diff --git a/pom.xml b/pom.xml
index e26bbc9db..e737fe177 100644
--- a/pom.xml
+++ b/pom.xml
@@ -114,6 +114,7 @@
${skipTests}
3.3.0-SNAPSHOT
+ 2.2.0
@@ -141,6 +142,11 @@
${junit-cc-testcontainer}
test
+
+ org.junit-pioneer
+ junit-pioneer
+ ${junit-pioneer.version}
+
io.github.classgraph
classgraph
@@ -441,6 +447,11 @@
blockhound
test
+
+ org.junit-pioneer
+ junit-pioneer
+ test
+
diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java
index 0caea2c1d..9b54d6e50 100644
--- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java
+++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java
@@ -890,8 +890,7 @@ public final class Neo4jTemplate implements
// here a map entry is not always anymore a dynamic association
Object relatedObjectBeforeCallbacksApplied = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
Neo4jPersistentEntity> targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
-
- boolean isEntityNew = targetEntity.isNew(relatedObjectBeforeCallbacksApplied);
+ boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied);
Object newRelatedObject = stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied)
? stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied)
@@ -903,7 +902,13 @@ public final class Neo4jTemplate implements
if (stateMachine.hasProcessedValue(relatedValueToStore)) {
relatedInternalId = stateMachine.getObjectId(relatedValueToStore);
} else {
- savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath);
+ if (isNewEntity || relationshipDescription.cascadeUpdates()) {
+ savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath);
+ } else {
+ var targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
+ var requiredIdProperty = targetEntity.getRequiredIdProperty();
+ savedEntity = loadRelatedNode(targetEntity, targetPropertyAccessor.getProperty(requiredIdProperty));
+ }
relatedInternalId = TemplateSupport.rendererCanUseElementIdIfPresent(renderer, targetEntity) ? savedEntity.elementId() : savedEntity.id();
stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId);
if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) {
@@ -987,7 +992,7 @@ public final class Neo4jTemplate implements
}
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
- processNestedRelations(targetEntity, targetPropertyAccessor, isEntityNew, stateMachine, includeProperty, currentPropertyPath);
+ processNestedRelations(targetEntity, targetPropertyAccessor, isNewEntity, stateMachine, includeProperty, currentPropertyPath);
}
Object potentiallyRecreatedNewRelatedObject = MappingSupport.getRelationshipOrRelationshipPropertiesObject(neo4jMappingContext,
@@ -1039,6 +1044,23 @@ public final class Neo4jTemplate implements
return finalSubgraphRoot;
}
+ // The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, PropertyFilter.RelaxedPropertyPath)}
+ // We can't do without a query, as we need to refresh the internal id
+ private Entity loadRelatedNode(NodeDescription> targetNodeDescription, Object relatedInternalId) {
+
+ var targetPersistentEntity = (Neo4jPersistentEntity>) targetNodeDescription;
+ var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
+ var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue();
+
+ return neo4jClient
+ .query(() -> renderer.render(
+ cypherGenerator.prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(),
+ queryFragmentsAndParameters.getQueryFragments().getCondition()).returning(nodeName).build()))
+ .bindAll(queryFragmentsAndParameters.getParameters())
+ .fetchAs(Entity.class).mappedBy((t, r) -> r.get(nodeName).asNode())
+ .one().orElseThrow();
+ }
+
private void assignIdToRelationshipProperties(
NestedRelationshipContext relationshipContext,
Object relatedValueToStore,
diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java
index 813cf61d3..f9b661924 100644
--- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java
+++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java
@@ -969,6 +969,9 @@ public final class ReactiveNeo4jTemplate implements
Flux relationshipCreation = Flux.fromIterable(relatedValuesToStore).concatMap(relatedValueToStore -> {
Object relatedObjectBeforeCallbacksApplied = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
+ Neo4jPersistentEntity> targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
+ boolean isNewEntity = targetEntity.isNew(relatedObjectBeforeCallbacksApplied);
+
return Mono.deferContextual(ctx ->
(stateMachine.hasProcessedValue(relatedObjectBeforeCallbacksApplied)
@@ -976,7 +979,6 @@ public final class ReactiveNeo4jTemplate implements
: eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied))
.flatMap(newRelatedObject -> {
- Neo4jPersistentEntity> targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
Mono, AtomicReference>> queryOrSave;
if (stateMachine.hasProcessedValue(relatedValueToStore)) {
@@ -987,7 +989,16 @@ public final class ReactiveNeo4jTemplate implements
}
queryOrSave = Mono.just(Tuples.of(relatedInternalId, new AtomicReference<>()));
} else {
- queryOrSave = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath)
+ Mono savedEntity;
+ if (isNewEntity || relationshipDescription.cascadeUpdates()) {
+ savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath);
+ } else {
+ var targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
+ var requiredIdProperty = targetEntity.getRequiredIdProperty();
+ savedEntity = loadRelatedNode(targetEntity, targetPropertyAccessor.getProperty(requiredIdProperty));
+ }
+
+ queryOrSave = savedEntity
.map(entity -> Tuples.of(new AtomicReference<>((Object) (TemplateSupport.rendererCanUseElementIdIfPresent(renderer, targetEntity) ? entity.elementId() : entity.id())), new AtomicReference<>(entity)))
.doOnNext(t -> {
var relatedInternalId = t.getT1().get();
@@ -998,6 +1009,7 @@ public final class ReactiveNeo4jTemplate implements
}
});
}
+
return queryOrSave.flatMap(idAndEntity -> {
Object relatedInternalId = idAndEntity.getT1().get();
Entity savedEntity = idAndEntity.getT2().get();
@@ -1088,6 +1100,23 @@ public final class ReactiveNeo4jTemplate implements
}
+ // The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, PropertyFilter, PropertyFilter.RelaxedPropertyPath)}
+ // We can't do without a query, as we need to refresh the internal id
+ private Mono loadRelatedNode(NodeDescription> targetNodeDescription, Object relatedInternalId) {
+
+ var targetPersistentEntity = (Neo4jPersistentEntity>) targetNodeDescription;
+ var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
+ var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue();
+
+ return neo4jClient
+ .query(() -> renderer.render(
+ cypherGenerator.prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(),
+ queryFragmentsAndParameters.getQueryFragments().getCondition()).returning(nodeName).build()))
+ .bindAll(queryFragmentsAndParameters.getParameters())
+ .fetchAs(Entity.class).mappedBy((t, r) -> r.get(nodeName).asNode())
+ .one();
+ }
+
private Mono saveRelatedNode(Object relatedNode, Neo4jPersistentEntity> targetNodeDescription, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) {
return determineDynamicLabels(relatedNode, targetNodeDescription)
diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java
index 0c0b2c7c7..d7cc246a4 100644
--- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java
+++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java
@@ -756,6 +756,12 @@ public enum CypherGenerator {
return returnExpressions;
}
+
+ public StatementBuilder.OngoingReading prepareFindOf(NodeDescription> nodeDescription, @Nullable List initialMatchOn, @Nullable Condition condition) {
+ var rootNode = createRootNode(nodeDescription);
+ return prepareMatchOfRootNode(rootNode, initialMatchOn).where(conditionOrNoCondition(condition));
+ }
+
private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity> nodeDescription, SymbolicName nodeName,
Predicate includedProperties, @Nullable RelationshipDescription relationshipDescription, List processedRelationships) {
diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java
index 47f573e4b..505966a67 100644
--- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java
+++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java
@@ -169,7 +169,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
DefaultRelationshipDescription relationshipDescription = new DefaultRelationshipDescription(this,
obverseRelationshipDescription.orElse(null), type, dynamicAssociation, (NodeDescription>) getOwner(),
- this.getName(), obverseOwner, direction, relationshipPropertiesClass);
+ this.getName(), obverseOwner, direction, relationshipPropertiesClass, relationship == null || relationship.cascadeUpdates());
// Update the previous found, if any, relationship with the newly created one as its counterpart.
obverseRelationshipDescription
diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java
index ef053c578..ad08a68d9 100644
--- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java
+++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java
@@ -44,9 +44,12 @@ final class DefaultRelationshipDescription extends Association source, String fieldName, NodeDescription> target,
- Relationship.Direction direction, @Nullable NodeDescription> relationshipProperties) {
+ Relationship.Direction direction, @Nullable NodeDescription> relationshipProperties,
+ boolean cascadeUpdates) {
// the immutable obverse association-wise is always null because we cannot determine them on both sides
// if we consider to support bidirectional relationships.
@@ -60,6 +63,7 @@ final class DefaultRelationshipDescription extends Association, String> EXISTING_IDS = new HashMap<>();
+
+ @BeforeAll
+ static void clean(@Autowired Driver driver) {
+
+ EXISTING_IDS.clear();
+ driver.executableQuery("MATCH (n) DETACH DELETE n").execute();
+ for (Class extends Parent> type : List.of(PUI.class, PUE.class, PVI.class, PVE.class)) {
+ var label = type.getSimpleName();
+ var id = "";
+ var idReturn = "elementId(p) AS id";
+ var version = "";
+ if (ExternalId.class.isAssignableFrom(type)) {
+ id = "SET p.id = randomUUID()";
+ idReturn = "p.id AS id";
+ }
+ if (Versioned.class.isAssignableFrom(type)) {
+ version = "SET p.version = 1";
+
+ }
+ var newId = driver.executableQuery("""
+ WITH 'ParentDB' AS name
+ CREATE (p:%s {id: randomUUID(), name: name})
+ %s
+ %s
+ CREATE (p) -[:HAS_SINGLE_CUI]-> (sCUI:CUI {name: name + '.singleCUI'})
+ CREATE (p) -[:HAS_SINGLE_CUE]-> (sCUE:CUE {name: name + '.singleCUE', id: randomUUID()})
+ CREATE (p) -[:HAS_MANY_CUI]-> (mCUI1:CUI {name: name + '.cUI1'})
+ CREATE (p) -[:HAS_MANY_CUI]-> (mCUI2:CUI {name: name + '.cUI2'})
+ CREATE (p) -[:HAS_SINGLE_CVI]-> (sCVI:CVI {name: name + '.singleCVI', version: 0})
+ CREATE (p) -[:HAS_SINGLE_CVE]-> (sCVE:CVE {name: name + '.singleCVE', version: 0, id: randomUUID()})
+ CREATE (p) -[:HAS_MANY_CVI]-> (mCVI1:CVI {name: name + '.cVI1', version: 0})
+ CREATE (p) -[:HAS_MANY_CVI]-> (mCVI2:CVI {name: name + '.cVI2', version: 0})
+ CREATE (sCUI) -[:HAS_NESTED_CHILDREN]-> (:CUI {name: name + '.singleCUI.c1'})
+ CREATE (sCUI) -[:HAS_NESTED_CHILDREN]-> (:CUI {name: name + '.singleCUI.c2'})
+ CREATE (mCUI1) -[:HAS_NESTED_CHILDREN]-> (:CUI {name: name + '.cUI1.cc1'})
+ CREATE (mCUI1) -[:HAS_NESTED_CHILDREN]-> (:CUI {name: name + '.cUI1.cc2'})
+ CREATE (mCUI2) -[:HAS_NESTED_CHILDREN]-> (:CUI {name: name + '.cUI2.cc1'})
+ CREATE (mCUI2) -[:HAS_NESTED_CHILDREN]-> (:CUI {name: name + '.cUI2.cc2'})
+ RETURN %s
+ """.formatted(label, id, version, idReturn)).execute().records().get(0).get("id").asString();
+ EXISTING_IDS.put(type, newId);
+ }
+ }
+
+
+ void assertAllRelationshipsHaveBeenCreated(T instance) {
+
+ var type = instance.getClass();
+ try (var session = driver.session()) {
+ var result = session.run("""
+ MATCH (p:%s WHERE %s)
+ MATCH (p) -[:HAS_SINGLE_CUI]-> (sCUI)
+ MATCH (p) -[:HAS_SINGLE_CUE]-> (sCUE)
+ MATCH (p) -[:HAS_MANY_CUI]-> (mCUI)
+ MATCH (p) -[:HAS_SINGLE_CVI]-> (sCVI {version: 0})
+ MATCH (p) -[:HAS_SINGLE_CVE]-> (sCVE {version: 0})
+ MATCH (p) -[:HAS_MANY_CVI]-> (mCVI {version: 0})
+ MATCH (sCUI) -[:HAS_NESTED_CHILDREN]-> (nc1)
+ MATCH (mCUI) -[:HAS_NESTED_CHILDREN]-> (nc2)
+ RETURN p, sCUI, sCUE, collect(DISTINCT mCUI) AS mCUI, collect(DISTINCT nc1) AS nc1, collect(DISTINCT nc2) AS nc2,
+ sCVI, sCVE, collect(DISTINCT mCVI) AS mCVI
+ """.formatted(type.getSimpleName(), instance instanceof ExternalId ? "p.id = $id" : "elementId(p) = $id"), Map.of("id", instance.getId()))
+ .list();
+
+ assertThat(result).hasSize(1).element(0)
+ .satisfies(r -> {
+ if (instance instanceof Versioned) {
+ assertThat(r.get("p").asNode().get("version").asLong()).isZero();
+ }
+ if (instance instanceof ExternalId) {
+ assertThat(r.get("p").asNode().get("id").asString()).isEqualTo(instance.getId());
+ } else {
+ assertThat(r.get("p").asNode().elementId()).isEqualTo(instance.getId());
+ }
+ assertThat(r.get("sCUI").hasType(TypeSystem.getDefault().NODE())).isTrue();
+ assertThat(r.get("sCUE").hasType(TypeSystem.getDefault().NODE())).isTrue();
+ assertThat(r.get("mCUI").asList(v -> v.asNode().get("name").asString()))
+ .containsExactlyInAnyOrder("Parent.cUI1", "Parent.cUI2");
+ assertThat(r.get("nc1").asList(v -> v.asNode().get("name").asString()))
+ .containsExactlyInAnyOrder("Parent.singleCUI.cc1", "Parent.singleCUI.cc2");
+ assertThat(r.get("nc2").asList(v -> v.asNode().get("name").asString()))
+ .containsExactlyInAnyOrder("Parent.cUI1.cc1", "Parent.cUI1.cc2", "Parent.cUI2.cc1", "Parent.cUI2.cc2");
+ assertThat(r.get("sCVI").asNode().get("version").asLong()).isZero();
+ assertThat(r.get("sCVE").asNode().get("version").asLong()).isZero();
+ assertThat(r.get("mCVI").asList(v -> {
+ var node = v.asNode();
+ return node.get("name").asString() + "." + node.get("version").asLong();
+ }))
+ .containsExactlyInAnyOrder("Parent.cVI1.0", "Parent.cVI2.0");
+ });
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java
new file mode 100644
index 000000000..d76c8c395
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUE.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+import org.springframework.data.annotation.PersistenceCreator;
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Relationship;
+import org.springframework.data.neo4j.core.support.UUIDStringGenerator;
+
+/**
+ * Children / Unversioned / Externally generated id
+ */
+public class CUE implements ExternalId {
+
+ @Id
+ @GeneratedValue(UUIDStringGenerator.class)
+ private String id;
+
+ private String name;
+
+ @Relationship("HAS_NESTED_CHILDREN")
+ private List nested;
+
+ public CUE(String name) {
+ this.name = name;
+ this.nested = List.of(
+ new CUE(name + ".cc1", List.of()),
+ new CUE(name + ".cc2", List.of())
+ );
+ }
+
+ @PersistenceCreator
+ public CUE(String name, List nested) {
+ this.name = name;
+ this.nested = nested;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java
new file mode 100644
index 000000000..20ef3a1d0
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CUI.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+import org.springframework.data.annotation.PersistenceCreator;
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Relationship;
+
+/**
+ * Children / Unversioned / Internally generated id
+ */
+public class CUI {
+
+ @Id
+ @GeneratedValue
+ private String id;
+
+ private String name;
+
+ @Relationship(value = "HAS_NESTED_CHILDREN", cascadeUpdates = false)
+ private List nested;
+
+ public CUI(String name) {
+ this.name = name;
+ this.nested = List.of(
+ new CUI(name + ".cc1", List.of()),
+ new CUI(name + ".cc2", List.of())
+ );
+ }
+
+ @PersistenceCreator
+ public CUI(String name, List nested) {
+ this.name = name;
+ this.nested = nested;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getNested() {
+ return nested;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java
new file mode 100644
index 000000000..fce8b9201
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVE.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import org.springframework.data.annotation.Version;
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.support.UUIDStringGenerator;
+
+/**
+ * Children / Unversioned / Externally generated id
+ */
+public class CVE implements Versioned, ExternalId {
+
+ @Id
+ @GeneratedValue(UUIDStringGenerator.class)
+ private String id;
+
+ @Version
+ private Long version;
+
+ private String name;
+
+ public CVE(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Long getVersion() {
+ return version;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java
new file mode 100644
index 000000000..152a882fb
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CVI.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import org.springframework.data.annotation.Version;
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+
+/**
+ * Children / Unversioned / Internally generated id
+ */
+public class CVI implements Versioned {
+
+ @Id
+ @GeneratedValue
+ private String id;
+
+ @Version
+ private Long version;
+
+ private String name;
+
+ public CVI(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Long getVersion() {
+ return version;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java
new file mode 100644
index 000000000..2bbcc8550
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/CascadingIT.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+import org.junitpioneer.jupiter.cartesian.CartesianTest;
+import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
+import org.neo4j.driver.Driver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+import org.springframework.data.neo4j.test.Neo4jExtension;
+import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
+import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@Neo4jIntegrationTest
+@Import(CascadingIT.Config.class)
+class CascadingIT extends AbstractCascadingTestBase {
+
+ protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
+
+ @EnableTransactionManagement
+ @ComponentScan
+ static class Config extends Neo4jImperativeTestConfiguration {
+
+ @Bean
+ public Driver driver() {
+ return neo4jConnectionSupport.getDriver();
+ }
+
+ @Override
+ public boolean isCypher5Compatible() {
+ return neo4jConnectionSupport.isCypher5SyntaxCompatible();
+ }
+ }
+
+ @Autowired
+ Neo4jTemplate template;
+
+ @CartesianTest
+ void updatesMustNotCascade(
+ @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type,
+ @Values(booleans = {true, false}) boolean single) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
+
+ var id = EXISTING_IDS.get(type);
+ var instance = this.template.findById(id, type).orElseThrow();
+
+ instance.setName("Updated parent");
+ instance.getSingleCUE().setName("Updated single CUE");
+ instance.getSingleCUI().setName("Updated single CUI");
+ instance.getManyCUI().forEach(cui -> {
+ cui.setName(cui.getName() + ".updatedNested1");
+ cui.getNested().forEach(nested -> nested.setName(nested + ".updatedNested2"));
+ });
+
+ if (single) {
+ this.template.save(instance);
+ } else {
+ this.template.saveAll(List.of(instance, type.getDeclaredConstructor(String.class).newInstance("Parent2")));
+ }
+
+ // Can't assert on the instance above, as that would ofc be the purposefully modified state
+ var reloadedInstance = this.template.findById(id, type).orElseThrow();
+ assertThat(reloadedInstance.getName()).isEqualTo("Updated parent");
+
+ assertThat(reloadedInstance.getSingleCUE().getName()).isEqualTo("ParentDB.singleCUE");
+ assertThat(reloadedInstance.getSingleCUI().getName()).isEqualTo("ParentDB.singleCUI");
+ assertThat(reloadedInstance.getSingleCVE().getVersion()).isZero();
+ assertThat(reloadedInstance.getSingleCVI().getVersion()).isZero();
+ assertThat(reloadedInstance.getManyCUI()).allMatch(cui -> cui.getName().endsWith(".updatedNested1") && cui.getNested().stream().noneMatch(nested -> nested.getName().endsWith(".updatedNested2")));
+ assertThat(reloadedInstance.getManyCVI()).allMatch(cvi -> cvi.getVersion() == 0L);
+ }
+
+ @CartesianTest
+ void newItemsMustBePersistedRegardlessOfCascadeSingleSave(
+ @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type,
+ @Values(booleans = {true, false}) boolean single) throws Exception {
+
+ T instance;
+ if (single) {
+ instance = template.save(type.getDeclaredConstructor(String.class).newInstance("Parent"));
+ } else {
+ instance = template.saveAll(List.of(type.getDeclaredConstructor(String.class).newInstance("Parent"), type.getDeclaredConstructor(String.class).newInstance("Parent2"))).get(0);
+ }
+
+ assertAllRelationshipsHaveBeenCreated(instance);
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java
new file mode 100644
index 000000000..785e8aa1f
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/ExternalId.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+/**
+ * Marker for external id.
+ */
+public interface ExternalId {
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java
new file mode 100644
index 000000000..d9279c16a
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUE.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import org.springframework.data.neo4j.core.schema.Relationship;
+import org.springframework.data.neo4j.core.support.UUIDStringGenerator;
+
+/**
+ * Parent / Unversioned / Externally generated id
+ */
+@Node
+public class PUE implements Parent, ExternalId {
+
+ @Id
+ @GeneratedValue(UUIDStringGenerator.class)
+ private String id;
+
+ private String name;
+
+ @Relationship(value = "HAS_SINGLE_CUI", cascadeUpdates = false)
+ private CUI singleCUI;
+
+ @Relationship(value = "HAS_SINGLE_CUE", cascadeUpdates = false)
+ private CUE singleCUE;
+
+ @Relationship("HAS_MANY_CUI")
+ private List manyCUI;
+
+ @Relationship(value = "HAS_SINGLE_CVI", cascadeUpdates = false)
+ private CVI singleCVI;
+
+ @Relationship(value = "HAS_SINGLE_CVE", cascadeUpdates = false)
+ private CVE singleCVE;
+
+ @Relationship(value = "HAS_MANY_CVI", cascadeUpdates = false)
+ private List manyCVI;
+
+ public PUE(String name) {
+ this.name = name;
+ this.singleCUI = new CUI(name + ".singleCUI");
+ this.singleCUE = new CUE(name + ".singleCUE");
+ this.manyCUI = List.of(
+ new CUI(name + ".cUI1"),
+ new CUI(name + ".cUI2")
+ );
+ this.singleCVI = new CVI(name + ".singleCVI");
+ this.singleCVE = new CVE(name + ".singleCVE");
+ this.manyCVI = List.of(
+ new CVI(name + ".cVI1"),
+ new CVI(name + ".cVI2")
+ );
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public List getManyCUI() {
+ return manyCUI;
+ }
+
+ @Override
+ public List getManyCVI() {
+ return manyCVI;
+ }
+
+ @Override
+ public CUE getSingleCUE() {
+ return singleCUE;
+ }
+
+ @Override
+ public CUI getSingleCUI() {
+ return singleCUI;
+ }
+
+ @Override
+ public CVE getSingleCVE() {
+ return singleCVE;
+ }
+
+ @Override
+ public CVI getSingleCVI() {
+ return singleCVI;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java
new file mode 100644
index 000000000..e8640a0da
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PUI.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import org.springframework.data.neo4j.core.schema.Relationship;
+
+/**
+ * Parent / Unversioned / Internally generated id
+ */
+@Node
+public class PUI implements Parent {
+
+ @Id
+ @GeneratedValue
+ private String id;
+
+ private String name;
+
+ @Relationship(value = "HAS_SINGLE_CUI", cascadeUpdates = false)
+ private CUI singleCUI;
+
+ @Relationship(value = "HAS_SINGLE_CUE", cascadeUpdates = false)
+ private CUE singleCUE;
+
+ @Relationship("HAS_MANY_CUI")
+ private List manyCUI;
+
+ @Relationship(value = "HAS_SINGLE_CVI", cascadeUpdates = false)
+ private CVI singleCVI;
+
+ @Relationship(value = "HAS_SINGLE_CVE", cascadeUpdates = false)
+ private CVE singleCVE;
+
+ @Relationship(value = "HAS_MANY_CVI", cascadeUpdates = false)
+ private List manyCVI;
+
+ public PUI(String name) {
+ this.name = name;
+ this.singleCUI = new CUI(name + ".singleCUI");
+ this.singleCUE = new CUE(name + ".singleCUE");
+ this.manyCUI = List.of(
+ new CUI(name + ".cUI1"),
+ new CUI(name + ".cUI2")
+ );
+ this.singleCVI = new CVI(name + ".singleCVI");
+ this.singleCVE = new CVE(name + ".singleCVE");
+ this.manyCVI = List.of(
+ new CVI(name + ".cVI1"),
+ new CVI(name + ".cVI2")
+ );
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public List getManyCUI() {
+ return manyCUI;
+ }
+
+ @Override
+ public List getManyCVI() {
+ return manyCVI;
+ }
+
+ @Override
+ public CUE getSingleCUE() {
+ return singleCUE;
+ }
+
+ @Override
+ public CUI getSingleCUI() {
+ return singleCUI;
+ }
+
+ @Override
+ public CVE getSingleCVE() {
+ return singleCVE;
+ }
+
+ @Override
+ public CVI getSingleCVI() {
+ return singleCVI;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java
new file mode 100644
index 000000000..7b19bdfb4
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVE.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+import org.springframework.data.annotation.Version;
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import org.springframework.data.neo4j.core.schema.Relationship;
+import org.springframework.data.neo4j.core.support.UUIDStringGenerator;
+
+/**
+ * Parent / Versioned / Externally generated id
+ */
+@Node
+public class PVE implements Parent, Versioned, ExternalId {
+
+ @Id
+ @GeneratedValue(UUIDStringGenerator.class)
+ private String id;
+
+ @Version
+ private Long version;
+
+ private String name;
+
+ @Relationship(value = "HAS_SINGLE_CUI", cascadeUpdates = false)
+ private CUI singleCUI;
+
+ @Relationship(value = "HAS_SINGLE_CUE", cascadeUpdates = false)
+ private CUE singleCUE;
+
+ @Relationship("HAS_MANY_CUI")
+ private List manyCUI;
+
+ @Relationship(value = "HAS_SINGLE_CVI", cascadeUpdates = false)
+ private CVI singleCVI;
+
+ @Relationship(value = "HAS_SINGLE_CVE", cascadeUpdates = false)
+ private CVE singleCVE;
+
+ @Relationship(value = "HAS_MANY_CVI", cascadeUpdates = false)
+ private List manyCVI;
+
+ public PVE(String name) {
+ this.name = name;
+ this.singleCUI = new CUI(name + ".singleCUI");
+ this.singleCUE = new CUE(name + ".singleCUE");
+ this.manyCUI = List.of(
+ new CUI(name + ".cUI1"),
+ new CUI(name + ".cUI2")
+ );
+ this.singleCVI = new CVI(name + ".singleCVI");
+ this.singleCVE = new CVE(name + ".singleCVE");
+ this.manyCVI = List.of(
+ new CVI(name + ".cVI1"),
+ new CVI(name + ".cVI2")
+ );
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Long getVersion() {
+ return version;
+ }
+
+ @Override
+ public List getManyCUI() {
+ return manyCUI;
+ }
+
+ @Override
+ public List getManyCVI() {
+ return manyCVI;
+ }
+
+ @Override
+ public CUE getSingleCUE() {
+ return singleCUE;
+ }
+
+ @Override
+ public CUI getSingleCUI() {
+ return singleCUI;
+ }
+
+ @Override
+ public CVE getSingleCVE() {
+ return singleCVE;
+ }
+
+ @Override
+ public CVI getSingleCVI() {
+ return singleCVI;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java
new file mode 100644
index 000000000..18660725d
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/PVI.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+import org.springframework.data.annotation.Version;
+import org.springframework.data.neo4j.core.schema.GeneratedValue;
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import org.springframework.data.neo4j.core.schema.Relationship;
+
+/**
+ * Parent / Versioned / Internally generated id
+ */
+@Node
+public class PVI implements Parent, Versioned {
+
+ @Id
+ @GeneratedValue
+ private String id;
+
+ @Version
+ private Long version;
+
+ private String name;
+
+ @Relationship(value = "HAS_SINGLE_CUI", cascadeUpdates = false)
+ private CUI singleCUI;
+
+ @Relationship(value = "HAS_SINGLE_CUE", cascadeUpdates = false)
+ private CUE singleCUE;
+
+ @Relationship("HAS_MANY_CUI")
+ private List manyCUI;
+
+ @Relationship(value = "HAS_SINGLE_CVI", cascadeUpdates = false)
+ private CVI singleCVI;
+
+ @Relationship(value = "HAS_SINGLE_CVE", cascadeUpdates = false)
+ private CVE singleCVE;
+
+ @Relationship(value = "HAS_MANY_CVI", cascadeUpdates = false)
+ private List manyCVI;
+
+ public PVI(String name) {
+ this.name = name;
+ this.singleCUI = new CUI(name + ".singleCUI");
+ this.singleCUE = new CUE(name + ".singleCUE");
+ this.manyCUI = List.of(
+ new CUI(name + ".cUI1"),
+ new CUI(name + ".cUI2")
+ );
+ this.singleCVI = new CVI(name + ".singleCVI");
+ this.singleCVE = new CVE(name + ".singleCVE");
+ this.manyCVI = List.of(
+ new CVI(name + ".cVI1"),
+ new CVI(name + ".cVI2")
+ );
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Long getVersion() {
+ return version;
+ }
+
+ @Override
+ public List getManyCUI() {
+ return manyCUI;
+ }
+
+ @Override
+ public List getManyCVI() {
+ return manyCVI;
+ }
+
+ @Override
+ public CUE getSingleCUE() {
+ return singleCUE;
+ }
+
+ @Override
+ public CUI getSingleCUI() {
+ return singleCUI;
+ }
+
+ @Override
+ public CVE getSingleCVE() {
+ return singleCVE;
+ }
+
+ @Override
+ public CVI getSingleCVI() {
+ return singleCVI;
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java
new file mode 100644
index 000000000..dda7a723a
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/Parent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import java.util.List;
+
+/**
+ * Marker for parent
+ */
+public interface Parent {
+
+ String getId();
+
+ String getName();
+
+ void setName(String name);
+
+ List getManyCUI();
+
+ List getManyCVI();
+
+ CUE getSingleCUE();
+
+ CUI getSingleCUI();
+
+ CVE getSingleCVE();
+
+ CVI getSingleCVI();
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java
new file mode 100644
index 000000000..3c23f940a
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/ReactiveCascadingIT.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+import org.junitpioneer.jupiter.cartesian.CartesianTest;
+import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
+import org.neo4j.driver.Driver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
+import org.springframework.data.neo4j.test.Neo4jExtension;
+import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
+import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@Neo4jIntegrationTest
+@Import(ReactiveCascadingIT.Config.class)
+class ReactiveCascadingIT extends AbstractCascadingTestBase {
+
+ protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
+
+ @EnableTransactionManagement
+ @ComponentScan
+ static class Config extends Neo4jReactiveTestConfiguration {
+
+ @Bean
+ public Driver driver() {
+ return neo4jConnectionSupport.getDriver();
+ }
+
+ @Override
+ public boolean isCypher5Compatible() {
+ return neo4jConnectionSupport.isCypher5SyntaxCompatible();
+ }
+ }
+
+ @Autowired
+ ReactiveNeo4jTemplate template;
+
+ @CartesianTest
+ void updatesMustNotCascade(
+ @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type,
+ @Values(booleans = {true, false}) boolean single) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
+
+ var id = EXISTING_IDS.get(type);
+ var instance = this.template.findById(id, type).single().block();
+
+ instance.setName("Updated parent");
+ instance.getSingleCUE().setName("Updated single CUE");
+ instance.getSingleCUI().setName("Updated single CUI");
+ instance.getManyCUI().forEach(cui -> {
+ cui.setName(cui.getName() + ".updatedNested1");
+ cui.getNested().forEach(nested -> nested.setName(nested + ".updatedNested2"));
+ });
+
+ if (single) {
+ this.template.save(instance).block();
+ } else {
+ this.template.saveAll(List.of(instance, type.getDeclaredConstructor(String.class).newInstance("Parent2"))).collectList().block();
+ }
+
+ // Can't assert on the instance above, as that would ofc be the purposefully modified state
+ var reloadedInstance = this.template.findById(id, type).singleOptional().block().orElseThrow();
+ assertThat(reloadedInstance.getName()).isEqualTo("Updated parent");
+
+ assertThat(reloadedInstance.getSingleCUE().getName()).isEqualTo("ParentDB.singleCUE");
+ assertThat(reloadedInstance.getSingleCUI().getName()).isEqualTo("ParentDB.singleCUI");
+ assertThat(reloadedInstance.getSingleCVE().getVersion()).isZero();
+ assertThat(reloadedInstance.getSingleCVI().getVersion()).isZero();
+ assertThat(reloadedInstance.getManyCUI()).allMatch(cui -> cui.getName().endsWith(".updatedNested1") && cui.getNested().stream().noneMatch(nested -> nested.getName().endsWith(".updatedNested2")));
+ assertThat(reloadedInstance.getManyCVI()).allMatch(cvi -> cvi.getVersion() == 0L);
+ }
+
+ @CartesianTest
+ void newItemsMustBePersistedRegardlessOfCascadeSingleSave(
+ @Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class type,
+ @Values(booleans = {true, false}) boolean single) throws Exception {
+
+ T instance;
+ if (single) {
+ instance = template.save(type.getDeclaredConstructor(String.class).newInstance("Parent")).block();
+ } else {
+ instance = template.saveAll(List.of(type.getDeclaredConstructor(String.class).newInstance("Parent"), type.getDeclaredConstructor(String.class).newInstance("Parent2")))
+ .collectList()
+ .block()
+ .get(0);
+ }
+
+ assertAllRelationshipsHaveBeenCreated(instance);
+ }
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java
new file mode 100644
index 000000000..9005fe9cb
--- /dev/null
+++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/Versioned.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2011-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.neo4j.integration.cascading;
+
+/**
+ * Marker for versioned
+ */
+public interface Versioned {
+
+ Long getVersion();
+}
diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java
index 1060be879..cbea2b87d 100644
--- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java
+++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/compose_as_ids/CompositeIdsIT.java
@@ -97,8 +97,7 @@ class CompositeIdsIT {
@Test
void compositeIdsShouldWork(@Autowired ThingWithCompositeIdRepository repository) {
- ThingWithCompositeId thing = new ThingWithCompositeId(new CompositeValue("a,", 1), "first entity");
- ThingWithCompositeId saved = repository.save(thing);
+ ThingWithCompositeId saved = repository.save(new ThingWithCompositeId(new CompositeValue("a,", 1), "first entity"));
assertThat(saved.getVersion()).isGreaterThanOrEqualTo(0);
saved.setName("foobar");