Leveraging the Power of
Graph Databases
in PHP
Jeremy Kendall
Nashville PHP
November 2014
Obligatory Intro Slide
Also - New Father
What Kind of
Database?
Graphs != Charts
https://www.flickr.com/photos/markgroves/3065192499/
Graphs != Charts
http://stephenwildish.tumblr.com/post/101408321763/friday-project-witch-moral-compass
Graph Databases
Graph Databases
• Data Model!
• Nodes with properties
• Typed relationships
Graph Databases
• Data Model!
• Nodes with properties
• Typed relationships
• Strengths!
• Highly connected data
• ACID
Graph Databases
• Data Model!
• Nodes with properties
• Typed relationships
• Strengths!
• Highly connected data
• ACID
• Weaknesses!
• Paradigm shift
Graph Databases
• Data Model!
• Nodes with properties
• Typed relationships
• Strengths!
• Highly connected data
• ACID
• Weaknesses!
• Paradigm shift
• Examples!
• Neo4j, Titan, OrientDB
Why Care?
Why Care?
• All the NoSQL Joy
• Schema-less
• Semi-structured data
Why Care?
• All the NoSQL Joy
• Schema-less
• Semi-structured data
• Escape from JOIN Hell
Why Care?
• All the NoSQL Joy
• Schema-less
• Semi-structured data
• Escape from JOIN Hell
• Speed
Why Care?
Why Care?
• Relationships have 1st class status
Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
• You can have properties & labels
Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
• You can have properties & labels
• Multiple relationships
Why Care?
Speed
Depth MySQL Query Time Neo4j Query Time Records Returned
2 0.028 (28 MS) 0.04 ~900
3 0.213 0.06 ~999
4 10.273 0.07 ~999
5 92.613 0.07 ~999
1,000 people with an average 50 friends each
Crazy Speed
Depth MySQL Query Time Neo4j Query Time Records Returned
2 0.016 (16 MS) 0.01 ~2500
3 30.27 0.168 ~125,000
4 1543.505 1.359 ~600,000
5 Stopped after 1 hour 2.132 ~800,000
1,000,000 people with an average 50 friends each
Neo4j + Cypher
Cypher
• Neo4j’s declarative query language
• Easy to pick up
• Some clauses and concepts familiar from SQL
Simple Example
Goal
Create Some Nodes
CREATE (jk:Person { name: "Jeremy Kendall" })!
CREATE (gs:Company { name: "Graph Story" })!
!
CREATE (tn:State { name: "Tennessee" })!
CREATE (memphis:City { name: "Memphis" })!
CREATE (nashville:City { name: "Nashville" })!
!
CREATE (hotchicken:Food { name: "Hot Chicken" })!
CREATE (bbq:Food { name: "Barbecue" })!
CREATE (photography:Hobby { name: "Photography" })!
CREATE (language:Language { name: "PHP" })!
!
// . . . snip . . .!
Create Some Relationships
// . . . snip . . .!
!
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),!
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),!
(hotchicken)-[:ONLY_IN]->(nashville),!
(bbq)-[:ONLY_IN]->(memphis),!
(jk)-[:LOVES]->(hotchicken),!
!
// . . . snip . . .!
Create Some Relationships
// . . . snip . . .!
!
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),!
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),!
(hotchicken)-[:ONLY_IN]->(nashville),!
(bbq)-[:ONLY_IN]->(memphis),!
(jk)-[:LOVES]->(hotchicken),!
!
// . . . snip . . .!
Create Some Relationships
// . . . snip . . .!
!
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),!
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),!
(hotchicken)-[:ONLY_IN]->(nashville),!
(bbq)-[:ONLY_IN]->(memphis),!
(jk)-[:LOVES]->(hotchicken),!
!
// . . . snip . . .!
Create Some Relationships
// . . . snip . . .!
!
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),!
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),!
(hotchicken)-[:ONLY_IN]->(nashville),!
(bbq)-[:ONLY_IN]->(memphis),!
(jk)-[:LOVES]->(hotchicken),!
!
// . . . snip . . .!
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)!
WITH p, l!
MATCH (p)-[:WORKS_AT]->(j)!
WITH p, l, j!
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)!
RETURN p, l, j, o
Query Result
Neo4j + PHP
Neo4jPHP
• PHP wrapper for the Neo4j REST API
• Installable via Composer
• Used internally at Graph Story
• Used in this presentation
• Well tested
• https://packagist.org/packages/everyman/
neo4jphp
Also see: NeoClient
• Written by Neoxygen
• Alternative PHP wrapper for the Neo4j REST API
• Installable via Composer
• Under review for internal use at Graph Story
• Well tested
• https://packagist.org/packages/neoxygen/neoclient
Connecting
$neo4jClient = new EverymanNeo4jClient(!
‘yourgraph.example.com’, !
7473!
);!
!
$neo4jClient->getTransport()!
->setAuth('username', 'password')!
->getTransport()->useHttps();
Creating a Node and Label
$node = new Node($neo4jClient);!
!
$label = $neo4jClient->makeLabel('Person');!
!
$node->setProperty('name', ‘Jeremy Kendall');!
!
$node->save()->addLabels(array($label));
Searching
// Searching for a label by property!
$label = $neo4jClient->makeLabel('Person');!
$nodes = $label->getNodes('name', $name);
Querying (Cypher)
$queryString = !
'MATCH (p:Person { name: { name }}) RETURN p';!
!
$query = new EverymanNeo4jCypherQuery(!
$neo4jClient,!
$queryString,!
['name' => ‘Jeremy Kendall']!
);!
!
$result = $query->getResultSet();
Named Parameters
Named Parameters
$queryString = !
'MATCH (p:Person { name: { name }}) RETURN p';!
!
$query = new EverymanNeo4jCypherQuery(!
$neo4jClient,!
$queryString,!
['name' => ‘Jeremy Kendall']!
);!
!
$result = $query->getResultSet();
Named Parameters
$queryString = !
'MATCH (p:Person { name: { name }}) RETURN p';!
!
$query = new EverymanNeo4jCypherQuery(!
$neo4jClient,!
$queryString,!
['name' => ‘Jeremy Kendall']!
);!
!
$result = $query->getResultSet();
Content Modeling:
News Feeds
News Feed
• Modeled as a list of posts
• Newest post first
• All subsequent posts follow
• Relationships: LASTPOST and NEXTPOST
LASTPOST
NEXTPOST
The Content Model
class Content!
{!
public $node;!
public $nodeId;!
public $contentId;!
public $title;!
public $url;!
public $tagstr;!
public $timestamp;!
public $userNameForPost;!
public $owner = false;!
}
Adding Content
public static function add($username, Content $content)!
{!
$queryString =<<<CYPHER!
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner!
CYPHER;!
!
$query = new Query(!
Neo4jClient::client(),!
$queryString,!
array(!
'u' => $username,!
'title' => $content->title,!
'url' => $content->url,!
'tagstr' => $content->tagstr,!
'timestamp' => time(),!
'contentId' => uniqid()!
)!
);!
$result = $query->getResultSet();!
!
return self::returnMappedContent($result);!
}
Adding Content
public static function add($username, Content $content)!
{!
$queryString =<<<CYPHER!
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner!
CYPHER;!
!
$query = new Query(!
Neo4jClient::client(),!
$queryString,!
array(!
'u' => $username,!
'title' => $content->title,!
'url' => $content->url,!
'tagstr' => $content->tagstr,!
'timestamp' => time(),!
'contentId' => uniqid()!
)!
);!
$result = $query->getResultSet();!
!
return self::returnMappedContent($result);!
}
Adding Content
public static function add($username, Content $content)!
{!
$queryString =<<<CYPHER!
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner!
CYPHER;!
!
$query = new Query(!
Neo4jClient::client(),!
$queryString,!
array(!
'u' => $username,!
'title' => $content->title,!
'url' => $content->url,!
'tagstr' => $content->tagstr,!
'timestamp' => time(),!
'contentId' => uniqid()!
)!
);!
$result = $query->getResultSet();!
!
return self::returnMappedContent($result);!
}
Adding Content
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})!
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)!
DELETE r!
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })!
WITH p, collect(lastpost) as lastposts!
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)!
RETURN p, {u} as username, true as owner
Adding Content
$query = new Query(!
$neo4jClient,!
$queryString,!
array(!
'u' => $username,!
'title' => $content->title,!
'url' => $content->url,!
'tagstr' => $content->tagstr,!
'timestamp' => time(),!
'contentId' => uniqid()!
)!
);!
!
$result = $query->getResultSet();
Retrieving Content
public static function getContent($username, $skip)!
{!
$queryString = <<<CYPHER!
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4!
CYPHER;!
!
$query = new Query(!
Neo4jClient::client(),!
$queryString,!
array(!
'u' => $username,!
'skip' => $skip,!
)!
);!
!
$result = $query->getResultSet();!
!
return self::returnMappedContent($result);!
}
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f!
WITH DISTINCT f, u!
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p!
RETURN p, f.username as username, f = u as owner!
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Editing Content
public static function edit(Content $content)!
{!
$updatedAt = time();!
!
$node = $content->node;!
$node->setProperty('title', $content->title);!
$node->setProperty('url', $content->url);!
$node->setProperty('tagstr', $content->tagstr);!
$node->setProperty('updated', $updatedAt);!
$node->save();!
!
$content->updated = $updatedAt;!
!
return $content;!
}
Editing Content
public static function edit(Content $content)!
{!
$updatedAt = time();!
!
$node = $content->node;!
$node->setProperty('title', $content->title);!
$node->setProperty('url', $content->url);!
$node->setProperty('tagstr', $content->tagstr);!
$node->setProperty('updated', $updatedAt);!
$node->save();!
!
$content->updated = $updatedAt;!
!
return $content;!
}
Editing Content
public static function edit(Content $content)!
{!
$updatedAt = time();!
!
$node = $content->node;!
$node->setProperty('title', $content->title);!
$node->setProperty('url', $content->url);!
$node->setProperty('tagstr', $content->tagstr);!
$node->setProperty('updated', $updatedAt);!
$node->save();!
!
$content->updated = $updatedAt;!
!
return $content;!
}
Editing Content
public static function edit(Content $content)!
{!
$updatedAt = time();!
!
$node = $content->node;!
$node->setProperty('title', $content->title);!
$node->setProperty('url', $content->url);!
$node->setProperty('tagstr', $content->tagstr);!
$node->setProperty('updated', $updatedAt);!
$node->save();!
!
$content->updated = $updatedAt;!
!
return $content;!
}
Editing Content
public static function edit(Content $content)!
{!
$updatedAt = time();!
!
$node = $content->node;!
$node->setProperty('title', $content->title);!
$node->setProperty('url', $content->url);!
$node->setProperty('tagstr', $content->tagstr);!
$node->setProperty('updated', $updatedAt);!
$node->save();!
!
$content->updated = $updatedAt;!
!
return $content;!
}
Deleting Content
public static function delete($username, $contentId)!
{!
$queryString = self::getDeleteQueryString(!
$username, !
$contentId!
);!
!
$params = array(!
'username' => $username,!
'contentId' => $contentId,!
);!
!
$query = new Query(!
$neo4jClient,!
$queryString, !
$params!
);!
$query->getResultSet();!
}
Deleting Content
public static function delete($username, $contentId)!
{!
$queryString = self::getDeleteQueryString(!
$username, !
$contentId!
);!
!
$params = array(!
'username' => $username,!
'contentId' => $contentId,!
);!
!
$query = new Query(!
$neo4jClient,!
$queryString, !
$params!
);!
$query->getResultSet();!
}
Deleting Content
public static function delete($username, $contentId)!
{!
$queryString = self::getDeleteQueryString(!
$username, !
$contentId!
);!
!
$params = array(!
'username' => $username,!
'contentId' => $contentId,!
);!
!
$query = new Query(!
$neo4jClient,!
$queryString, !
$params!
);!
$query->getResultSet();!
}
Deleting Content
public static function delete($username, $contentId)!
{!
$queryString = self::getDeleteQueryString(!
$username, !
$contentId!
);!
!
$params = array(!
'username' => $username,!
'contentId' => $contentId,!
);!
!
$query = new Query(!
$neo4jClient,!
$queryString, !
$params!
);!
$query->getResultSet();!
}
Deleting Content: Leaf
// If leaf!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})!
WITH c!
MATCH (c)-[r]-()!
DELETE c, r
Deleting Content: Leaf
// If leaf!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})!
WITH c!
MATCH (c)-[r]-()!
DELETE c, r
Deleting Content: Leaf
// If leaf!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})!
WITH c!
MATCH (c)-[r]-()!
DELETE c, r
Deleting Content: Leaf
// If leaf!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})!
WITH c!
MATCH (c)-[r]-()!
DELETE c, r
Deleting Content: Leaf
// If leaf!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})!
WITH c!
MATCH (c)-[r]-()!
DELETE c, r
Deleting Content: LASTPOST
// If last!
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)!
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!
DELETE lp, del, np
Deleting Content: LASTPOST
// If last!
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)!
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!
DELETE lp, del, np
Deleting Content: LASTPOST
// If last!
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)!
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!
DELETE lp, del, np
Deleting Content: LASTPOST
// If last!
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)!
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)!
DELETE lp, del, np
Deleting Content: Other
// All other!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),!
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)!
CREATE UNIQUE (before)-[:NEXTPOST]->(after)!
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),!
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)!
CREATE UNIQUE (before)-[:NEXTPOST]->(after)!
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),!
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)!
CREATE UNIQUE (before)-[:NEXTPOST]->(after)!
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),!
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)!
CREATE UNIQUE (before)-[:NEXTPOST]->(after)!
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other!
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),!
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)!
CREATE UNIQUE (before)-[:NEXTPOST]->(after)!
DELETE del, delBefore, delAfter
Questions?
Thanks!
!
jeremy.kendall@graphstory.com
@JeremyKendall
http://www.graphstory.com

Leveraging the Power of Graph Databases in PHP

  • 1.
    Leveraging the Powerof Graph Databases in PHP Jeremy Kendall Nashville PHP November 2014
  • 2.
  • 3.
    Also - NewFather
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
    Graph Databases • DataModel! • Nodes with properties • Typed relationships
  • 9.
    Graph Databases • DataModel! • Nodes with properties • Typed relationships • Strengths! • Highly connected data • ACID
  • 10.
    Graph Databases • DataModel! • Nodes with properties • Typed relationships • Strengths! • Highly connected data • ACID • Weaknesses! • Paradigm shift
  • 11.
    Graph Databases • DataModel! • Nodes with properties • Typed relationships • Strengths! • Highly connected data • ACID • Weaknesses! • Paradigm shift • Examples! • Neo4j, Titan, OrientDB
  • 12.
  • 13.
    Why Care? • Allthe NoSQL Joy • Schema-less • Semi-structured data
  • 14.
    Why Care? • Allthe NoSQL Joy • Schema-less • Semi-structured data • Escape from JOIN Hell
  • 15.
    Why Care? • Allthe NoSQL Joy • Schema-less • Semi-structured data • Escape from JOIN Hell • Speed
  • 16.
  • 17.
    Why Care? • Relationshipshave 1st class status
  • 18.
    Why Care? • Relationshipshave 1st class status • Just as important as the objects connecting them
  • 19.
    Why Care? • Relationshipshave 1st class status • Just as important as the objects connecting them • You can have properties & labels
  • 20.
    Why Care? • Relationshipshave 1st class status • Just as important as the objects connecting them • You can have properties & labels • Multiple relationships
  • 21.
  • 22.
    Speed Depth MySQL QueryTime Neo4j Query Time Records Returned 2 0.028 (28 MS) 0.04 ~900 3 0.213 0.06 ~999 4 10.273 0.07 ~999 5 92.613 0.07 ~999 1,000 people with an average 50 friends each
  • 23.
    Crazy Speed Depth MySQLQuery Time Neo4j Query Time Records Returned 2 0.016 (16 MS) 0.01 ~2500 3 30.27 0.168 ~125,000 4 1543.505 1.359 ~600,000 5 Stopped after 1 hour 2.132 ~800,000 1,000,000 people with an average 50 friends each
  • 24.
  • 25.
    Cypher • Neo4j’s declarativequery language • Easy to pick up • Some clauses and concepts familiar from SQL
  • 26.
  • 27.
  • 28.
    Create Some Nodes CREATE(jk:Person { name: "Jeremy Kendall" })! CREATE (gs:Company { name: "Graph Story" })! ! CREATE (tn:State { name: "Tennessee" })! CREATE (memphis:City { name: "Memphis" })! CREATE (nashville:City { name: "Nashville" })! ! CREATE (hotchicken:Food { name: "Hot Chicken" })! CREATE (bbq:Food { name: "Barbecue" })! CREATE (photography:Hobby { name: "Photography" })! CREATE (language:Language { name: "PHP" })! ! // . . . snip . . .!
  • 29.
    Create Some Relationships //. . . snip . . .! ! CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! ! // . . . snip . . .!
  • 30.
    Create Some Relationships //. . . snip . . .! ! CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! ! // . . . snip . . .!
  • 31.
    Create Some Relationships //. . . snip . . .! ! CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! ! // . . . snip . . .!
  • 32.
    Create Some Relationships //. . . snip . . .! ! CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),! (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),! (hotchicken)-[:ONLY_IN]->(nashville),! (bbq)-[:ONLY_IN]->(memphis),! (jk)-[:LOVES]->(hotchicken),! ! // . . . snip . . .!
  • 33.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 34.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 35.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 36.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 37.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 38.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 39.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)! WITH p, l! MATCH (p)-[:WORKS_AT]->(j)! WITH p, l, j! MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)! RETURN p, l, j, o
  • 40.
  • 41.
  • 42.
    Neo4jPHP • PHP wrapperfor the Neo4j REST API • Installable via Composer • Used internally at Graph Story • Used in this presentation • Well tested • https://packagist.org/packages/everyman/ neo4jphp
  • 43.
    Also see: NeoClient •Written by Neoxygen • Alternative PHP wrapper for the Neo4j REST API • Installable via Composer • Under review for internal use at Graph Story • Well tested • https://packagist.org/packages/neoxygen/neoclient
  • 44.
    Connecting $neo4jClient = newEverymanNeo4jClient(! ‘yourgraph.example.com’, ! 7473! );! ! $neo4jClient->getTransport()! ->setAuth('username', 'password')! ->getTransport()->useHttps();
  • 45.
    Creating a Nodeand Label $node = new Node($neo4jClient);! ! $label = $neo4jClient->makeLabel('Person');! ! $node->setProperty('name', ‘Jeremy Kendall');! ! $node->save()->addLabels(array($label));
  • 46.
    Searching // Searching fora label by property! $label = $neo4jClient->makeLabel('Person');! $nodes = $label->getNodes('name', $name);
  • 47.
    Querying (Cypher) $queryString =! 'MATCH (p:Person { name: { name }}) RETURN p';! ! $query = new EverymanNeo4jCypherQuery(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']! );! ! $result = $query->getResultSet();
  • 48.
  • 49.
    Named Parameters $queryString =! 'MATCH (p:Person { name: { name }}) RETURN p';! ! $query = new EverymanNeo4jCypherQuery(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']! );! ! $result = $query->getResultSet();
  • 50.
    Named Parameters $queryString =! 'MATCH (p:Person { name: { name }}) RETURN p';! ! $query = new EverymanNeo4jCypherQuery(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']! );! ! $result = $query->getResultSet();
  • 51.
  • 52.
    News Feed • Modeledas a list of posts • Newest post first • All subsequent posts follow • Relationships: LASTPOST and NEXTPOST
  • 53.
  • 54.
  • 55.
    The Content Model classContent! {! public $node;! public $nodeId;! public $contentId;! public $title;! public $url;! public $tagstr;! public $timestamp;! public $userNameForPost;! public $owner = false;! }
  • 56.
    Adding Content public staticfunction add($username, Content $content)! {! $queryString =<<<CYPHER! MATCH (user { username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner! CYPHER;! ! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! $result = $query->getResultSet();! ! return self::returnMappedContent($result);! }
  • 57.
    Adding Content public staticfunction add($username, Content $content)! {! $queryString =<<<CYPHER! MATCH (user { username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner! CYPHER;! ! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! $result = $query->getResultSet();! ! return self::returnMappedContent($result);! }
  • 58.
    Adding Content public staticfunction add($username, Content $content)! {! $queryString =<<<CYPHER! MATCH (user { username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner! CYPHER;! ! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! $result = $query->getResultSet();! ! return self::returnMappedContent($result);! }
  • 59.
    Adding Content MATCH (user{ username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner
  • 60.
    Adding Content MATCH (user{ username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner
  • 61.
    Adding Content MATCH (user{ username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner
  • 62.
    Adding Content MATCH (user{ username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner
  • 63.
    Adding Content MATCH (user{ username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner
  • 64.
    Adding Content MATCH (user{ username: {u}})! OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)! DELETE r! CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} })! WITH p, collect(lastpost) as lastposts! FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)! RETURN p, {u} as username, true as owner
  • 65.
    Adding Content $query =new Query(! $neo4jClient,! $queryString,! array(! 'u' => $username,! 'title' => $content->title,! 'url' => $content->url,! 'tagstr' => $content->tagstr,! 'timestamp' => time(),! 'contentId' => uniqid()! )! );! ! $result = $query->getResultSet();
  • 66.
    Retrieving Content public staticfunction getContent($username, $skip)! {! $queryString = <<<CYPHER! MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4! CYPHER;! ! $query = new Query(! Neo4jClient::client(),! $queryString,! array(! 'u' => $username,! 'skip' => $skip,! )! );! ! $result = $query->getResultSet();! ! return self::returnMappedContent($result);! }
  • 67.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 68.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 69.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 70.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 71.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 72.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f! WITH DISTINCT f, u! MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p! RETURN p, f.username as username, f = u as owner! ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 73.
    Editing Content public staticfunction edit(Content $content)! {! $updatedAt = time();! ! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();! ! $content->updated = $updatedAt;! ! return $content;! }
  • 74.
    Editing Content public staticfunction edit(Content $content)! {! $updatedAt = time();! ! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();! ! $content->updated = $updatedAt;! ! return $content;! }
  • 75.
    Editing Content public staticfunction edit(Content $content)! {! $updatedAt = time();! ! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();! ! $content->updated = $updatedAt;! ! return $content;! }
  • 76.
    Editing Content public staticfunction edit(Content $content)! {! $updatedAt = time();! ! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();! ! $content->updated = $updatedAt;! ! return $content;! }
  • 77.
    Editing Content public staticfunction edit(Content $content)! {! $updatedAt = time();! ! $node = $content->node;! $node->setProperty('title', $content->title);! $node->setProperty('url', $content->url);! $node->setProperty('tagstr', $content->tagstr);! $node->setProperty('updated', $updatedAt);! $node->save();! ! $content->updated = $updatedAt;! ! return $content;! }
  • 78.
    Deleting Content public staticfunction delete($username, $contentId)! {! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );! ! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );! ! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();! }
  • 79.
    Deleting Content public staticfunction delete($username, $contentId)! {! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );! ! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );! ! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();! }
  • 80.
    Deleting Content public staticfunction delete($username, $contentId)! {! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );! ! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );! ! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();! }
  • 81.
    Deleting Content public staticfunction delete($username, $contentId)! {! $queryString = self::getDeleteQueryString(! $username, ! $contentId! );! ! $params = array(! 'username' => $username,! 'contentId' => $contentId,! );! ! $query = new Query(! $neo4jClient,! $queryString, ! $params! );! $query->getResultSet();! }
  • 82.
    Deleting Content: Leaf //If leaf! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }})! WITH c! MATCH (c)-[r]-()! DELETE c, r
  • 83.
    Deleting Content: Leaf //If leaf! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }})! WITH c! MATCH (c)-[r]-()! DELETE c, r
  • 84.
    Deleting Content: Leaf //If leaf! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }})! WITH c! MATCH (c)-[r]-()! DELETE c, r
  • 85.
    Deleting Content: Leaf //If leaf! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }})! WITH c! MATCH (c)-[r]-()! DELETE c, r
  • 86.
    Deleting Content: Leaf //If leaf! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }})! WITH c! MATCH (c)-[r]-()! DELETE c, r
  • 87.
    Deleting Content: LASTPOST //If last! MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost)! CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)! DELETE lp, del, np
  • 88.
    Deleting Content: LASTPOST //If last! MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost)! CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)! DELETE lp, del, np
  • 89.
    Deleting Content: LASTPOST //If last! MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost)! CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)! DELETE lp, del, np
  • 90.
    Deleting Content: LASTPOST //If last! MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost)! CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)! DELETE lp, del, np
  • 91.
    Deleting Content: Other //All other! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)! CREATE UNIQUE (before)-[:NEXTPOST]->(after)! DELETE del, delBefore, delAfter
  • 92.
    Deleting Content: Other //All other! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)! CREATE UNIQUE (before)-[:NEXTPOST]->(after)! DELETE del, delBefore, delAfter
  • 93.
    Deleting Content: Other //All other! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)! CREATE UNIQUE (before)-[:NEXTPOST]->(after)! DELETE del, delBefore, delAfter
  • 94.
    Deleting Content: Other //All other! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)! CREATE UNIQUE (before)-[:NEXTPOST]->(after)! DELETE del, delBefore, delAfter
  • 95.
    Deleting Content: Other //All other! MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before),! (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after)! CREATE UNIQUE (before)-[:NEXTPOST]->(after)! DELETE del, delBefore, delAfter
  • 96.
  • 97.