diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java index 0f18291264..34fadaf569 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultRelationshipDescription.java @@ -19,20 +19,23 @@ import org.springframework.data.mapping.Association; import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; /** * @author Michael J. Simons * @author Gerrit Meier + * @author Andreas Berger * @since 6.0 */ -final class DefaultRelationshipDescription extends Association implements RelationshipDescription { +final class DefaultRelationshipDescription extends Association + implements RelationshipDescription { private final String type; private final boolean dynamic; - private final NodeDescription source; + private final Lazy> source; private final NodeDescription target; @@ -55,13 +58,30 @@ final class DefaultRelationshipDescription extends Association getSourceForField(source, type, dynamic, target, direction, relationshipProperties)); this.fieldName = fieldName; this.target = target; this.direction = direction; this.relationshipPropertiesClass = relationshipProperties; } + private static NodeDescription getSourceForField(NodeDescription source, String type, boolean dynamic, + NodeDescription target, Relationship.Direction direction, + @Nullable NodeDescription relationshipProperties) { + NodeDescription parent = source.getParentNodeDescription(); + if (parent == null) { + return source; + } + for (RelationshipDescription relationship : parent.getRelationships()) { + if (Objects.equals(relationship.getTarget(), target) && Objects.equals(relationship.getDirection(), direction) + && Objects.equals(relationship.isDynamic(), dynamic) && Objects.equals(relationship.getType(), type) + && Objects.equals(relationship.getRelationshipPropertiesEntity(), relationshipProperties)) { + return getSourceForField(relationship.getSource(), type, dynamic, target, direction, relationshipProperties); + } + } + return source; + } + @Override public String getType() { return type; @@ -79,7 +99,7 @@ public NodeDescription getTarget() { @Override public NodeDescription getSource() { - return source; + return source.get(); } @Override @@ -132,8 +152,9 @@ public boolean equals(Object o) { return false; } DefaultRelationshipDescription that = (DefaultRelationshipDescription) o; - return (isDynamic() ? getFieldName().equals(that.getFieldName()) : getType().equals(that.getType())) && getTarget().equals(that.getTarget()) - && getSource().equals(that.getSource()) && getDirection().equals(that.getDirection()); + return (isDynamic() ? getFieldName().equals(that.getFieldName()) : getType().equals(that.getType())) + && getTarget().equals(that.getTarget()) && getSource().equals(that.getSource()) + && getDirection().equals(that.getDirection()); } @Override diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java new file mode 100644 index 0000000000..83fba94210 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/AccountingMeasurementMeta.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import static org.springframework.data.neo4j.core.schema.Relationship.Direction.*; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import java.util.Set; + +/** + * @author Andreas Berger + */ +@Node +@Data +@Setter(AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true) +@SuperBuilder(toBuilder = true) +public class AccountingMeasurementMeta extends MeasurementMeta { + + private String formula; + + @Relationship(type = "WEIGHTS", direction = OUTGOING) private MeasurementMeta baseMeasurement; + + @Relationship(type = "USES", direction = OUTGOING) + private Set variables; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeEntity.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeEntity.java new file mode 100644 index 0000000000..20f6a7dc1c --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeEntity.java @@ -0,0 +1,47 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.NonFinal; +import lombok.experimental.SuperBuilder; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.support.UUIDStringGenerator; + +/** + * @author Andreas Berger + */ +@org.springframework.data.neo4j.core.schema.Node +@NonFinal +@Data +@Setter(AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@SuperBuilder(toBuilder = true) +public class BaseNodeEntity { + + @Id + @GeneratedValue(UUIDStringGenerator.class) + @EqualsAndHashCode.Include private String nodeId; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java new file mode 100644 index 0000000000..79f21ab60e --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/BaseNodeRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +/** + * @author Andreas Berger + */ +public interface BaseNodeRepository extends Neo4jRepository { + + R findByNodeId(String nodeIds, Class clazz); +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java new file mode 100644 index 0000000000..2dcd55009d --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/DataPoint.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.With; + +import org.springframework.data.annotation.Immutable; +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 Andreas Berger + */ +@RelationshipProperties +@Value +@With +@AllArgsConstructor +@Immutable +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class DataPoint { + + @RelationshipId Long id; + + boolean manual; + + @TargetNode + @EqualsAndHashCode.Include Measurand measurand; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/GH2526IT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/GH2526IT.java new file mode 100644 index 0000000000..ca3b731689 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/GH2526IT.java @@ -0,0 +1,110 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.InstanceOfAssertFactories.*; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.config.AbstractNeo4jConfig; +import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Andreas Berger + */ +@Neo4jIntegrationTest +public class GH2526IT { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + @BeforeEach + void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) { + + try (Session session = driver.session()) { + session.run("MATCH (n) DETACH DELETE n").consume(); + session.run("CREATE (o1:Measurand {measurandId: 'o1'})" + + "CREATE (acc1:AccountingMeasurementMeta:BaseNodeEntity {nodeId: 'acc1'})" + + "CREATE (m1:MeasurementMeta:BaseNodeEntity {nodeId: 'm1'})" + + "CREATE (acc1)-[:USES{variable: 'A'}]->(m1)" + + "CREATE (o1)-[:IS_MEASURED_BY{ manual: true }]->(acc1)").consume(); + bookmarkCapture.seedWith(session.lastBookmark()); + } + } + + @Test + // GH-2526 + void testRichRelationWithInheritance(@Autowired BaseNodeRepository repository) { + MeasurementProjection m = repository.findByNodeId("acc1", MeasurementProjection.class); + assertThat(m).isNotNull(); + assertThat(m).extracting(MeasurementProjection::getDataPoints, collection(DataPoint.class)) + .extracting(DataPoint::isManual, DataPoint::getMeasurand).contains(tuple(true, new Measurand("o1"))); + } + + interface BaseNodeFieldsProjection{ + String getNodeId(); + } + + interface MeasurementProjection extends BaseNodeFieldsProjection { + Set getDataPoints(); + Set getVariables(); + } + + interface VariableProjection { + BaseNodeFieldsProjection getMeasurement(); + String getVariable(); + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories + static class Config extends AbstractNeo4jConfig { + + @Bean + public BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + } + + @Bean + public Driver driver() { + + return neo4jConnectionSupport.getDriver(); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java new file mode 100644 index 0000000000..bb0ded0527 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Measurand.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import org.springframework.data.annotation.Immutable; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +/** + * @author Andreas Berger + */ +@Node +@Value +@AllArgsConstructor +@Immutable +public class Measurand { + + @Id String measurandId; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java new file mode 100644 index 0000000000..2df430fb51 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/MeasurementMeta.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011-2022 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.gh2526; + +import static org.springframework.data.neo4j.core.schema.Relationship.Direction.*; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.util.Set; + +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Andreas Berger + */ +@org.springframework.data.neo4j.core.schema.Node +@Data +@Setter(AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true) +@SuperBuilder(toBuilder = true) +public class MeasurementMeta extends BaseNodeEntity { + + @Relationship(type = "IS_MEASURED_BY", direction = INCOMING) private Set dataPoints; +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java new file mode 100644 index 0000000000..38e2df29c7 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/gh2526/Variable.java @@ -0,0 +1,36 @@ +package org.springframework.data.neo4j.integration.issues.gh2526; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.With; + +import org.springframework.data.annotation.Immutable; +import org.springframework.data.neo4j.core.schema.RelationshipId; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +@RelationshipProperties +@Value +@With +@AllArgsConstructor +@EqualsAndHashCode +@Immutable +public class Variable { + @RelationshipId + Long id; + + @TargetNode + MeasurementMeta measurement; + + String variable; + + public static Variable create(MeasurementMeta measurement, String variable) { + return new Variable(null, measurement, variable); + } + + @Override + public String toString() { + return variable + ": " + measurement.getNodeId(); + } +}