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 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");