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:
@@ -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__";
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user