Neo4j- questions on Syntax of apoc trigger, procedure and WITH statements

I have the following APOC trigger which contains a apoc procedure. This works as desired but I am wondering if I used a lot of WITH statements, is that the way to go? or is there a better way to refactor this code. I am still new to neo4j so not sure.

The control flow- Whenever the Enrollment node is created, it creates an SPerson node and depending on the size(number of SPerson nodes) it creates an enrolled or waitlist(if size>3, and deletes enrolled relation) relation with the course node.

The MATCH (e:Enrollment) DETACH DELETE e is to delete any 'enollment' node after creation. I don't want any 'enrollment' nodes in neo4j at the end. They are only here for starting the trigger. The nodes that have a purpose are the 'SPerson' nodes which will have properties(created from the enrollment nodes and enrollment nodes will then get deleted) and relations with the course node.
For Example
CREATE (:Enrollment { name: "Manish", cell: "99876", created_at: TIMESTAMP()});
#This will start the trigger and create a 'SPerson' node with a 'enrolled' relationship with the 'course' node.

My code

CALL apoc.trigger.add('loadEnrollments',   
"UNWIND apoc.trigger.nodesByLabel($assignedLabels, 'Enrollment') AS node
 MERGE (p1:SPerson { name: node.name, cell: node.cell, created_at: node.created_at})
 WITH p1, node
 MATCH (c:Course {name: 'Paradigm Shifting 101'})
 WITH node
 MATCH (n:SPerson)
 WITH node, COUNT(n) as size
 CALL apoc.do.when(
 size>3,
 'MATCH(p1:SPerson),(c:Course) 
 WHERE p1.name=node.name
 CREATE (p1)-[:Waitlist]->(c)
 WITH p1,c 
 MATCH (e:Enrollment) DETACH DELETE e',
 'MATCH(p1:SPerson),(c:Course) 
 WHERE p1.name=node.name
 CREATE (p1)-[:Enrolled]->(c)
 WITH p1,c 
 MATCH (e:Enrollment) DETACH DELETE e', {node:node}) YIELD value
 DETACH DELETE node", 
 
 { phase: 'after' });

There's no way that's doing what you think it is.

  1. You can run multiple MATCH statements.
    • WITH p1, node MATCH (c:Course {...}) MATCH (n:SPerson) ...
  2. When you use WITH only the variables included in the WITH clause will be available for subsequent commands.
    • MATCH (c:Course {name: 'Paradigm Shifting 101'}) does nothing.
    • MATCH(p1:SPerson),(c:Course) WHERE p1.name=node.name CREATE (p1)-[:Waitlist]->(c) will put that person on a waitlist for every course.

I see what you're going for, and while I could rewrite your Cypher for you, I think that will be hurting more than helping. Work on the smaller pieces first, and make sure they're doing what you think, before trying to plug it all together.

1 Like

Thank you Tony for the feedback. I really appreciate it. I rewrote my query. Would love to hear your thoughts.

 CALL apoc.trigger.add('loadEnrollments',
"UNWIND apoc.trigger.nodesByLabel($assignedLabels, 'Enrollment') AS node
 MERGE (p1:SPerson { name: node.name, cell: node.cell, created_at: node.created_at})
 WITH p1, node
 MATCH (n:SPerson)
 WITH node, COUNT(n) as size
 CALL apoc.do.when(
 size>3,
 '
 MATCH(p1:SPerson),(c:Course)
 WHERE p1.name=node.name AND c.name=\"Paradigm Shifting 101\"
 CREATE (p1)-[:Waitlist]->(c)
 WITH node
 RETURN NULL',
 '
 MATCH(p1:SPerson),(c:Course)
 WHERE p1.name=node.name AND c.name=\"Paradigm Shifting 101\"
 CREATE (p1)-[:Enrolled]->(c)
 WITH node
 RETURN NULL', {node:node}) YIELD value
 DETACH DELETE node",

 { phase: 'after' })

That's a step in the right direction.

UNWIND apoc.trigger.nodesByLabel($assignedLabels, 'Enrollment') AS node
MERGE (p1:SPerson { name: node.name, cell: node.cell, created_at: node.created_at})
WITH p1, node
MATCH (n:SPerson)  # are you trying to get a total of all nodes with the :SPerson label? 
WITH node, COUNT(n) as size

At this point, you only have 2 variables in context: node, and size, where size will always be the count of MATCH(:SPerson). I suspect you are instead attempting to count the number of matching :SPerson nodes, for which you only need the p1 variable.

Your existing query is again, doing nothing with the p1 variable you first create, and instead find it a second time within the do.when block.

Instead:

WITH p1, node, COUNT(p1) as size
CALL apoc.do.when( size>3 ,
'
MATCH (c:Course {name: \"Paradigm Shifting 101\"})
MERGE (p1)-[:Waitlist]->(c)
'
...

That should be all you really need there, to do what it looks like you're trying to do with the code, but several things still escape me. For instance, why does it matter the count of SPeople, and why don't you have the course-name in the :Enrollment node?

Since you've put some effort into it though, I'll give you something functional, but I strongly advise you carefully analyze why this works, and try to better understand exactly what yours was doing differently.

I'll assume the course-name is in the :Enrollment node.

UNWIND apoc.trigger.nodesByLabel($assignedLabels, 'Enrollment') AS node
MERGE (p1:SPerson { name: node.name, cell: node.cell, created_at: node.created_at})
WITH p1, node
MATCH (c:Course {name: node.course_name})<-[:Enrolled]-(n:SPerson)
WITH node, p1, c, COUNT(n) as size

# DO WHEN size (number of currently enrolled) <= 4 (no more than 4 enrolled at one time?)
MERGE (p1)-[:Enrolled]->(c)  // MERGE to prevent re-adding the same person multiple times
DETACH DELETE node

# ELSE
MERGE (p1)-[:Waitlist]->(c)
DETACH DELETE node

It seems you're struggling a bit with variables, and script context. Remember the following simple rules:

  1. When doing (variable :Thing), every matching node is available in the variable.
  2. WITH is only sometimes needed, and it specifies exactly which variables will stay in the context.
    • MATCH (a)-[r]-(b) WITH a will drop r and b. Only a will be able to be used in the rest of the script.
  3. MATCH early, then operate on the variables.
    • If you're repeatedly calling MATCH to find a reference to a node that you've previously had a reference to, you're doing something wrong. Worse, if you do a single DETACH or DELETE on that node, between the MATCH statements for it, you're setting yourself up for some serious problems in the transaction.

A better way.

It looks like you're trying to handle a student request to enroll in a class. If we adjust some terms to reflect better graph design, things will be simplified. Labels are singular nouns, and rels are verbs.

(s:Student {id, name})
(c:Class {id, name, max_students})
(e:Enrollment {student_id, student_name, class_id})
(s)<-[:Applying]-(a)
(s)-[:Enrolled]->(c)
(s)-[:Waiting]->(c)

Rather than going all fancy with triggers, we can handle this with a MERGE when creating the Enrollment:

MATCH (s:Student: {id: 123}), (c:Class {id:456})<-[:Enrolled]-(current:Student)
MERGE (e:Enrollment {student_id: s.id, class_id: c.id})
WITH s, c, a, count(current) as numEnrolled
CALL apoc.do.when(
   numEnrolled <=4,
   "MERGE (s)-[:Enrolled]->(c)",
   "MERGE (s)-[:Waiting]->(c)"
)
DETACH DELETE Enrollment;

This makes it clear that even the Enrollment label itself is unnecessary, if we handle this when attempting to create the Enrollment to begin with:

:params {student_id: 123, class_id: 456}
MATCH (s:Student: {id: $student_id}), (c:Class {id: $class_id})<-[:Enrolled]-(current:Student)
WITH s, c, count(current) as numEnrolled
CALL apoc.do.when(
   numEnrolled <=4,
   "MERGE (s)-[:Enrolled]->(c)",
   "MERGE (s)-[:Waiting]->(c)"
)
2 Likes