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 a6ff1ede5..6b8445269 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 @@ -326,7 +326,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { knownObjects.removeFromInCreation(internalId); - populateProperties(queryResult, nodeDescription, internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false); + populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false); PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(getMostCurrentInstance(internalId, instance)); ET bean = propertyAccessor.getBean(); @@ -351,7 +351,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { // AND (!!!) // 2. mutable target types // because we cannot just create new instances - populateProperties(queryResult, nodeDescription, internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true); + populateProperties(queryResult, (Neo4jPersistentEntity) genericTargetNodeDescription, nodeDescription, internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true); } // due to a needed side effect in `populateProperties`, the entity might have been changed return getMostCurrentInstance(internalId, mappedObject); @@ -363,13 +363,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } - private void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity nodeDescription, String internalId, + private void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity baseNodeDescription, Neo4jPersistentEntity moreConcreteNodeDescription, String internalId, ET mappedObject, @Nullable Object lastMappedEntity, Collection relationshipsFromResult, Collection nodesFromResult, boolean objectAlreadyMapped) { - List allLabels = getLabels(queryResult, nodeDescription); + List allLabels = getLabels(queryResult, moreConcreteNodeDescription); NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore - .deriveConcreteNodeDescription(nodeDescription, allLabels); + .deriveConcreteNodeDescription(moreConcreteNodeDescription, allLabels); @SuppressWarnings("unchecked") Neo4jPersistentEntity concreteNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels @@ -397,7 +397,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { knownObjects.storeObject(internalId, propertyAccessor.getBean()); AssociationHandlerSupport.of(concreteNodeDescription).doWithAssociations( - populateFrom(queryResult, nodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult)); + populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult)); } @NonNull @@ -469,12 +469,12 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { // If we cannot find any value it does not mean that there isn't any. // The result set might contain associations not named CONCRETE_TYPE_TARGET but ABSTRACT_TYPE_TARGET. // For this we bubble up the hierarchy of NodeDescriptions. - result = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, nodeDescription, genericNodeDescription, relationshipsFromResult, nodesFromResult) + result = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, genericNodeDescription, relationshipsFromResult, nodesFromResult) .orElseGet(() -> { NodeDescription parentNodeDescription = nodeDescription.getParentNodeDescription(); T resultValue = null; while (parentNodeDescription != null) { - Optional value = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, parentNodeDescription, parentNodeDescription, relationshipsFromResult, nodesFromResult); + Optional value = createInstanceOfRelationships(matchingProperty, values, relationshipDescription, parentNodeDescription, relationshipsFromResult, nodesFromResult); if (value.isPresent()) { resultValue = (T) value.get(); break; @@ -567,7 +567,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { && propertyValueNotNull; if (populatedCollection) { - createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, baseDescription, relationshipsFromResult, nodesFromResult, false) + createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult, false) .ifPresent(value -> { Collection providedCollection = (Collection) value; Collection existingValue = (Collection) propertyValue; @@ -590,7 +590,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return; } - createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, baseDescription, relationshipsFromResult, nodesFromResult) + createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult) .ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value)); }; } @@ -614,13 +614,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - RelationshipDescription relationshipDescription, NodeDescription baseDescription, NodeDescription genericNodeDescription, Collection relationshipsFromResult, + RelationshipDescription relationshipDescription, NodeDescription baseDescription, Collection relationshipsFromResult, Collection nodesFromResult) { - return createInstanceOfRelationships(persistentProperty, values, relationshipDescription, baseDescription, genericNodeDescription, relationshipsFromResult, nodesFromResult, true); + return createInstanceOfRelationships(persistentProperty, values, relationshipDescription, baseDescription, relationshipsFromResult, nodesFromResult, true); } private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - RelationshipDescription relationshipDescription, NodeDescription baseDescription, NodeDescription genericNodeDescription, Collection relationshipsFromResult, + RelationshipDescription relationshipDescription, NodeDescription baseDescription, Collection relationshipsFromResult, Collection nodesFromResult, boolean fetchMore) { String typeOfRelationship = relationshipDescription.getType(); @@ -654,8 +654,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { mappedObjectHandler = (type, mappedObject) -> value.add(mappedObject); } - String collectionName = relationshipDescription.generateRelatedNodesCollectionName(genericNodeDescription); - + String collectionName = relationshipDescription.generateRelatedNodesCollectionName(baseDescription); Value list = values.get(collectionName); boolean relationshipListEmptyOrNull = Values.NULL.equals(list); @@ -715,12 +714,12 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { if (fetchMore) { mappedObject = sourceNodeId != null && sourceNodeId.equals(targetNodeId) ? knownObjects.getObject("N" + sourceNodeId) - : map(possibleValueNode, concreteTargetNodeDescription, genericNodeDescription, null, null, relationshipsFromResult, nodesFromResult); + : map(possibleValueNode, concreteTargetNodeDescription, baseDescription, null, null, relationshipsFromResult, nodesFromResult); } else { Object objectFromStore = knownObjects.getObject("N" + targetNodeId); mappedObject = objectFromStore != null ? objectFromStore - : map(possibleValueNode, concreteTargetNodeDescription, genericNodeDescription, null, null, relationshipsFromResult, nodesFromResult); + : map(possibleValueNode, concreteTargetNodeDescription, baseDescription, null, null, relationshipsFromResult, nodesFromResult); } if (relationshipDescription.hasRelationshipProperties()) { @@ -752,16 +751,16 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Object valueEntry; if (fetchMore) { - valueEntry = map(relatedEntity, concreteTargetNodeDescription, genericNodeDescription, null, null, relationshipsFromResult, nodesFromResult); + valueEntry = map(relatedEntity, concreteTargetNodeDescription, genericTargetNodeDescription, null, null, relationshipsFromResult, nodesFromResult); } else { Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(relatedEntity, null)); valueEntry = objectFromStore != null ? objectFromStore - : map(relatedEntity, concreteTargetNodeDescription, genericNodeDescription, null, null, relationshipsFromResult, nodesFromResult); + : map(relatedEntity, concreteTargetNodeDescription, genericTargetNodeDescription, null, null, relationshipsFromResult, nodesFromResult); } if (relationshipDescription.hasRelationshipProperties()) { - String sourceLabel = relationshipDescription.getSource().getMostAbstractParentLabel(genericNodeDescription); + String sourceLabel = relationshipDescription.getSource().getMostAbstractParentLabel(baseDescription); String relationshipSymbolicName = sourceLabel + RelationshipDescription.NAME_OF_RELATIONSHIP + targetLabel; Relationship relatedEntityRelationship = relatedEntity.get(relationshipSymbolicName) 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 83d5d0726..1ece09c21 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 @@ -143,6 +143,8 @@ import org.springframework.data.neo4j.integration.issues.gh2639.Individual; import org.springframework.data.neo4j.integration.issues.gh2639.LanguageRelationship; import org.springframework.data.neo4j.integration.issues.gh2639.ProgrammingLanguage; import org.springframework.data.neo4j.integration.issues.gh2639.Sales; +import org.springframework.data.neo4j.integration.issues.gh2819.GH2819Model; +import org.springframework.data.neo4j.integration.issues.gh2819.GH2819Repository; 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; @@ -1103,6 +1105,25 @@ class IssuesIT extends TestBase { }); } + @Test + @Tag("GH-2819") + void inheritanceAndProjectionShouldMapRelatedNodesCorrectly(@Autowired GH2819Repository repository, @Autowired Driver driver) { + try (var session = driver.session()) { + session.run("CREATE (a:ParentA:ChildA{name:'parentA', id:'a'})-[:HasBs]->(b:ParentB:ChildB{name:'parentB', id:'b'})-[:HasCs]->(c:ParentC:ChildC{name:'parentC', id:'c'})").consume(); + } + + var childAProjection = repository.findById("a", GH2819Model.ChildAProjection.class); + + assertThat(childAProjection.getName()).isEqualTo("parentA"); + var parentB = childAProjection.getParentB(); + assertThat(parentB).isNotNull(); + assertThat(parentB.getName()).isEqualTo("parentB"); + var parentC = parentB.getParentC(); + assertThat(parentC).isNotNull(); + assertThat(parentC.getName()).isEqualTo("parentC"); + + } + @Configuration @EnableTransactionManagement @EnableNeo4jRepositories(namedQueriesLocation = "more-custom-queries.properties") diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java new file mode 100644 index 000000000..59a86d3de --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Model.java @@ -0,0 +1,129 @@ +/* + * Copyright 2011-2023 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.gh2819; + +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Gerrit Meier + */ +public class GH2819Model { + + /** + * Projection of ParentA/ChildA + */ + public interface ChildAProjection { + String getName(); + GH2819Model.ChildBProjection getParentB(); + } + + /** + * Projection of ParentB/ChildB + */ + public interface ChildBProjection { + String getName(); + GH2819Model.ChildCProjection getParentC(); + } + + /** + * Projection of ParentC/ChildC + */ + public interface ChildCProjection { + String getName(); + } + + /** + * ParentA + */ + @Node + public static class ParentA { + @Id public String id; + + @Relationship(type = "HasBs", direction = Relationship.Direction.OUTGOING) + public ParentB parentB; + + public String name; + + public String getName() { + return name; + } + + public ParentB getParentB() { + return parentB; + } + } + + /** + * ParentB + */ + @Node + public static class ParentB { + @Id public String id; + + @Relationship(type = "HasCs", direction = Relationship.Direction.OUTGOING) + public ParentC parentC; + + public String name; + + public ParentC getParentC() { + return parentC; + } + + public String getName() { + return name; + } + } + + /** + * ParentC + */ + @Node + public static class ParentC { + @Id public String id; + + public String name; + + public String getName() { + return name; + } + } + + /** + * ChildA + */ + @Node + public static class ChildA extends ParentA { + + } + + /** + * ChildB + */ + @Node + public static class ChildB extends ParentB { + + } + + /** + * ChildC + */ + @Node + public static class ChildC extends ParentC { + + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java new file mode 100644 index 000000000..0b579d5d4 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2819/GH2819Repository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011-2023 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.gh2819; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface GH2819Repository extends Neo4jRepository { + + GH2819Model.ChildAProjection findById(String id, Class projectionClass); +}