How to correctly update a relationship

Hello ! I'm having problems to update the relation property.

Given the :

public class SomeEntity {

    @Id
    @GeneratedValue
    protected Long id;

    @Property
    protected String name;

    @Relationship
    protected Map<String, List<SomeLink>> relationships;
}

@RelationshipProperties
public class SomeLink {
    @Id
    @GeneratedValue
    protected Long id;

    @Property
    protected boolean active = true;

    @TargetNode
    protected SomeEntity target;
}

@Transactional
public interface SomeEntityRepository extends Neo4jRepository<SomeEntity, Long> {

    @Query("MATCH p=(n)-[*0..1]->(m) WHERE id(n)=$id RETURN n, collect(relationships(p)), collect(m);")
    Optional<SomeEntity> findByIdWithLevelOneLinks(@Param("id") Long id);
}

As a result, we have the graphe like:
(n1:SomeEntity)-[r1:SomeLink]->(n2:SomeEntity)-[r2:SomeLink]->(n3:SomeEntity)-[...]->(...)

I want to update r1, but r2 is removed if I use findByIdWithLevelOneLinks in the repository. It is comprehensible.

I tried to use the projections as suggested in https://community.neo4j.com/t/relationships-eliminated-after-updating-node-properties/55117/4?u=pandalei97

public interface SomeEntityWithoutRelationship {
    Long getId();
    String getName();
}

public interface SomeEntityNodeWithOneLevelLinks {
    Long getId();
    String getName();
    Map<String, List<SomeLinkWithoutTargetRelationships>> getRelationships();
}

public interface SomeLinkWithoutTargetRelationships {
    Long getId();
    boolean isActive();
    SomeEntityWithoutRelationship getTarget();
}

Since projections only contains getters, I can't update the DTO that I retrieved. So I continue to use SomeEntity in the repository and when I update the node, I tried to do a Projection projection = neo4jTemplate.saveAs(someEntityObject, SomeEntityNodeWithOneLevelLinks.class);

But r2 is still detached.

Please note that this is a simplified class. In our real class of SomeEntity and SomeLink, there are special setters and some composite properties. As a result, it is difficult to use Class-based Projections or Cypher-DSL.

I tried to solve this problem for several days but I still can't find a solution. Can someone help me ?

Thanks in advance for your response.

You are on the right track: Loading the entity as it is (without all relationships) and persisting it via the Neo4jTemplate#saveAs(entity, YourProjection.class) should do the job.
Sorry to hear that this is not working for you as expected. I am happy to investigate this because it might be the combination of dynamic relationships and relationship properties. Could you provide a reproducer for the problem? Maybe I have overseen something but I could not make it fail in our test base.

Hi Gerrit !

Thanks for your confirmation that I'm not on the wrong way.

I have created a simple demo here: GitHub - Pandalei97/neo4jDemoBugRelationship

I defined hard coded examples in the controller to save time.

When I reproduce the problem, I found that it occurs when we use @GeneratedValue. When we use hard coded user-defined ID, it doesn't seem to detach the relationship. I hope that can give you a clearer vision of the solution.

I think that I have found the problem in the code base. It was the dynamic relationship.
Created an issue to track Projection support for dynamic relationships · Issue #2533 · spring-projects/spring-data-neo4j · GitHub
Also there is now a 6.2.5-GH-2533-SNAPSHOT available to test. Would be helpful to get your feedback on this.
To use the snapshot, you have to add Spring's snapshot (and milestone) repository to your pom.

<repositories>
  <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
    <snapshots>
      <enabled>false</enabled>
    </snapshots>
  </repository>
  <repository>
    <id>spring-snapshots</id>
    <name>Spring Snapshots</name>
    <url>https://repo.spring.io/snapshot</url>
    <releases>
      <enabled>false</enabled>
    </releases>
  </repository>
</repositories>

Thanks for the reproducer, it was really helpful.

1 Like

Thanks a lot for the quick bugfix ! I have do several tests, it seems to work correctly now. I will do some more tests and give you a feedback these days. :grin:

Thanks for the feedback. No hurry, I am still working on the tests for this.

Hi Gerrit,

I have found another use case where SDN doesn't seem to work correctly when multiple dynamic relationships exists in a class.

I have updated the demo in GitHub - Pandalei97/neo4jDemoBugRelationship

The example is defined in the endpoint createAnotherExample. In the demo we can see that the nodes aren't created correctly.

Thanks in advance for your feedback.

But you are only persisting it via neo4jTemplate.saveAs(n1, EntityWithOnlyOtherRelationshipProjection.class);.
where EntityWithOnlyOtherRelationshipProjection does only define the otherRelationships.
This is the result I am looking at:
image

Changing EntityWithOnlyOtherRelationshipProjection to extend EntityWithOneLevelRelationshipProjection will result in:
image

Yes, that's exactly what I have. The target node of N1 is an empty node, which eventually leads to all links pointing to this same node.

Is it expected ?

Stupid me. Sorry, I have only looked at the mentioned problem with the relationship(s) and not the data in the target node itself. No, this should not happen, or I have missed something obvious.
I will have a look and keep you updated.

1 Like

Status update
And now I know (again) why we initially did not support dynamic relationships in projections:
When it comes to the detection of the defined paths in the projection interface/object tree, we rely on PropertyPath functionality of Spring Data commons. There is one limitation: It does take the Map key but not the value as a valid part of the path. Adding projection.relationships.id will throw an exception, if I enforce this because it already claims that relationships is a String and not the projection/entity class.
We have some kind of workaround this restriction in a related place in SDN and I am currently trying to replace the types. I cannot promise anything at this point and might rollback the already merged changes of GH-2533 and re-open it for later.
Better not supporting this feature at the moment instead of a broken/incomplete solution.

Thanks for the investigation. This means that projection should not be used when we have dynamic relationships ?

Is there another solution to avoid the detachment of the relationship in this case or should I downgrade to SDN 5 ?

Alright. Nobody needs to downgrade :wink: There should be a 6.2.5-GH2533-2-SNAPSHOT available now. There were more changes required than expected to make it work but I think it is now possible to do what you want. At least the example shows me the node properties and the related node because the projections points to the entity itself.
image

1 Like

Thanks a lot ! I will give you a feedback soon. You are a real hero !! :partying_face: :tada:

1 Like

Hi Gerrit,

I think there may still exist a little problem for the entities without properties. This may be rare, but sometimes it's possible to get node with only relationships but no properties.

I just updated the reproducer. I created two simplified examples defined in with two new endpoints createNodeWithoutProperties1 and createNodeWithoutProperties2.

The expected graphe to have for the two examples:
createNodeWithoutProperties1:

(n1) -> (n2) -> (n3)

(n2)-> (m1)

createNodeWithoutProperties2:

(n1) -> (n2) -> (n3)

(n1)-> (m1)

Result by using the projections:
createNodeWithoutProperties1:

(n1) 

createNodeWithoutProperties2:

(n1)-> (m1)

Excuse me for creating strange test cases...

No worries, I really appreciate how hard you try to "challenge" me. :smiley:
It helps not only you but also the other users. But I need a little bit more input on this, I might miss something in my interpretation.

Given your projection:

public interface EntityWithOneLevelRelationshipProjection {
    Long getId();

    String getName();

    Map<String, String> getAdditionalProperties();

    Map<String, List<LinkWithoutTargetRelationshipProjection>> getRelationships();
}

This only mentions the relationships field. The n1.setOtherRelationships part will get ignored and as a consequence also the other relationships / nodes. Using n1.setRelationships with the right types, would do the magic.

Hi Gerrit,

Sorry for the late response. It took me some time trying to correct the reproducer. You are right, I made a mistake in my demo, I was a bit lost with the ugly variable names.

I have recreated other classes to make the demo clearer, since this time I don't think it's the problem of the projection. This time the problem happens even without projection but on the save.

You can find the a minimised demo here in the endpoint createRootNode: https://github.com/Pandalei97/neo4jDemoBugRelationship/blob/main/src/main/java/com/example/demo/controller/RootNodeController.java

The classes used in the demo are :
RootNode, PropertyNode, SinglePropertyNode and MultiplePropertyNode.

SinglePropertyNode, MultiplePropertyNode are inherited from PropertyNode.

Ideally, I should get a graphe
image

But with the reproducer, we've got
image

There may be a problem in the management of the inheritance. Because if we move the property name from PropertyNode into SinglePropertyNode and MultiplePropertyNode, we will get the expected graphe.

The problem here is the data model (and Lombok providing a false safety feeling).
You have to add @EqualsAndHashCode(callSuper = true) to the sub-classes SinglePropertyNode and MultiPropertyNode.
As you have right observed, the behaviour changes when pushing name down to the child entities. This would make @Data consider them for equals/hashCode. But it does not include them per default, if they are part of the parent class.
As a consequence, SDN will consider the M2 as the very same object as M1 and not persist M2 at all. Of course when not even looking into M2 it won't discover the connection to S3. (The outcome depends on the random order, the relationships are processed, sometimes it could also be that M2 is first in the "cache" and M1 is the skipped entity).

On a side note and before the question arises: I do not know the details, how Lombok processes the Maps. Given the fact that all PropertyNodes are the same, even a deep equals into the elements will evaluate to true because in the end, they only differ in their names.

Yes, we should not rely too much on Lombok and we will take more care of the equator. Thanks for the detailed explications !

I have no more questions for the moment. I really appreciate our exchanges these days, I have really learned a lot ! :smiley:

1 Like

I can only second this. It is good to see people digging deep into the functionality we provide and find problems with corner cases.
Fun fact: When I implemented the projections for the dynamic relationships on Wednesday, I also remembered why we have forbidden this in the first place and the flag isDynamicRelationship would not process.
But now we have dropped Spring Data commons' strict PropertyPath at this place to be more flexible.
Thanks a lot for your patience and detailed reports. :clap: