Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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__";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.internal.value.NullValue;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Relationship;
Expand Down Expand Up @@ -233,6 +234,10 @@ public void write(Object source, Map<String, Object> parameters) {

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) -> {
Expand Down Expand Up @@ -427,6 +432,8 @@ private Neo4jPersistentEntity<?> getMostConcreteTargetNodeDescription(

/**
* 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}.
Expand All @@ -440,6 +447,13 @@ private List<String> getLabels(MapAccessor queryResult, @Nullable NodeDescriptio
} 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 instanceof NullValue) {
labels.addAll(nodeDescription.getStaticLabels());
} else {
labels.add(value.asString());
}
} else if (containsOnePlainNode(queryResult)) {
for (Value value : queryResult.values()) {
if (value.hasType(nodeType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ public boolean isRelationshipPropertiesEntity() {
return this.isRelationshipPropertiesEntity.get();
}

@Override
public boolean hasRelationshipPropertyPersistTypeInfoFlag() {
if (!isRelationshipPropertiesEntity()) {
return false;
}
return getRequiredAnnotation(RelationshipProperties.class).persistTypeInfo();
}

/*
* (non-Javadoc)
* @see BasicPersistentEntity#getFallbackIsNewStrategy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@
@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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@
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;
Expand Down Expand Up @@ -1698,6 +1705,68 @@ void customQueriesShouldKeepWorkingWithoutSpecifyingTheRelDirectionInTheirQuerie
assertThat(rootModelFromDbCustom).map(MyModel::getMyNestedModel).isPresent();
}

@Tag("GH-2973")
@Test
void testTypeMapping(@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).hasExactlyElementsOfTypes(RelationshipA.class, RelationshipA.class, RelationshipB.class);
assertThat(relationshipsB).hasExactlyElementsOfTypes(RelationshipB.class, RelationshipA.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);
var type = relationshipsCFail.get(0).getClass();
assertThat(relationshipsCFail).hasExactlyElementsOfTypes(type, type);
}

@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties")
Expand Down Expand Up @@ -1855,4 +1924,6 @@ private static EntitiesAndProjections.GH2533Entity createData(GH2533Repository r

return repository.save(n1);
}


}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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> {
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading