Skip to content

Commit e2aeafe

Browse files
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 7490d99)
1 parent e9922eb commit e2aeafe

File tree

13 files changed

+384
-0
lines changed

13 files changed

+384
-0
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public final class Constants {
6969
public static final String NAME_OF_KNOWN_RELATIONSHIPS_PARAM = "__knownRelationShipIds__";
7070
public static final String NAME_OF_ALL_PROPERTIES = "__allProperties__";
7171

72+
/**
73+
* Optional property for relationship properties' simple class name to keep type info
74+
*/
75+
public static final String NAME_OF_RELATIONSHIP_TYPE = "__relationshipType__";
76+
7277
public static final String NAME_OF_SYNTHESIZED_ROOT_NODE = "__sn__";
7378
public static final String NAME_OF_SYNTHESIZED_RELATED_NODES = "__srn__";
7479
public static final String NAME_OF_SYNTHESIZED_RELATIONS = "__sr__";

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java

+13
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ public void write(Object source, Map<String, Object> parameters) {
233233

234234
Neo4jPersistentEntity<?> nodeDescription = (Neo4jPersistentEntity<?>) nodeDescriptionStore
235235
.getNodeDescription(source.getClass());
236+
if (nodeDescription.hasRelationshipPropertyPersistTypeInfoFlag()) {
237+
// add type info when write to the database
238+
properties.put(Constants.NAME_OF_RELATIONSHIP_TYPE, nodeDescription.getPrimaryLabel());
239+
}
236240

237241
PersistentPropertyAccessor<Object> propertyAccessor = nodeDescription.getPropertyAccessor(source);
238242
PropertyHandlerSupport.of(nodeDescription).doWithProperties((Neo4jPersistentProperty p) -> {
@@ -427,6 +431,8 @@ private Neo4jPersistentEntity<?> getMostConcreteTargetNodeDescription(
427431

428432
/**
429433
* Returns the list of labels for the entity to be created from the "main" node returned.
434+
* In case of a relationship that maps to a relationship properties definition,
435+
* return the optional persisted type.
430436
*
431437
* @param queryResult The complete query result
432438
* @return The list of labels defined by the query variable {@link Constants#NAME_OF_LABELS}.
@@ -440,6 +446,13 @@ private List<String> getLabels(MapAccessor queryResult, @Nullable NodeDescriptio
440446
} else if (queryResult instanceof Node) {
441447
Node nodeRepresentation = (Node) queryResult;
442448
nodeRepresentation.labels().forEach(labels::add);
449+
} else if (queryResult instanceof Relationship) {
450+
Value value = queryResult.get(Constants.NAME_OF_RELATIONSHIP_TYPE);
451+
if (value.isNull()) {
452+
labels.addAll(nodeDescription.getStaticLabels());
453+
} else {
454+
labels.add(value.asString());
455+
}
443456
} else if (containsOnePlainNode(queryResult)) {
444457
for (Value value : queryResult.values()) {
445458
if (value.hasType(nodeType)) {

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java

+8
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,14 @@ public boolean isRelationshipPropertiesEntity() {
185185
return this.isRelationshipPropertiesEntity.get();
186186
}
187187

188+
@Override
189+
public boolean hasRelationshipPropertyPersistTypeInfoFlag() {
190+
if (!isRelationshipPropertiesEntity()) {
191+
return false;
192+
}
193+
return getRequiredAnnotation(RelationshipProperties.class).persistTypeInfo();
194+
}
195+
188196
/*
189197
* (non-Javadoc)
190198
* @see BasicPersistentEntity#getFallbackIsNewStrategy()

src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java

+7
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ public interface Neo4jPersistentEntity<T>
5656
*/
5757
boolean isRelationshipPropertiesEntity();
5858

59+
/**
60+
* Determines if the entity is annotated with {@link org.springframework.data.neo4j.core.schema.RelationshipProperties}
61+
* and has the flag {@link org.springframework.data.neo4j.core.schema.RelationshipProperties#persistTypeInfo()} set to true.
62+
* @return true if this is a relationship properties class and the type info should be persisted, otherwise false.
63+
*/
64+
boolean hasRelationshipPropertyPersistTypeInfoFlag();
65+
5966
/**
6067
* @return True if the underlying domain classes uses {@code id()} to compute internally generated ids.
6168
*/

src/main/java/org/springframework/data/neo4j/core/schema/RelationshipProperties.java

+8
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,12 @@
5252
@Inherited
5353
@API(status = API.Status.STABLE, since = "6.0")
5454
public @interface RelationshipProperties {
55+
/**
56+
* Set to true will persist {@link org.springframework.data.neo4j.core.mapping.Constants#NAME_OF_RELATIONSHIP_TYPE} to {@link Class#getSimpleName()}
57+
* as a property in relationships. This property will be used to determine the type of the relationship
58+
* when mapping back to the domain model.
59+
*
60+
* @return whether to persist type information for the annotated class.
61+
*/
62+
boolean persistTypeInfo() default false;
5563
}

src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java

+75
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@
189189
import org.springframework.data.neo4j.integration.issues.gh2918.ConditionRepository;
190190
import org.springframework.data.neo4j.integration.issues.gh2963.MyModel;
191191
import org.springframework.data.neo4j.integration.issues.gh2963.MyRepository;
192+
import org.springframework.data.neo4j.integration.issues.gh2973.BaseNode;
193+
import org.springframework.data.neo4j.integration.issues.gh2973.BaseRelationship;
194+
import org.springframework.data.neo4j.integration.issues.gh2973.Gh2973Repository;
195+
import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipA;
196+
import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipB;
197+
import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipC;
198+
import org.springframework.data.neo4j.integration.issues.gh2973.RelationshipD;
192199
import org.springframework.data.neo4j.integration.issues.qbe.A;
193200
import org.springframework.data.neo4j.integration.issues.qbe.ARepository;
194201
import org.springframework.data.neo4j.integration.issues.qbe.B;
@@ -1698,6 +1705,72 @@ void customQueriesShouldKeepWorkingWithoutSpecifyingTheRelDirectionInTheirQuerie
16981705
assertThat(rootModelFromDbCustom).map(MyModel::getMyNestedModel).isPresent();
16991706
}
17001707

1708+
@Tag("GH-2973")
1709+
@Test
1710+
void abstractedRelationshipTypesShouldBeMappedCorrectly(@Autowired Gh2973Repository gh2973Repository) {
1711+
var node = new BaseNode();
1712+
var nodeFail = new BaseNode();
1713+
RelationshipA a1 = new RelationshipA();
1714+
RelationshipA a2 = new RelationshipA();
1715+
RelationshipA a3 = new RelationshipA();
1716+
RelationshipB b1 = new RelationshipB();
1717+
RelationshipB b2 = new RelationshipB();
1718+
RelationshipC c1 = new RelationshipC();
1719+
RelationshipD d1 = new RelationshipD();
1720+
1721+
a1.setTargetNode(new BaseNode());
1722+
a1.setA("a1");
1723+
a2.setTargetNode(new BaseNode());
1724+
a2.setA("a2");
1725+
a3.setTargetNode(new BaseNode());
1726+
a3.setA("a3");
1727+
1728+
b1.setTargetNode(new BaseNode());
1729+
b1.setB("b1");
1730+
b2.setTargetNode(new BaseNode());
1731+
b2.setB("b2");
1732+
1733+
c1.setTargetNode(new BaseNode());
1734+
c1.setC("c1");
1735+
1736+
d1.setTargetNode(new BaseNode());
1737+
d1.setD("d1");
1738+
1739+
node.setRelationships(Map.of(
1740+
"a", List.of(
1741+
a1, a2, b2
1742+
),
1743+
"b", List.of(
1744+
b1, a3
1745+
)
1746+
));
1747+
nodeFail.setRelationships(Map.of(
1748+
"c", List.of(
1749+
c1, d1
1750+
)
1751+
));
1752+
var persistedNode = gh2973Repository.save(node);
1753+
var persistedNodeFail = gh2973Repository.save(nodeFail);
1754+
1755+
// with type info, the relationships are of the correct type
1756+
var loadedNode = gh2973Repository.findById(persistedNode.getId()).get();
1757+
List<BaseRelationship> relationshipsA = loadedNode.getRelationships().get("a");
1758+
List<BaseRelationship> relationshipsB = loadedNode.getRelationships().get("b");
1759+
assertThat(relationshipsA).satisfiesExactlyInAnyOrder(
1760+
r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class),
1761+
r2 -> assertThat(r2).isOfAnyClassIn(RelationshipA.class),
1762+
r3 -> assertThat(r3).isOfAnyClassIn(RelationshipB.class)
1763+
);
1764+
assertThat(relationshipsB).satisfiesExactlyInAnyOrder(
1765+
r1 -> assertThat(r1).isOfAnyClassIn(RelationshipA.class),
1766+
r2 -> assertThat(r2).isOfAnyClassIn(RelationshipB.class)
1767+
);
1768+
// without type info, the relationships are all same type and not the base class BaseRelationship
1769+
var loadedNodeFail = gh2973Repository.findById(persistedNodeFail.getId()).get();
1770+
List<BaseRelationship> relationshipsCFail = loadedNodeFail.getRelationships().get("c");
1771+
assertThat(relationshipsCFail.get(0)).isNotExactlyInstanceOf(BaseRelationship.class);
1772+
}
1773+
17011774
@Configuration
17021775
@EnableTransactionManagement
17031776
@EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties")
@@ -1855,4 +1928,6 @@ private static EntitiesAndProjections.GH2533Entity createData(GH2533Repository r
18551928

18561929
return repository.save(n1);
18571930
}
1931+
1932+
18581933
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2973;
17+
18+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
19+
import org.springframework.data.neo4j.core.schema.Id;
20+
import org.springframework.data.neo4j.core.schema.Node;
21+
import org.springframework.data.neo4j.core.schema.Relationship;
22+
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.UUID;
27+
28+
29+
/**
30+
* @author yangyaofei
31+
*/
32+
@Node
33+
public class BaseNode {
34+
@Id
35+
@GeneratedValue
36+
private UUID id;
37+
@Relationship(direction = Relationship.Direction.OUTGOING)
38+
private Map<String, List<BaseRelationship>> relationships = new HashMap<>();
39+
40+
public UUID getId() {
41+
return id;
42+
}
43+
44+
public void setId(UUID id) {
45+
this.id = id;
46+
}
47+
48+
public Map<String, List<BaseRelationship>> getRelationships() {
49+
return relationships;
50+
}
51+
52+
public void setRelationships(Map<String, List<BaseRelationship>> relationships) {
53+
this.relationships = relationships;
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2973;
17+
18+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
19+
import org.springframework.data.neo4j.core.schema.RelationshipId;
20+
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
21+
import org.springframework.data.neo4j.core.schema.TargetNode;
22+
23+
/**
24+
* @author yangyaofei
25+
*/
26+
@RelationshipProperties
27+
public abstract class BaseRelationship {
28+
@RelationshipId
29+
@GeneratedValue
30+
private Long id;
31+
@TargetNode
32+
private BaseNode targetNode;
33+
34+
public Long getId() {
35+
return id;
36+
}
37+
38+
public void setId(Long id) {
39+
this.id = id;
40+
}
41+
42+
public BaseNode getTargetNode() {
43+
return targetNode;
44+
}
45+
46+
public void setTargetNode(BaseNode targetNode) {
47+
this.targetNode = targetNode;
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2973;
17+
18+
import org.springframework.data.neo4j.repository.Neo4jRepository;
19+
20+
import java.util.UUID;
21+
22+
/**
23+
* Test repository for GH-2973
24+
*
25+
* @author yangyaofei
26+
*/
27+
public interface Gh2973Repository extends Neo4jRepository<BaseNode, UUID> {
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2973;
17+
18+
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
19+
20+
/**
21+
* @author yangyaofei
22+
*/
23+
@RelationshipProperties(persistTypeInfo = true)
24+
public class RelationshipA extends BaseRelationship {
25+
String a;
26+
27+
public String getA() {
28+
return a;
29+
}
30+
31+
public void setA(String a) {
32+
this.a = a;
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.issues.gh2973;
17+
18+
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
19+
20+
/**
21+
* @author yangyaofei
22+
*/
23+
@RelationshipProperties(persistTypeInfo = true)
24+
public class RelationshipB extends BaseRelationship {
25+
String b;
26+
27+
public String getB() {
28+
return b;
29+
}
30+
31+
public void setB(String b) {
32+
this.b = b;
33+
}
34+
}

0 commit comments

Comments
 (0)