From 349d640250f50639ec3eaacbfead5510cccc002e Mon Sep 17 00:00:00 2001 From: yangyaofei Date: Thu, 23 Jan 2025 16:40:04 +0800 Subject: [PATCH] GH-2973: Persist type information on relationship properties (#2974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional parameter to `@RelationshipProperties` that allows users to persist the type information on this relationship. This will allow SDN to detect the right class it needs to instantiate if, due to abstraction of the implementation, no other information is available. Closes #2973 Signed-off-by: 杨耀飞 Signed-off-by: yangyaofei Signed-off-by: Gerrit Meier Co-authored-by: Gerrit Meier (cherry picked from commit 7490d99096022aff7cb2e07c1904d66ca1df9cec) --- .../data/neo4j/core/mapping/Constants.java | 5 ++ .../mapping/DefaultNeo4jEntityConverter.java | 13 ++++ .../mapping/DefaultNeo4jPersistentEntity.java | 8 ++ .../core/mapping/Neo4jPersistentEntity.java | 7 ++ .../core/schema/RelationshipProperties.java | 8 ++ .../neo4j/integration/issues/IssuesIT.java | 75 +++++++++++++++++++ .../integration/issues/gh2973/BaseNode.java | 55 ++++++++++++++ .../issues/gh2973/BaseRelationship.java | 49 ++++++++++++ .../issues/gh2973/Gh2973Repository.java | 28 +++++++ .../issues/gh2973/RelationshipA.java | 34 +++++++++ .../issues/gh2973/RelationshipB.java | 34 +++++++++ .../issues/gh2973/RelationshipC.java | 34 +++++++++ .../issues/gh2973/RelationshipD.java | 34 +++++++++ 13 files changed, 384 insertions(+) create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java index b15a30c14..e87bd0684 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java @@ -69,6 +69,11 @@ public final class Constants { public static final String NAME_OF_KNOWN_RELATIONSHIPS_PARAM = "__knownRelationShipIds__"; public static final String NAME_OF_ALL_PROPERTIES = "__allProperties__"; + /** + * Optional property for relationship properties' simple class name to keep type info + */ + public static final String NAME_OF_RELATIONSHIP_TYPE = "__relationshipType__"; + public static final String NAME_OF_SYNTHESIZED_ROOT_NODE = "__sn__"; public static final String NAME_OF_SYNTHESIZED_RELATED_NODES = "__srn__"; public static final String NAME_OF_SYNTHESIZED_RELATIONS = "__sr__"; 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 c4600d700..85ba9f1a6 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 @@ -233,6 +233,10 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Neo4jPersistentEntity nodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore .getNodeDescription(source.getClass()); + if (nodeDescription.hasRelationshipPropertyPersistTypeInfoFlag()) { + // add type info when write to the database + properties.put(Constants.NAME_OF_RELATIONSHIP_TYPE, nodeDescription.getPrimaryLabel()); + } PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source); PropertyHandlerSupport.of(nodeDescription).doWithProperties((Neo4jPersistentProperty p) -> { @@ -427,6 +431,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { /** * Returns the list of labels for the entity to be created from the "main" node returned. + * In case of a relationship that maps to a relationship properties definition, + * return the optional persisted type. * * @param queryResult The complete query result * @return The list of labels defined by the query variable {@link Constants#NAME_OF_LABELS}. @@ -440,6 +446,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } else if (queryResult instanceof Node) { Node nodeRepresentation = (Node) queryResult; nodeRepresentation.labels().forEach(labels::add); + } else if (queryResult instanceof Relationship) { + Value value = queryResult.get(Constants.NAME_OF_RELATIONSHIP_TYPE); + if (value.isNull()) { + labels.addAll(nodeDescription.getStaticLabels()); + } else { + labels.add(value.asString()); + } } else if (containsOnePlainNode(queryResult)) { for (Value value : queryResult.values()) { if (value.hasType(nodeType)) { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index c9ac515f1..d5df9f90a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -185,6 +185,14 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity */ boolean isRelationshipPropertiesEntity(); + /** + * Determines if the entity is annotated with {@link org.springframework.data.neo4j.core.schema.RelationshipProperties} + * and has the flag {@link org.springframework.data.neo4j.core.schema.RelationshipProperties#persistTypeInfo()} set to true. + * @return true if this is a relationship properties class and the type info should be persisted, otherwise false. + */ + boolean hasRelationshipPropertyPersistTypeInfoFlag(); + /** * @return True if the underlying domain classes uses {@code id()} to compute internally generated ids. */ diff --git a/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java b/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java index d575ecfc2..a2b9bd641 100644 --- a/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java +++ b/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java @@ -52,4 +52,12 @@ import org.apiguardian.api.API; @Inherited @API(status = API.Status.STABLE, since = "6.0") public @interface RelationshipProperties { + /** + * Set to true will persist {@link org.springframework.data.neo4j.core.mapping.Constants#NAME_OF_RELATIONSHIP_TYPE} to {@link Class#getSimpleName()} + * as a property in relationships. This property will be used to determine the type of the relationship + * when mapping back to the domain model. + * + * @return whether to persist type information for the annotated class. + */ + boolean persistTypeInfo() default false; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java index 860998747..5dd9aebbc 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java @@ -189,6 +189,13 @@ import org.springframework.data.neo4j.integration.issues.gh2918.ConditionNode; import org.springframework.data.neo4j.integration.issues.gh2918.ConditionRepository; import org.springframework.data.neo4j.integration.issues.gh2963.MyModel; import org.springframework.data.neo4j.integration.issues.gh2963.MyRepository; +import org.springframework.data.neo4j.integration.issues.gh2973.BaseNode; +import org.springframework.data.neo4j.integration.issues.gh2973.BaseRelationship; +import org.springframework.data.neo4j.integration.issues.gh2973.Gh2973Repository; +import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipA; +import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipB; +import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipC; +import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipD; import org.springframework.data.neo4j.integration.issues.qbe.A; import org.springframework.data.neo4j.integration.issues.qbe.ARepository; import org.springframework.data.neo4j.integration.issues.qbe.B; @@ -1698,6 +1705,72 @@ class IssuesIT extends TestBase { assertThat(rootModelFromDbCustom).map(MyModel::getMyNestedModel).isPresent(); } + @Tag("GH-2973") + @Test + void abstractedRelationshipTypesShouldBeMappedCorrectly(@Autowired Gh2973Repository gh2973Repository) { + var node = new BaseNode(); + var nodeFail = new BaseNode(); + RelationshipA a1 = new RelationshipA(); + RelationshipA a2 = new RelationshipA(); + RelationshipA a3 = new RelationshipA(); + RelationshipB b1 = new RelationshipB(); + RelationshipB b2 = new RelationshipB(); + RelationshipC c1 = new RelationshipC(); + RelationshipD d1 = new RelationshipD(); + + a1.setTargetNode(new BaseNode()); + a1.setA("a1"); + a2.setTargetNode(new BaseNode()); + a2.setA("a2"); + a3.setTargetNode(new BaseNode()); + a3.setA("a3"); + + b1.setTargetNode(new BaseNode()); + b1.setB("b1"); + b2.setTargetNode(new BaseNode()); + b2.setB("b2"); + + c1.setTargetNode(new BaseNode()); + c1.setC("c1"); + + d1.setTargetNode(new BaseNode()); + d1.setD("d1"); + + node.setRelationships(Map.of( + "a", List.of( + a1, a2, b2 + ), + "b", List.of( + b1, a3 + ) + )); + nodeFail.setRelationships(Map.of( + "c", List.of( + c1, d1 + ) + )); + var persistedNode = gh2973Repository.save(node); + var persistedNodeFail = gh2973Repository.save(nodeFail); + + // with type info, the relationships are of the correct type + var loadedNode = gh2973Repository.findById(persistedNode.getId()).get(); + List relationshipsA = loadedNode.getRelationships().get("a"); + List relationshipsB = loadedNode.getRelationships().get("b"); + assertThat(relationshipsA).satisfiesExactlyInAnyOrder( + r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class), + r2 -> assertThat(r2).isOfAnyClassIn(RelationshipA.class), + r3 -> assertThat(r3).isOfAnyClassIn(RelationshipB.class) + ); + assertThat(relationshipsB).satisfiesExactlyInAnyOrder( + r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class), + r2 -> assertThat(r2).isOfAnyClassIn(RelationshipB.class) + ); + // without type info, the relationships are all same type and not the base class BaseRelationship + var loadedNodeFail = gh2973Repository.findById(persistedNodeFail.getId()).get(); + List relationshipsCFail = loadedNodeFail.getRelationships().get("c"); + assertThat(relationshipsCFail.get(0)).isNotExactlyInstanceOf(BaseRelationship.class); + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties") @@ -1855,4 +1928,6 @@ class IssuesIT extends TestBase { return repository.save(n1); } + + } diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java new file mode 100644 index 000000000..91ac2899e --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseNode.java @@ -0,0 +1,55 @@ +/* + * Copyright 2011-2025 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.gh2973; + +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 java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + + +/** + * @author yangyaofei + */ +@Node +public class BaseNode { + @Id + @GeneratedValue + private UUID id; + @Relationship(direction = Relationship.Direction.OUTGOING) + private Map> relationships = new HashMap<>(); + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public Map> getRelationships() { + return relationships; + } + + public void setRelationships(Map> relationships) { + this.relationships = relationships; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java new file mode 100644 index 000000000..b3d3bb153 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/BaseRelationship.java @@ -0,0 +1,49 @@ +/* + * Copyright 2011-2025 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.gh2973; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.RelationshipId; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +/** + * @author yangyaofei + */ +@RelationshipProperties +public abstract class BaseRelationship { + @RelationshipId + @GeneratedValue + private Long id; + @TargetNode + private BaseNode targetNode; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public BaseNode getTargetNode() { + return targetNode; + } + + public void setTargetNode(BaseNode targetNode) { + this.targetNode = targetNode; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java new file mode 100644 index 000000000..42a03cfee --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/Gh2973Repository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2011-2025 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.gh2973; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import java.util.UUID; + +/** + * Test repository for GH-2973 + * + * @author yangyaofei + */ +public interface Gh2973Repository extends Neo4jRepository { +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java new file mode 100644 index 000000000..22b026e9c --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipA.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011-2025 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.gh2973; + +import org.springframework.data.neo4j.core.schema.RelationshipProperties; + +/** + * @author yangyaofei + */ +@RelationshipProperties(persistTypeInfo = true) +public class RelationshipA extends BaseRelationship { + String a; + + public String getA() { + return a; + } + + public void setA(String a) { + this.a = a; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java new file mode 100644 index 000000000..1243a159a --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipB.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011-2025 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.gh2973; + +import org.springframework.data.neo4j.core.schema.RelationshipProperties; + +/** + * @author yangyaofei + */ +@RelationshipProperties(persistTypeInfo = true) +public class RelationshipB extends BaseRelationship { + String b; + + public String getB() { + return b; + } + + public void setB(String b) { + this.b = b; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java new file mode 100644 index 000000000..9172778f3 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipC.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011-2025 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.gh2973; + +import org.springframework.data.neo4j.core.schema.RelationshipProperties; + +/** + * @author yangyaofei + */ +@RelationshipProperties +public class RelationshipC extends BaseRelationship { + String c; + + public String getC() { + return c; + } + + public void setC(String c) { + this.c = c; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java new file mode 100644 index 000000000..480ad6114 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2973/RelationshipD.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011-2025 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.gh2973; + +import org.springframework.data.neo4j.core.schema.RelationshipProperties; + +/** + * @author yangyaofei + */ +@RelationshipProperties +public class RelationshipD extends BaseRelationship { + String d; + + public String getD() { + return d; + } + + public void setD(String d) { + this.d = d; + } +}