2

I am using Neo4j (version 3.4.1) and Spring-data-neo4j (5.0.10.RELEASE) in my application. I am also using OGM.

I have the following domain model (Vehicle and Part):

Vehicle class

@NodeEntity
@Data
@NoArgsConstructor
public class Vehicle {

@Id
@GeneratedValue
private Long id;

@Relationship(type = "HAS_PART", direction = Relationship.OUTGOING)
private List<Part> parts = new ArrayList<>();
}

Part class

@NodeEntity
@Data
@NoArgsConstructor
public class Part {

@Id
private String name;

public Part(String name) {
    this.name = name;
}
}

I am able to set parts for a given vehicle and store the nodes in the Neo4j database using a spring data neo4j repository interface.

    Vehicle myVehicle = new Vehicle();

    Part brake = new Part("brake");
    Part accelerator= new Part("accelerator");
    Part clutch= new Part("clutch");

    myVehicle.setParts(Arrays.asList(brake, accelerator, clutch));
    Vehicle vehicle = vehicleRepository.save(myVehicle);
    System.out.println("vehicle = " + vehicle);

I am able to see both the Vehicle and Part nodes in the database using Neo4j browser, something as shown below :

enter image description here

The problem that I am facing is during updating of the existing vehicle. I am trying to change the parts that the vehicle has. I am doing as follows:

    Vehicle myVehicle = vehicleRepository.findById(582L).get();

    Part brake = new Part("brake");
    Part accelerator= new Part("accelerator");

    myVehicle.setParts(Arrays.asList(brake, accelerator));
    Vehicle savedVehicle = vehicleRepository.save(myVehicle);
    System.out.println("vehicle = " + savedVehicle);

In the IDE debug session, I can see that the savedVehicle object (returned after calling save on vehicleRepository) has only 2 parts "brake" and "accelerator". It doesn't have "clutch" part in it, the way I want it to be.

However, when I check the same vehicle object in the Neo4j database using browser, I see that the vehicle object still has 3 parts (even the "clutch").

I am not able to understand why is clutch node still related to the vehicle node as I have overwritten the node and its relationship. Also why is the savedVehicle object different from one in the database.

Is someone able to shed some light on this? Also how can I remove the HAS_PART relationship with clutch node on save/update.

Regards, V

LearnToLive
  • 462
  • 9
  • 25

1 Answers1

2

A good way to understand what's going on is to add logging.level.org.neo4j.ogm.drivers.bolt=DEBUG to your application.properties. This will give you the actual cypher being executed in the logs. You can see that your code translates to MERGE statements like below.

Request: UNWIND {rows} as row MERGE (n:`Vehicle`{name: row.props.name}) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeRef=-1, props={name=Car}}]}
Request: UNWIND {rows} as row MERGE (n:`Part`{name: row.props.name}) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeRef=-3, props={name=brake}}, {nodeRef=-5, props={name=accelerator}}, {nodeRef=-7, props={name=clutch}}]}

The MERGE clause ensures that a pattern exists in the graph. Either the pattern already exists, or it needs to be created. It will not remove any existing nodes.

Coming to the solution, there are few different ways to achieve what you're looking for, the naive solution would be to do something like below:

Part p = new Part("brake");
partRepository.delete(p);

However, notice this will delete all the parts with name as "brake" against ALL vehicle nodes. Which may not be what you want always. For Eg. You may want to detach clutch from only vehicles with name of Car.

Say you had a name property on the vehicle label, you could do something like this in your code, to detach & delete clutch from a Vehicle with name Car (if you don't have a name for Vehicle, use id or another unique property):

Vehicle car = vehicleRepository.findByName("Car");
for(Part p: car.getParts()){
    if("clutch".equalsIgnoreCase(p.getName())){
         partRepository.delete(p);
    }
}

Another option is to write a method in your VehicleRepository with @Query annotation to execute the equivalent of below query:

MATCH(n:Vehicle {name:"Car"})-[:HAS_PART]->(p:Part{ name:"brake"}) 
DETACH DELETE p

Then you can simply invoke it from your service class.

See here for examples.


Edit: Adding more info on deleting only the relation between Vehicle and a Part.

This can be done by writing a custom query in the vehicleRepository like below (I assume you have a name property on Vehicle as well, if not you can use id instead of name):

@Query("MATCH(n:Vehicle {name: {0}})-[r:HAS_PART]-(p:Part{ name:{1}}) DELETE r")
Vehicle detachPartFromVehicle(@Param("partName") String vehicleNameName, @Param("partName") String partName);

Now, from your code you can just invoke it, for eg.:

vehicleRepository.detachPartFromVehicle("Car", "clutch");

This should result in deleting the HAS_PART relation -- the clutch part will remain in the graph, but it will get disconnected from the Vehicle Car.

Further, you can disconnect multiple parts by converting this into an IN query:

@Query("MATCH(n:Vehicle {name: {0}})-[r:HAS_PART]-(p:Part) WHERE p.name IN {1} DELETE r")
Vehicle detachPartFromVehicle(@Param("partName") String vehicleNameName, @Param("partNames") String[] partNames);

.

vehicleRepository.detachPartFromVehicle("Car", new String[] {"clutch", "brake"});
Bajal
  • 5,487
  • 3
  • 20
  • 25
  • Thanks @Bajal. You were bang on with the your explanation. It does a `MERGE` to check if relationship exists and creates one if it doesn't exist. It doesn't delete any relationships. I was able to resolve my issue by delete the existing node and saving the updated node. I had to set the `id` as `null` though as Neo4j didn't save the node with same id. It saved the node as a completely new node (which was ok in my case) However, a question which still remains unanswered is why did the entity returned on `save` show only 2 `Part` nodes and not 3 as the case was in db. This is so misleading. – LearnToLive Dec 26 '18 at 11:28
  • Based on @Bajal's answer and his query, I decided to explore if I can achieve the update behaviour by expanding on his query, which would delete old parts and add any new ones as well and return the updated vehicle back. Here is my attempt at the same: `with ['seat'] AS newParts` `unwind newParts as newPart` `match (r:Vehicle) where id(r)=639` `merge (r)-[:HAS_PART]->(:Part{name:newPart})` `with r optional match (r)-[rel:HAS_PART]-(p:Part) where p.name in ['brake']` `delete rel` `with r match q=(r)--()` `return nodes(q), relationships(q)` – LearnToLive Dec 27 '18 at 15:11