GH-2973: Persist type information on relationship properties (#2974)

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: 杨耀飞 <yangyaofei@gmail.com>
Signed-off-by: yangyaofei <yangyaofei@gmail.com>
Signed-off-by: Gerrit Meier <meistermeier@gmail.com>
Co-authored-by: Gerrit Meier <meistermeier@gmail.com>
(cherry picked from commit 7490d99096)
This commit is contained in:
yangyaofei
2025-01-23 16:40:04 +08:00
committed by Gerrit Meier
parent 74c6e30676
commit 349d640250
13 changed files with 384 additions and 0 deletions

View File

@@ -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__";

View File

@@ -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<Object> 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)) {

View File

@@ -185,6 +185,14 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
return this.isRelationshipPropertiesEntity.get();
}
@Override
public boolean hasRelationshipPropertyPersistTypeInfoFlag() {
if (!isRelationshipPropertiesEntity()) {
return false;
}
return getRequiredAnnotation(RelationshipProperties.class).persistTypeInfo();
}
/*
* (non-Javadoc)
* @see BasicPersistentEntity#getFallbackIsNewStrategy()

View File

@@ -56,6 +56,13 @@ public interface Neo4jPersistentEntity<T>
*/
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.
*/

View File

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

View File

@@ -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<BaseRelationship> relationshipsA = loadedNode.getRelationships().get("a");
List<BaseRelationship> 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<BaseRelationship> 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);
}
}

View File

@@ -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<String, List<BaseRelationship>> relationships = new HashMap<>();
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public Map<String, List<BaseRelationship>> getRelationships() {
return relationships;
}
public void setRelationships(Map<String, List<BaseRelationship>> relationships) {
this.relationships = relationships;
}
}

View File

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

View File

@@ -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<BaseNode, UUID> {
}

View File

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

View File

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

View File

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

View File

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