Rehydrating @RelationshipEntity without the rel 'id'

I want to use session.save() to update a @NodeEntity and its subgraph via @RelationshipEntity. I also have a situation where it is not convenient to have the ids of the @RelationshipEntity's on hand.

If I do not have the @Id value set, it appears to duplicate existing relationships, resulting in multiple copies of any outgoing or incoming relationship.

Is there a way to avoid this double-binding situation, without that id?

I think it's reasonable that it doesn't work without the id (much as it wouldn't for a node). While I have a workaround, now I'm encountering an issue with session.save(). It seems to act like MERGE, where it will update nodes or relationships, but not delete them if they are omitted. The docs for session.save are pretty light – is this supposed to be supported?

For example, take a node with a list of @RelationshipEntities, remove one and save the result. This does not actually detach the removed endNode. I note the docs do mention:

Under the bonnet, the implementation of Session has access to the MappingContext that keeps track of the data that has been loaded from Neo4j during the lifetime of the session

However, I did not originally load the data during the session's lifetime; it was rehydrated elsewhere, prior to the session. Would that account for lack of destructive delta support?

I'm still struggling with this. Sorry to mention @gerrit.meier, but I know you're intimately familiar with the OGM.

So, I know that mostly people use Session.save() after using Session.load, or otherwise getting data directly from the graph. In my case, I want to use Session.save() on "rehydrated" data, i.e. data which has been communicated with another party via a RESTful interface. When I get that data back, it definitely has all the node @Id values, but not the relationship @Ids. This is fine for freestanding graphs, but problematic if they connect to preexisting nodes.

While I've gotten weird results (per my OP) or errors by faking these relationship IDs in the graph I send to save(), nothing works. It appears I have to have those relationship IDs, or the OGM will not work properly.

Would you say this is just a hard fact of life? I wouldn't be surprised, although I've secretly hoped there might be some workaround.

Thanks!

Before I can give you a more detailed answer, I will try to recap this in my own words. Please tell me if I got it correctly.

Just to have some names I will call the NodeEntity Person and the RelationshipEntity LivesAt.

@NodeEntity
public Person {
  @Relationship(type = "LIVES_AT")
  List<LivesAt> addresses;

  // id and other values omitted
}
@RelationshipEntity("LIVES_AT")
public LivesAt {
  @StartNode
  Person person;
  @EndNode
  Address address;
  // id and other values omitted
}

setting: a person has homeAddress and workAddress in the graph.

From what I understand you are getting a person from an external source but it lacks of the id information for the addresses, right?

You are absolutely right that without doing loading and saving within one transaction Neo4j-OGM cannot track changes to those collections and will duplicate the relationships. The same is true for relationships without any id value because it assumes that they are new.

If you get a "full" set of data back from your REST layer, you could do this: (I know that the naming is not accurate but I speak about Neo4j and REST data just to distinguish where the data comes from)

  • with a hydrated (REST) person and its addresses
  1. reload the person from Neo4j with its addresses (transaction starts here)
  2. find the matching (Neo4j) addresses to the person's (REST) addresses
  3. set the id of the (Neo4j) address to the (REST) address
  4. save back the person (transaction ends here)

I don't know if this is manageable for you but you should consider to work in a somehow more node centric way in your code to avoid the duplicated relationships. Just "patching" an address without any known context of it (because it was not loaded within the same transaction/session) will confuse Neo4j-OGM. Also I suggest -thinking Domain Driven Design- that you should have a clear model of your domain (aggregates) with strict definitions "from which side" (aggregate root) to access your data.

1 Like

Your restatement of the problem is correct, thank you.

So for updating a subgraph, I'd need to load it, then traverse both old and new graphs in memory, reassigning lost relationship IDs. Makes sense.

Where I'm lost is when I want to save a new subgraphs that intersect with the existing graph. In this case, most of the nodes being saved have no ID, and would be created properly by the save(). The ones that do exist include the node ID, but in those cases, no rel-ID exists anywhere yet. Using a null rel-ID fails with an error that the node with already exists (correct) but the relationship doesn't, which is what I'm trying to establish.

Perhaps I need to pre-load all of these external nodes into the session first?

I'm also willing to concede I might be going outside the bounds of what the OGM is really intended for, here, and should probably entertain the prospect of building a custom extension or some crazy cypher to handle this merge-mapping. This whole thing would be moot if I only had a few such entities and relationships, but there are many, interconnected in a wide variety of ways.

By this point, instead of looking for a universal solution, I'm trying to special-case it for the few places it's indispensable. But even so, hitting strange issues. In the query below, it works fine if I omit any one of the WITH sr_s UNWIND... clause. But with all 3, I get a strange internal error:

ERROR 8668 --- [o4jDriverIO-2-2] ChannelErrorHandler                      : [0xb2f0fa0a][] Fatal error occurred in the pipeline

org.neo4j.driver.v1.exceptions.DatabaseException: (  _@334,RefSlot(2,true,Any)) (of class scala.Tuple2)
	at org.neo4j.driver.internal.util.ErrorUtil.newNeo4jError(ErrorUtil.java:72) ~[neo4j-java-driver-1.7.0.jar:1.7.0-b583401e539a45fc508a79038cb6a85bdccbd9e5]
	at org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher.handleFailureMessage(InboundMessageDispatcher.java:105) ~[neo4j-java-driver-1.7.0.jar:1.7.0-b583401e539a45fc508a79038cb6a85bdccbd9e5]

This on Neo4j 3.4.5-enterprise. And the query (may include fresh or existing nodes):

MATCH (sr_s:Space { uuid: {map}.selfMap.uuid })
SET sr_s += {map}.selfMap

WITH sr_s
UNWIND range(0, size({map}.aggregatedAxes) - 1) AS index
WITH sr_s, index, {map}.aggregatedAxes[index] AS axisMap
OPTIONAL MATCH (sr_s)-[A:AGGREGATES]->(a:Axis { uuid: axisMap.selfMap.uuid })
FOREACH (_ IN CASE WHEN a IS NULL THEN [true] ELSE [] END |
    CREATE (sr_s)-[:AGGREGATES { order: index }]->(a2:Axis)
    SET a2 += axisMap.selfMap
)
SET a += axisMap.selfMap, A += { order: index }
WITH sr_s
UNWIND {map}.projectedAxes AS axisMap
OPTIONAL MATCH (sr_s)-[:PROJECTS]->(a:Axis { uuid: axisMap.selfMap.uuid })
FOREACH (_ IN CASE WHEN a IS NULL THEN [true] ELSE [] END |
    CREATE (sr_s)-[:PROJECTS]->(a2:Axis)
    SET a2 += axisMap.selfMap
)
SET a += axisMap.selfMap
WITH sr_s
UNWIND {map}.points AS pointMap
OPTIONAL MATCH (sr_s)-[:CONTAINS]->(p:Point { uuid: pointMap.selfMap.uuid })
FOREACH (_ IN CASE WHEN p IS NULL THEN [true] ELSE [] END |
    CREATE (sr_s)-[:CONTAINS]->(p2:Point)
    SET p2 += pointMap.selfMap
)
SET p += pointMap.selfMap

WITH sr_s
OPTIONAL MATCH (sr_s)-[sr_AG:AGGREGATES]->(sr_ag:Axis)
OPTIONAL MATCH (sr_s)-[sr_PR:PROJECTS]->(sr_pr:Axis)
OPTIONAL MATCH (sr_s)-[sr_C:CONTAINS]->(pt_p:Point)
OPTIONAL MATCH (pt_p)-[pt_A:ALONG]->(pt_a:Axis)
OPTIONAL MATCH (sr_s)-[sr_P:PRESENTS]->(ir_i:Insight)
OPTIONAL MATCH (ir_i)-[ir_T:TITLED]->(ir_mdT:MessageDefinition)
OPTIONAL MATCH (ir_i)-[ir_S:STATES]->(ir_mdS:MessageDefinition)
OPTIONAL MATCH (ir_i)-[ir_AT:AT]->(pt_p:Point)
RETURN *

I tried ensuring the rel and node names were unique (despite the WITH); didn't help. All the <map>.selfMap contain primitives or (in one case) array of primitives. Thanks for any insight!