GH-2294 - Support @ReadOnlyProperty.

Closes #2294.
This commit is contained in:
Michael Simons
2021-06-17 17:59:22 +02:00
parent 39637c034c
commit c6c5ea67bc
9 changed files with 310 additions and 10 deletions

View File

@@ -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(),

View File

@@ -725,8 +725,10 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Rea
sourceEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) 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(),

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Sku, Long> {
}
@Repository
public interface SkuRORepository extends Neo4jRepository<SkuRO, Long> {
SkuRO findOneByName(String name);
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)

View File

@@ -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;
}
}

View File

@@ -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<SkuRO> 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<Sku, Long> {
}
@Repository
public interface SkuRORepository extends ReactiveNeo4jRepository<SkuRO, Long> {
Mono<SkuRO> findOneByName(String name);
}
@Configuration
@EnableTransactionManagement
@EnableReactiveNeo4jRepositories(considerNestedRepositories = true)

View File

@@ -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<RangeRelationRO> rangeRelationsOut = new HashSet<>();
@ReadOnlyProperty
@Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.INCOMING)
private Set<RangeRelationRO> 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;
}
}

View File

@@ -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<RangeRelationRO> rangeRelationsOut = new HashSet<>();
@ReadOnlyProperty
@Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.INCOMING)
private Set<RangeRelationRO> 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;
}
}