From c6c5ea67bc59dd980ac28075fae53346d5827125 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 17 Jun 2021 17:59:22 +0200 Subject: [PATCH] GH-2294 - Support `@ReadOnlyProperty`. Closes #2294. --- .../data/neo4j/core/Neo4jTemplate.java | 3 + .../neo4j/core/ReactiveNeo4jTemplate.java | 6 +- .../mapping/DefaultNeo4jEntityConverter.java | 3 +- .../mapping/NestedRelationshipContext.java | 5 ++ .../integration/issues/gh2289/GH2289IT.java | 49 +++++++++++-- .../issues/gh2289/RangeRelationRO.java | 49 +++++++++++++ .../issues/gh2289/ReactiveGH2289IT.java | 63 +++++++++++++++- .../integration/issues/gh2289/SkuRO.java | 71 +++++++++++++++++++ .../issues/gh2289/SkuRO.java~Stashed changes | 71 +++++++++++++++++++ 9 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java~Stashed changes 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 08bf1264d..15d11b4a3 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -641,6 +641,9 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio // create context to bundle parameters NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor, sourceEntity); + if (relationshipContext.isReadOnly()) { + return; + } Object rawValue = relationshipContext.getValue(); Collection relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(), 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 8db4c1d2b..5105508df 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -725,8 +725,10 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Rea sourceEntity.doWithAssociations((AssociationHandler) association -> { // create context to bundle parameters - NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, parentPropertyAccessor, - sourceEntity); + NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, parentPropertyAccessor, sourceEntity); + if (relationshipContext.isReadOnly()) { + return; + } Object rawValue = relationshipContext.getValue(); Collection relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(), diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index b20b669e4..d8f5ae013 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -42,6 +42,7 @@ import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.Type; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.CollectionFactory; +import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -184,7 +185,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> { // Skip the internal properties, we don't want them to end up stored as properties - if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty()) { + if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty() || p.isAnnotationPresent(ReadOnlyProperty.class)) { return; } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java index 82e3a9e18..8609e19e9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import org.apiguardian.api.API; +import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.neo4j.core.schema.TargetNode; @@ -53,6 +54,10 @@ public final class NestedRelationshipContext { this.inverseValueIsEmpty = inverseValueIsEmpty; } + public boolean isReadOnly() { + return inverse.isAnnotationPresent(ReadOnlyProperty.class); + } + public Neo4jPersistentProperty getInverse() { return inverse; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/GH2289IT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/GH2289IT.java index b9d0619eb..b926c8768 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/GH2289IT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/GH2289IT.java @@ -15,12 +15,14 @@ */ package org.springframework.data.neo4j.integration.issues.gh2289; -import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.RepeatedTest; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,6 +49,10 @@ class GH2289IT { Transaction transaction = session.beginTransaction(); ) { transaction.run("MATCH (n) detach delete n"); + for (int i = 0; i < 4; ++i) { + transaction.run("CREATE (s:SKU_RO {number: $i, name: $n})", + Values.parameters("i", i, "n", new String(new char[] { (char) ('A' + i) }))); + } transaction.commit(); bookmarkCapture.seedWith(session.lastBookmark()); } @@ -64,20 +70,53 @@ class GH2289IT { a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); a = skuRepo.save(a); - Assertions.assertThat(a.getRangeRelationsOut()).hasSize(3); + assertThat(a.getRangeRelationsOut()).hasSize(3); b = skuRepo.findById(b.getId()).get(); - Assertions.assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsIn()).hasSize(1); b.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); b = skuRepo.save(b); - Assertions.assertThat(b.getRangeRelationsIn()).hasSize(1); - Assertions.assertThat(b.getRangeRelationsOut()).hasSize(1); + assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsOut()).hasSize(1); + } + + @RepeatedTest(5) // GH-2294 + void testNewRelationRo(@Autowired SkuRORepository skuRepo) { + SkuRO a = skuRepo.findOneByName("A"); + SkuRO b = skuRepo.findOneByName("B"); + SkuRO c = skuRepo.findOneByName("C"); + SkuRO d = skuRepo.findOneByName("D"); + + a.rangeRelationTo(b, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); + a.setName("a new name"); + a = skuRepo.save(a); + assertThat(a.getRangeRelationsOut()).hasSize(3); + assertThat(a.getName()).isEqualTo("a new name"); + + assertThat(skuRepo.findOneByName("a new name")).isNull(); + + b = skuRepo.findOneByName("B"); + assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsOut()).hasSizeLessThanOrEqualTo(1); + + b.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); + b = skuRepo.save(b); + assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsOut()).hasSize(1); } @Repository public interface SkuRepository extends Neo4jRepository { } + @Repository + public interface SkuRORepository extends Neo4jRepository { + + SkuRO findOneByName(String name); + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(considerNestedRepositories = true) diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java new file mode 100644 index 000000000..32a66b98b --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/RangeRelationRO.java @@ -0,0 +1,49 @@ +/* + * Copyright 2011-2021 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.issues.gh2289; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +/** + * @author Michael J. Simons + */ +@Data // lombok +@RelationshipProperties +public class RangeRelationRO { + + @EqualsAndHashCode.Exclude + @Id @GeneratedValue private Long id; + + @Property private double minDelta; + @Property private double maxDelta; + @Property private RelationType relationType; + + @TargetNode private SkuRO targetSku; + + public RangeRelationRO(SkuRO targetSku, double minDelta, double maxDelta, RelationType relationType) { + this.targetSku = targetSku; + this.minDelta = minDelta; + this.maxDelta = maxDelta; + this.relationType = relationType; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveGH2289IT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveGH2289IT.java index a22e7c06a..5ee8f4de2 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveGH2289IT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/ReactiveGH2289IT.java @@ -15,18 +15,21 @@ */ package org.springframework.data.neo4j.integration.issues.gh2289; +import static org.assertj.core.api.Assertions.assertThat; + +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,6 +57,10 @@ class ReactiveGH2289IT { Transaction transaction = session.beginTransaction(); ) { transaction.run("MATCH (n) detach delete n"); + for (int i = 0; i < 4; ++i) { + transaction.run("CREATE (s:SKU_RO {number: $i, name: $n})", + Values.parameters("i", i, "n", new String(new char[] { (char) ('A' + i) }))); + } transaction.commit(); bookmarkCapture.seedWith(session.lastBookmark()); } @@ -84,7 +91,7 @@ class ReactiveGH2289IT { .verifyComplete(); skuRepo.findById(bId.get()) - .doOnNext(b -> Assertions.assertThat(b.getRangeRelationsIn()).hasSize(1)) + .doOnNext(b -> assertThat(b.getRangeRelationsIn()).hasSize(1)) .flatMap(b -> { b.rangeRelationTo(cRef.get(), 1, 1, RelationType.MULTIPLICATIVE); return skuRepo.save(b); @@ -94,10 +101,62 @@ class ReactiveGH2289IT { .verifyComplete(); } + @RepeatedTest(5) // GH-2294 + void testNewRelationRo(@Autowired SkuRORepository skuRepo) { + + AtomicLong bId = new AtomicLong(); + AtomicReference cRef = new AtomicReference<>(); + skuRepo.findOneByName("A") + .zipWith(skuRepo.findOneByName("B")) + .zipWith(skuRepo.findOneByName("C")) + .zipWith(skuRepo.findOneByName("D")) + .flatMap(t -> { + SkuRO a = t.getT1().getT1().getT1(); + SkuRO b = t.getT1().getT1().getT2(); + SkuRO c = t.getT1().getT2(); + SkuRO d = t.getT2(); + + bId.set(b.getId()); + cRef.set(c); + a.rangeRelationTo(b, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(c, 1, 1, RelationType.MULTIPLICATIVE); + a.rangeRelationTo(d, 1, 1, RelationType.MULTIPLICATIVE); + + a.setName("a new name"); + + return skuRepo.save(a); + }).as(StepVerifier::create) + .expectNextMatches(a -> a.getRangeRelationsOut().size() == 3 && "a new name".equals(a.getName())) + .verifyComplete(); + + skuRepo.findOneByName("a new name") + .as(StepVerifier::create) + .verifyComplete(); + + skuRepo.findOneByName("B") + .doOnNext(b -> { + assertThat(b.getRangeRelationsIn()).hasSize(1); + assertThat(b.getRangeRelationsOut()).hasSizeLessThanOrEqualTo(1); + }) + .flatMap(b -> { + b.rangeRelationTo(cRef.get(), 1, 1, RelationType.MULTIPLICATIVE); + return skuRepo.save(b); + }) + .as(StepVerifier::create) + .expectNextMatches(b -> b.getRangeRelationsIn().size() == 1 && b.getRangeRelationsOut().size() == 1) + .verifyComplete(); + } + @Repository public interface SkuRepository extends ReactiveNeo4jRepository { } + @Repository + public interface SkuRORepository extends ReactiveNeo4jRepository { + + Mono findOneByName(String name); + } + @Configuration @EnableTransactionManagement @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java new file mode 100644 index 000000000..2c1be4e42 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java @@ -0,0 +1,71 @@ +/* + * Copyright 2011-2021 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.issues.gh2289; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.annotation.ReadOnlyProperty; +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.Property; +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Michael J. Simons + */ +@Node("SKU_RO") +@Getter // lombok +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class SkuRO { + + @Id @GeneratedValue + @EqualsAndHashCode.Include + private Long id; + + @Property("number") + @EqualsAndHashCode.Include + private Long number; + + @ReadOnlyProperty + @Property("name") + @EqualsAndHashCode.Include + private String name; + + @Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.OUTGOING) + private Set rangeRelationsOut = new HashSet<>(); + + @ReadOnlyProperty + @Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.INCOMING) + private Set rangeRelationsIn = new HashSet<>(); + + public SkuRO(Long number, String name) { + this.number = number; + this.name = name; + } + + public RangeRelationRO rangeRelationTo(SkuRO sku, double minDelta, double maxDelta, RelationType relationType) { + RangeRelationRO relationOut = new RangeRelationRO(sku, minDelta, maxDelta, relationType); + rangeRelationsOut.add(relationOut); + return relationOut; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java~Stashed changes b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java~Stashed changes new file mode 100644 index 000000000..2c1be4e42 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java~Stashed changes @@ -0,0 +1,71 @@ +/* + * Copyright 2011-2021 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.issues.gh2289; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.annotation.ReadOnlyProperty; +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.Property; +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Michael J. Simons + */ +@Node("SKU_RO") +@Getter // lombok +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class SkuRO { + + @Id @GeneratedValue + @EqualsAndHashCode.Include + private Long id; + + @Property("number") + @EqualsAndHashCode.Include + private Long number; + + @ReadOnlyProperty + @Property("name") + @EqualsAndHashCode.Include + private String name; + + @Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.OUTGOING) + private Set rangeRelationsOut = new HashSet<>(); + + @ReadOnlyProperty + @Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.INCOMING) + private Set rangeRelationsIn = new HashSet<>(); + + public SkuRO(Long number, String name) { + this.number = number; + this.name = name; + } + + public RangeRelationRO rangeRelationTo(SkuRO sku, double minDelta, double maxDelta, RelationType relationType) { + RangeRelationRO relationOut = new RangeRelationRO(sku, minDelta, maxDelta, relationType); + rangeRelationsOut.add(relationOut); + return relationOut; + } +}