Skip to content

Projecting rich relation for inherited nodes fails #2526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Andy2003 opened this issue Apr 27, 2022 · 9 comments
Closed

Projecting rich relation for inherited nodes fails #2526

Andy2003 opened this issue Apr 27, 2022 · 9 comments
Assignees
Labels
status: feedback-provided Feedback has been provided

Comments

@Andy2003
Copy link
Contributor

Andy2003 commented Apr 27, 2022

Given:

@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;
}
@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<DataPoint> dataPoints;
}
@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;
}
@Node
@Value
@AllArgsConstructor
@Immutable
public class Measurand {

	@Id
	String measurandId;
}
@RelationshipProperties
@Value
@With
@AllArgsConstructor
@Immutable
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class DataPoint {

	@RelationshipId
	Long id;

	boolean manual;

	@TargetNode
	@EqualsAndHashCode.Include
	Measurand measurand;
}
public interface BaseNodeRepository extends Neo4jRepository<BaseNodeEntity, String> {

	<R> R findByNodeId(String nodeIds, Class<R> clazz);
}

And the following data:

CREATE (o1:Measurand {measurandId: 'o1'})
CREATE (acc1:AccountingMeasurementMeta:BaseNodeEntity {nodeId: 'acc1'})
CREATE (o1)-[:IS_MEASURED_BY{ manual: true }]->(acc1)

the following test will fail:

interface Projection {
	String getNodeId();
	Set<DataPoint> getDataPoints();
}

@Test
void test(@Autowired BaseNodeRepository repository) {
	Projection m = repository.findByNodeId("acc1", Projection.class);
	assertThat(m).isNotNull();
}

with this exception:

org.springframework.data.mapping.MappingException: Error mapping Record<{baseNodeEntity: {__internalNeo4jId__: 1, BaseNodeEntity_IS_MEASURED_BY_Measurand: [{measurandId: "o1", __internalNeo4jId__: 0, __nodeLabels__: ["Measurand"], MeasurementMeta__relationship__Measurand: relationship<0>}], nodeId: "acc1", __nodeLabels__: ["AccountingMeasurementMeta", "BaseNodeEntity"]}}>

	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.read(DefaultNeo4jEntityConverter.java:122)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.read(DefaultNeo4jEntityConverter.java:71)
	at org.springframework.data.neo4j.core.mapping.Schema.lambda$getRequiredMappingFunctionFor$0(Schema.java:96)
	at org.springframework.data.neo4j.core.mapping.EntityInstanceWithSource.lambda$decorateMappingFunction$0(EntityInstanceWithSource.java:49)
	at org.springframework.data.neo4j.core.PreparedQuery$AggregatingMappingFunction.apply(PreparedQuery.java:246)
	at org.springframework.data.neo4j.core.PreparedQuery$AggregatingMappingFunction.apply(PreparedQuery.java:158)
	at org.springframework.data.neo4j.core.DelegatingMappingFunctionWithNullCheck.apply(DelegatingMappingFunctionWithNullCheck.java:45)
	at org.springframework.data.neo4j.core.DelegatingMappingFunctionWithNullCheck.apply(DelegatingMappingFunctionWithNullCheck.java:35)
	at org.springframework.data.neo4j.core.DefaultNeo4jClient$DefaultRecordFetchSpec.one(DefaultNeo4jClient.java:455)
	at java.base/java.util.Optional.flatMap(Optional.java:294)
	at org.springframework.data.neo4j.core.Neo4jTemplate$DefaultExecutableQuery.getSingleResult(Neo4jTemplate.java:1010)
	at org.springframework.data.neo4j.repository.query.Neo4jQueryExecution$DefaultQueryExecution.execute(Neo4jQueryExecution.java:53)
	at org.springframework.data.neo4j.repository.query.AbstractNeo4jQuery.execute(AbstractNeo4jQuery.java:94)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:159)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at com.sun.proxy.$Proxy60.findByNodeId(Unknown Source)
	at org.springframework.data.neo4j.integration.issues.ghNew.GHNewIT.shouldNotDeleteFreshlyCreatedRelationships(GHNewIT.java:66)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.neo4j.driver.exceptions.value.Uncoercible: Cannot coerce NULL to Relationship
	at org.neo4j.driver.internal.value.ValueAdapter.asRelationship(ValueAdapter.java:311)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.createInstanceOfRelationships(DefaultNeo4jEntityConverter.java:645)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.lambda$populateFrom$6(DefaultNeo4jEntityConverter.java:539)
	at org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport.lambda$doWithAssociations$0(AssociationHandlerSupport.java:51)
	at org.springframework.data.mapping.model.BasicPersistentEntity.doWithAssociations(BasicPersistentEntity.java:394)
	at org.springframework.data.neo4j.core.mapping.AssociationHandlerSupport.doWithAssociations(AssociationHandlerSupport.java:49)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.populateProperties(DefaultNeo4jEntityConverter.java:355)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.lambda$map$2(DefaultNeo4jEntityConverter.java:291)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.map(DefaultNeo4jEntityConverter.java:305)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.map(DefaultNeo4jEntityConverter.java:259)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jEntityConverter.read(DefaultNeo4jEntityConverter.java:120)
	... 99 more

In case the DataPoint would not be a rich relation (by removing the manual field) the test will succeed.

Test-Case: https://github.com/Andy2003/spring-data-neo4j/tree/issue/gh-2526

Background:

I need to query BaseNodeEntity by not knowing the concrete node type. The projection will also contain fileds for some specialized BaseNodeEntitys like the dataPoints for all MeasurementMeta-Nodes

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 27, 2022
Andy2003 added a commit to Andy2003/spring-data-neo4j that referenced this issue Apr 27, 2022
@Andy2003
Copy link
Contributor Author

neo4j is trying to map:

{
	baseNodeEntity: {
		__internalNeo4jId__: 1,
		BaseNodeEntity_IS_MEASURED_BY_Measurand: [
			{
				measurandId: "o1",
				__internalNeo4jId__: 0,
				__nodeLabels__: [
					"Measurand"
				],
				MeasurementMeta__relationship__Measurand: relationship<0>
			}
		],
		nodeId: "acc1",
		__nodeLabels__: [
			"AccountingMeasurementMeta",
			"BaseNodeEntity"
		]
	}
}

It expects the property AccountingMeasurementMeta__relationship__Measurand instead of MeasurementMeta__relationship__Measurand.

The first part of the results property AccountingMeasurementMeta__relationship__Measurand is provided by the RelationshipDescription which states that the source of the relation is the AccountingMeasurementMeta instead of the MeasurementMeta

Andy2003 added a commit to Andy2003/spring-data-neo4j that referenced this issue Apr 27, 2022
Andy2003 added a commit to Andy2003/spring-data-neo4j that referenced this issue Apr 27, 2022
Andy2003 added a commit to Andy2003/spring-data-neo4j that referenced this issue Apr 27, 2022
Andy2003 added a commit to Andy2003/spring-data-neo4j that referenced this issue Apr 29, 2022
meistermeier added a commit that referenced this issue May 9, 2022
meistermeier added a commit that referenced this issue May 9, 2022
@meistermeier
Copy link
Collaborator

I would like to keep the logic for stepping down to a common relationship "source name" for Cypher creation and mapping in the place where it already happens. There is now a 6.2.5-GH-2526-SNAPSHOT available for testing. Would be good to get your feedback on this.
The problem with the source calculation for me is that it is not the truth anymore because it can reflect a class higher that defines the relationship instead of the type, that is explicitly defined. Secondly, the Lazy calculation is fixed after the first request and if an additional class comes lazy (unfortunately this is technical possible) into the hierarchy, it will get missed.

@meistermeier meistermeier self-assigned this May 9, 2022
@meistermeier meistermeier added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels May 9, 2022
@Andy2003
Copy link
Contributor Author

Andy2003 commented May 9, 2022

You branch does not work for my use-case. It is not determining the right base class. I need to dig deeper to find the cause. I'll keep you updated.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 9, 2022
Andy2003 pushed a commit to Andy2003/spring-data-neo4j that referenced this issue May 9, 2022
@Andy2003
Copy link
Contributor Author

Andy2003 commented May 9, 2022

@meistermeier please take a look at the extended test I've added via Andy2003@6ada466 this will work with my solution but not with your branch.

@meistermeier
Copy link
Collaborator

Added the extended test and things are still working.

@meistermeier meistermeier added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 10, 2022
meistermeier added a commit that referenced this issue May 10, 2022
@Andy2003
Copy link
Contributor Author

You missed to setup the extended data:

CREATE (o1:Measurand {measurandId: 'o1'})
CREATE (acc1:AccountingMeasurementMeta:MeasurementMeta:BaseNodeEntity {nodeId: 'acc1'})
CREATE (m1:MeasurementMeta:BaseNodeEntity {nodeId: 'm1'})
CREATE (acc1)-[:USES{variable: 'A'}]->(m1)
CREATE (o1)-[:IS_MEASURED_BY{ manual: true }]->(acc1)

with this data the test will fail

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 10, 2022
meistermeier added a commit that referenced this issue May 10, 2022
@meistermeier
Copy link
Collaborator

Thanks, I have missed this one. It is now fixed in the branch.

@meistermeier meistermeier removed the status: feedback-provided Feedback has been provided label May 10, 2022
@meistermeier meistermeier added the status: waiting-for-feedback We need additional information before we can continue label May 10, 2022
@Andy2003
Copy link
Contributor Author

Perfect, all my tests are green with this solution!

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 10, 2022
@meistermeier
Copy link
Collaborator

Thanks for your feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided
Projects
None yet
Development

No branches or pull requests

3 participants