feat: Allow cascading updates to be disabled. (#2897)
This adds a boolean attribute `cascadeUpdates` to `@Relationship`, selectively preventing the cascade of updates. This attribute will be `false` by default. It does not have an effect when storing new entities. It does not affect the deletion of relationships. It does not affect the storing of relationships with or without properties. Be aware that with a non-cascading update, you can bring your aggregate root in a state in which it is no longer in sync with the actual state of it in the graph. Thanks to @shanon84 for valuable input. Closes #2604
This commit is contained in:
11
pom.xml
11
pom.xml
@@ -114,6 +114,7 @@
|
||||
<skipUnitTests>${skipTests}</skipUnitTests>
|
||||
|
||||
<springdata.commons>3.3.0-SNAPSHOT</springdata.commons>
|
||||
<junit-pioneer.version>2.2.0</junit-pioneer.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -141,6 +142,11 @@
|
||||
<version>${junit-cc-testcontainer}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit-pioneer</groupId>
|
||||
<artifactId>junit-pioneer</artifactId>
|
||||
<version>${junit-pioneer.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.classgraph</groupId>
|
||||
<artifactId>classgraph</artifactId>
|
||||
@@ -441,6 +447,11 @@
|
||||
<artifactId>blockhound</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit-pioneer</groupId>
|
||||
<artifactId>junit-pioneer</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -969,6 +969,9 @@ public final class ReactiveNeo4jTemplate implements
|
||||
Flux<RelationshipHandler> 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<Tuple2<AtomicReference<Object>, AtomicReference<Entity>>> 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<Entity> 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<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();
|
||||
}
|
||||
|
||||
private Mono<Entity> saveRelatedNode(Object relatedNode, Neo4jPersistentEntity<?> targetNodeDescription, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) {
|
||||
|
||||
return determineDynamicLabels(relatedNode, targetNodeDescription)
|
||||
|
||||
@@ -756,6 +756,12 @@ public enum CypherGenerator {
|
||||
return returnExpressions;
|
||||
}
|
||||
|
||||
|
||||
public StatementBuilder.OngoingReading prepareFindOf(NodeDescription<?> nodeDescription, @Nullable List<PatternElement> 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<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships) {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,9 +44,12 @@ final class DefaultRelationshipDescription extends Association<Neo4jPersistentPr
|
||||
|
||||
private RelationshipDescription relationshipObverse;
|
||||
|
||||
private final boolean cascadeUpdates;
|
||||
|
||||
DefaultRelationshipDescription(Neo4jPersistentProperty inverse, @Nullable RelationshipDescription relationshipObverse,
|
||||
String type, boolean dynamic, NodeDescription<?> 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<Neo4jPersistentPr
|
||||
this.target = target;
|
||||
this.direction = direction;
|
||||
this.relationshipPropertiesClass = relationshipProperties;
|
||||
this.cascadeUpdates = cascadeUpdates;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,6 +121,11 @@ final class DefaultRelationshipDescription extends Association<Neo4jPersistentPr
|
||||
return this.relationshipObverse != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cascadeUpdates() {
|
||||
return cascadeUpdates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultRelationshipDescription{" + "type='" + type + '\'' + ", source='" + source + '\'' + ", direction='"
|
||||
|
||||
@@ -138,4 +138,9 @@ public interface RelationshipDescription {
|
||||
* @return true if a logically same relationship in the target entity exists, otherwise false.
|
||||
*/
|
||||
boolean hasRelationshipObverse();
|
||||
|
||||
/**
|
||||
* {@return true if updates should be cascaded along this relationship}
|
||||
*/
|
||||
boolean cascadeUpdates();
|
||||
}
|
||||
|
||||
@@ -82,4 +82,13 @@ public @interface Relationship {
|
||||
* @return The direction of the relationship.
|
||||
*/
|
||||
Direction direction() default Direction.OUTGOING;
|
||||
|
||||
/**
|
||||
* Set this attribute to {@literal false} if you don't want updates on an aggregate root to be cascaded to related objects.
|
||||
* Be aware that in this case you are responsible to manually save the related objects and that you might end up with a local
|
||||
* object graph that is not in sync with the actual graph.
|
||||
*
|
||||
* @return whether updates to the owning instance should be cascaded to the related objects
|
||||
*/
|
||||
boolean cascadeUpdates() default true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
abstract class AbstractCascadingTestBase {
|
||||
|
||||
@Autowired
|
||||
Driver driver;
|
||||
|
||||
static Map<Class<? extends Parent>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<T extends Parent> 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CUE> 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<CUE> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<CUI> 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<CUI> 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<CUI> getNested() {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
<T extends Parent> void updatesMustNotCascade(
|
||||
@Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class<T> 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
|
||||
<T extends Parent> void newItemsMustBePersistedRegardlessOfCascadeSingleSave(
|
||||
@Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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<CUI> 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<CVI> 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<CUI> getManyCUI() {
|
||||
return manyCUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CVI> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<CUI> 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<CVI> 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<CUI> getManyCUI() {
|
||||
return manyCUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CVI> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<CUI> 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<CVI> 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<CUI> getManyCUI() {
|
||||
return manyCUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CVI> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<CUI> 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<CVI> 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<CUI> getManyCUI() {
|
||||
return manyCUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CVI> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<CUI> getManyCUI();
|
||||
|
||||
List<CVI> getManyCVI();
|
||||
|
||||
CUE getSingleCUE();
|
||||
|
||||
CUI getSingleCUI();
|
||||
|
||||
CVE getSingleCVE();
|
||||
|
||||
CVI getSingleCVI();
|
||||
}
|
||||
@@ -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
|
||||
<T extends Parent> void updatesMustNotCascade(
|
||||
@Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class<T> 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
|
||||
<T extends Parent> void newItemsMustBePersistedRegardlessOfCascadeSingleSave(
|
||||
@Values(classes = {PUI.class, PUE.class, PVI.class, PVE.class}) Class<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user