-
Notifications
You must be signed in to change notification settings - Fork 493
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Fixes #1582: rebind flag in apoc.periodic.iterate * moved in full * Added docs
- Loading branch information
Showing
8 changed files
with
290 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
APOC provides a list of functions to rebind nodes and relationships: | ||
|
||
* `apoc.node.rebind(node)` | ||
* `apoc.rel.rebind(rel)` | ||
* `apoc.any.rebind(map/list/paths/...)` | ||
== Why use these functions | ||
|
||
Unlike versions up to 3.5, | ||
in Neo4j 4.x the entities hold a reference to their originating transaction. | ||
|
||
This can cause problems when for example, we create a node (called n1) in a transaction, open a new transaction in which we create another node (called n2) and execute | ||
via `org.neo4j.graphdb.Node.createRelationshipTo()`, | ||
that is `n1.createRelationshipTo(n2)`. | ||
|
||
|
||
Is what happens when we perform the following operation: | ||
|
||
[source,cypher] | ||
---- | ||
// node creation | ||
CREATE (:Article {content: 'contentBody'}); | ||
// iterate all (:Article) nodes and new transaction and rel creation | ||
CALL apoc.periodic.iterate('MATCH (art:Article) RETURN art', | ||
'CREATE (node:Category) with art, node call apoc.create.relationship(art, "CATEGORY", {b: 1}, node) yield rel return rel', {}); | ||
---- | ||
|
||
Basically, we create a node `(:Article)`, | ||
then the second parameter of the xref::overview/apoc.periodic/apoc.periodic.iterate.adoc[apoc.periodic.iterate] open a new transaction and create a node `(:Category)`, | ||
and finally we try to create a relationship between `(:Article)` and `(:Category)` via xref::overview/apoc.create/apoc.create.relationship.adoc[apoc.create.relationship] (which uses under the hood the `org.neo4j.graphdb.Node.createRelationshipTo()` ). | ||
|
||
If we try to execute the second query the apoc.periodic.iterate will return an `errorMessage` similar to this: | ||
---- | ||
Failed to invoke procedure `apoc.create.relationship`: Caused by: org.neo4j.graphdb.NotFoundException: Node[10] is deleted and cannot be used to create a relationship": 1 | ||
---- | ||
|
||
|
||
== How to solve | ||
|
||
To solve the previous `apoc.periodic,iterate`, | ||
we can leverage return the internal id from the first statement and then match the node via id, | ||
that means doing a `MATCH (n) WHERE id(n) = id(nodeId)` | ||
(this operation is called rebinding). | ||
|
||
That is: | ||
[source,cypher] | ||
---- | ||
CALL apoc.periodic.iterate('MATCH (art:Article) RETURN id(art) as id', | ||
'CREATE (node:Category) WITH id, node MATCH (art) where id(art) = id | ||
WITH art, node call apoc.create.relationship(art, "CATEGORY", {b: 1}, node) yield rel return rel', {}); | ||
---- | ||
|
||
Alternatively, we can wrap with the `apoc.node.rebind` function the node that have to be rebound, like this: | ||
[source,cypher] | ||
---- | ||
CALL apoc.periodic.iterate('MATCH (art:Article) RETURN art', | ||
'CREATE (node:Category) with art, node call apoc.create.relationship(art, "CATEGORY", {b: 1}, node) yield rel return rel', {}); | ||
---- | ||
|
||
Regarding relationships, we can use `apoc.rel.rebind`: | ||
[source,cypher] | ||
---- | ||
// other operations... | ||
MATCH (:Start)-[rel:REL]->(:End) /*...*/ RETURN apoc.rel.rebind(rel) | ||
---- | ||
|
||
We can also use the `apoc.any.rebind(ANY)` to rebind multiple entities placed in maps, lists, paths, or a combination of these three. | ||
This will return the same structure passed in the argument, but with rebound entities. | ||
For example: | ||
|
||
.Map of entities | ||
[source,cypher] | ||
---- | ||
CREATE (a:Foo)-[r1:MY_REL]->(b:Bar)-[r2:ANOTHER_REL]->(c:Baz) WITH a,b,c,r1,r2 | ||
RETURN apoc.any.rebind({first: a, second: b, third: c, rels: [r1, r2]}) as rebind | ||
---- | ||
|
||
.List of paths | ||
[source,cypher] | ||
---- | ||
CREATE p1=(a:Foo)-[r1:MY_REL]->(b:Bar), p2=(:Bar)-[r2:ANOTHER_REL]->(c:Baz) | ||
RETURN apoc.any.rebind([p1, p2]) as rebind | ||
---- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package apoc.nodes; | ||
|
||
import apoc.Extended; | ||
import apoc.util.EntityUtil; | ||
import apoc.util.Util; | ||
import org.neo4j.graphdb.Node; | ||
import org.neo4j.graphdb.Relationship; | ||
import org.neo4j.graphdb.Transaction; | ||
import org.neo4j.procedure.Context; | ||
import org.neo4j.procedure.Description; | ||
import org.neo4j.procedure.Name; | ||
import org.neo4j.procedure.UserFunction; | ||
|
||
@Extended | ||
public class NodesExtended { | ||
|
||
@Context | ||
public Transaction tx; | ||
|
||
@UserFunction("apoc.node.rebind") | ||
@Description("apoc.node.rebind(node - to rebind a node (i.e. executing a Transaction.getNodeById(node.getId()) ") | ||
public Node nodeRebind(@Name("node") Node node) { | ||
return Util.rebind(tx, node); | ||
} | ||
|
||
@UserFunction("apoc.rel.rebind") | ||
@Description("apoc.rel.rebind(rel) - to rebind a rel (i.e. executing a Transaction.getRelationshipById(rel.getId()) ") | ||
public Relationship relationshipRebind(@Name("rel") Relationship rel) { | ||
return Util.rebind(tx, rel); | ||
} | ||
|
||
@UserFunction("apoc.any.rebind") | ||
@Description("apoc.any.rebind(Object) - to rebind any rel, node, path, map, list or combination of them (i.e. executing a Transaction.getNodeById(node.getId()) / Transaction.getRelationshipById(rel.getId()))") | ||
public Object anyRebind(@Name("any") Object any) { | ||
return EntityUtil.anyRebind(tx, any); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package apoc.util; | ||
|
||
import org.neo4j.graphalgo.impl.util.PathImpl; | ||
import org.neo4j.graphdb.Entity; | ||
import org.neo4j.graphdb.Path; | ||
import org.neo4j.graphdb.Relationship; | ||
import org.neo4j.graphdb.Transaction; | ||
import org.neo4j.internal.helpers.collection.Iterables; | ||
|
||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
public class EntityUtil { | ||
|
||
public static <T> T anyRebind(Transaction tx, T any) { | ||
if (any instanceof Map) { | ||
return (T) ((Map<String, Object>) any).entrySet().stream() | ||
.collect(Collectors.toMap(e -> e.getKey(), e -> anyRebind(tx, e.getValue()))); | ||
} | ||
if (any instanceof Path) { | ||
final Path path = (Path) any; | ||
PathImpl.Builder builder = new PathImpl.Builder(Util.rebind(tx, path.startNode())); | ||
for (Relationship rel: path.relationships()) { | ||
builder = builder.push(Util.rebind(tx, rel)); | ||
} | ||
return (T) builder.build(); | ||
} | ||
if (any instanceof Iterable) { | ||
return (T) Iterables.stream((Iterable) any) | ||
.map(i -> anyRebind(tx, i)).collect(Collectors.toList()); | ||
} | ||
if (any instanceof Entity) { | ||
return (T) Util.rebind(tx, (Entity) any); | ||
} | ||
return any; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package apoc.nodes; | ||
|
||
import apoc.create.Create; | ||
import apoc.util.TestUtil; | ||
import org.junit.Before; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.neo4j.graphdb.Label; | ||
import org.neo4j.graphdb.Path; | ||
import org.neo4j.graphdb.Relationship; | ||
import org.neo4j.graphdb.RelationshipType; | ||
import org.neo4j.internal.helpers.collection.Iterables; | ||
import org.neo4j.test.rule.DbmsRule; | ||
import org.neo4j.test.rule.ImpermanentDbmsRule; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
|
||
|
||
public class NodesExtendedTest { | ||
|
||
@Rule | ||
public DbmsRule db = new ImpermanentDbmsRule(); | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
TestUtil.registerProcedure(db, NodesExtended.class, Create.class); | ||
} | ||
|
||
@Test | ||
public void rebind() { | ||
TestUtil.testCall(db, "CREATE (a:Foo)-[r1:MY_REL]->(b:Bar)-[r2:ANOTHER_REL]->(c:Baz) WITH a,b,c,r1,r2 \n" + | ||
"RETURN apoc.any.rebind({first: a, second: b, third: c, rels: [r1, r2]}) as rebind", | ||
(row) -> { | ||
final Map<String, Object> rebind = (Map<String, Object>) row.get("rebind"); | ||
final List<Relationship> rels = (List<Relationship>) rebind.get("rels"); | ||
final Relationship firstRel = rels.get(0); | ||
final Relationship secondRel = rels.get(1); | ||
assertEquals(firstRel.getStartNode(), rebind.get("first")); | ||
assertEquals(firstRel.getEndNode(), rebind.get("second")); | ||
assertEquals(secondRel.getStartNode(), rebind.get("second")); | ||
assertEquals(secondRel.getEndNode(), rebind.get("third")); | ||
}); | ||
|
||
TestUtil.testCall(db, "CREATE p1=(a:Foo)-[r1:MY_REL]->(b:Bar), p2=(:Bar)-[r2:ANOTHER_REL]->(c:Baz) \n" + | ||
"RETURN apoc.any.rebind([p1, p2]) as rebind", | ||
(row) -> { | ||
final List<Path> rebindList = (List<Path>) row.get("rebind"); | ||
assertEquals(2, rebindList.size()); | ||
final Path firstPath = rebindList.get(0); | ||
assertPath(firstPath, List.of("Foo", "Bar"), List.of("MY_REL")); | ||
final Path secondPath = rebindList.get(1); | ||
assertPath(secondPath, List.of("Bar", "Baz"), List.of("ANOTHER_REL")); | ||
}); | ||
} | ||
|
||
private void assertPath(Path rebind, List<String> labels, List<String> relTypes) { | ||
final List<String> actualLabels = Iterables.stream(rebind.nodes()) | ||
.map(i -> i.getLabels().iterator().next()) | ||
.map(Label::name).collect(Collectors.toList()); | ||
assertEquals(labels, actualLabels); | ||
final List<String> actualRelTypes = Iterables.stream(rebind.relationships()).map(Relationship::getType) | ||
.map(RelationshipType::name).collect(Collectors.toList()); | ||
assertEquals(relTypes, actualRelTypes); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters