productioncoder
productioncoder

Reputation: 4335

Cypher query - Optional Create

I am trying to create a social network-like structure. I would like to create a timeline of posts which looks like this

(user:Person)-[:POSTED]->(p1:POST)-[:PREV]->[p2:POST]...

My problem is the following. Assuming a post for a user already exists, I can create a new post by executing the following cypher query

MATCH (user:Person {id:#id})-[rel:POSTED]->(prev_post:POST)
DELETE rel
CREATE (user)-[:POSTED]->(post:POST {post:"#post", created:timestamp()}),
       (post)-[:PREV]->(prev_post);

Assuming, the user has not created a post yet, this query fails. So I tried to somehow include both cases (user has no posts / user has at least one post) in one update query (I would like to insert a new post in the "post timeline")

MATCH (user:Person {id:"#id"})
OPTIONAL MATCH (user)-[rel:POSTED]->(prev_post:POST)
CREATE (post:POST {post:"#post2", created:timestamp()})
FOREACH (o IN CASE WHEN rel IS NOT NULL THEN [rel] ELSE [] END |
  DELETE rel
)
FOREACH (o IN CASE WHEN prev_post IS NOT NULL THEN [prev_post] ELSE [] END |
  CREATE (post)-[:PREV]->(o)
)
MERGE (user)-[:POSTED]->(post)

Is there any kind of if-statement (or some type of CREATE IF NOT NULL) to avoid using a foreach loop two times (the query looks a litte bit complicated and I know that the loop will only run 1 time)?.

However, this was the only solution, I could come up with after studying this SO post. I read in an older post that there is no such thing as an if-statement.

EDIT: The question is: Is it even good to include both cases in one query since I know that the "no-post case" will only occur once and that all other cases are "at least one post"?

Cheers

Upvotes: 3

Views: 1467

Answers (4)

flash4syth
flash4syth

Reputation: 48

In the Neo4j v3.2 developer manual it specifies how you can create essentially a composite key made of multiple node properties at this link:

CREATE CONSTRAINT ON (n:Person) ASSERT (n.firstname, n.surname) IS NODE KEY

However, this is only available for the Enterprise Edition, not Community.

Upvotes: 2

I've seen a solution to cases like this in some articles. To use a single query for all cases, you could create a special terminating node for the list of posts. A person with no posts would be like:

(:Person)-[:POSTED]->(:PostListEnd)

Now in all cases you can run the query:

MATCH (user:Person {id:#id})-[rel:POSTED]->(prev_post)
DELETE rel
CREATE (user)-[:POSTED]->(post:POST {post:"#post", created:timestamp()}),
       (post)-[:PREV]->(prev_post);

Note that the no label is specified for prev_post, so it can match either (:POST) or (:PostListEnd).

After running the query, a person with 1 post will be like:

(:Person)-[:POSTED]->(:POST)-[:PREV]->(:PostListEnd)

Since the PostListEnd node has no info of its own, you can have the same one node for all your users.

Upvotes: 2

cybersam
cybersam

Reputation: 66967

I also do not see a better solution than using FOREACH.

However, I think I can make your query a bit more efficient. My solution essentially merges the 2 FOREACH tests into 1, since prev_postand rel must either be both NULL or both non-NULL. It also combines the CREATE and the MERGE (which should have been a CREATE, anyway).

MATCH (user:Person {id:"#id"})
OPTIONAL MATCH (user)-[rel:POSTED]->(prev_post:POST)
CREATE (user)-[:POSTED]->(post:POST {post:"#post2", created:timestamp()})
FOREACH (o IN CASE WHEN prev_post IS NOT NULL THEN [prev_post] ELSE [] END |
  DELETE rel
  CREATE (post)-[:PREV]->(o)
)

Upvotes: 2

BtySgtMajor
BtySgtMajor

Reputation: 1462

"CASE" is as close to an if-statement as you're going to get, I think.

The FOREACH probably isn't so bad given that you're likely limited in scope. But I see no particular downside to separating the query into two, especially to keep it readable and given the operations are fairly small.

Just my two cents.

Upvotes: 0

Related Questions