DATAGRAPH-253 Better abstraction layer for interfacing with Remote Neo4j

dedicated API to sync a set of relationships
This commit is contained in:
Michael Hunger
2014-10-01 14:39:52 +02:00
parent 0adee415b2
commit 57ade2ea69
18 changed files with 417 additions and 54 deletions

View File

@@ -68,6 +68,8 @@ public interface RestAPI extends RestAPIIndex, RestAPIInternal {
Transaction beginTx();
Iterable<Relationship> updateRelationships(Node start, Collection<Node> endNodes, RelationshipType type, Direction direction, String targetLabel);
Collection<String> getAllLabelNames();
Iterable<RelationshipType> getRelationshipTypes();
@@ -91,4 +93,6 @@ public interface RestAPI extends RestAPIIndex, RestAPIInternal {
void removeFromCache(long id);
void close();
Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props);
}

View File

@@ -21,7 +21,7 @@ package org.neo4j.rest.graphdb;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.helpers.Pair;
import org.neo4j.helpers.collection.IterableWrapper;
import org.neo4j.helpers.collection.IteratorUtil;
import org.neo4j.helpers.collection.MapUtil;
@@ -34,7 +34,6 @@ import org.neo4j.rest.graphdb.index.IndexInfo;
import org.neo4j.rest.graphdb.index.RestIndex;
import org.neo4j.rest.graphdb.index.RestIndexManager;
import org.neo4j.rest.graphdb.query.*;
import org.neo4j.rest.graphdb.transaction.RemoteCypherTransaction;
import org.neo4j.rest.graphdb.traversal.RestTraversalDescription;
import org.neo4j.rest.graphdb.traversal.RestTraverser;
import org.neo4j.rest.graphdb.util.QueryResult;
@@ -44,7 +43,10 @@ import org.neo4j.rest.graphdb.util.ResultConverter;
import javax.ws.rs.core.Response.Status;
import java.util.*;
import static java.util.Arrays.asList;
import static org.neo4j.helpers.collection.MapUtil.map;
import static org.neo4j.rest.graphdb.query.CypherTransaction.ResultType.row;
import static org.neo4j.rest.graphdb.query.CypherTransaction.Statement;
public class RestAPICypherImpl implements RestAPI {
@@ -168,8 +170,7 @@ public class RestAPICypherImpl implements RestAPI {
@Override
public RestNode createNode(Map<String, Object> props, Collection<String> labels) {
Map<?, Object> data = props == null ? Collections.emptyMap() : props;
Iterator<List<Object>> result = query(createNodeQuery(labels), map("props", data)).getData().iterator();
Iterator<List<Object>> result = query(createNodeQuery(labels), map("props", props(props))).getData().iterator();
if (result.hasNext()) {
return addToCache(toNode(result.next()));
}
@@ -177,9 +178,10 @@ public class RestAPICypherImpl implements RestAPI {
}
@Override
public RestNode merge(String labelName, String key, Object value, final Map<String, Object> nodeProperties, Collection<String> labels) {
public RestNode merge(String labelName, String key, Object value, Map<String, Object> nodeProperties, Collection<String> labels) {
if (labelName == null || key == null || value == null)
throw new IllegalArgumentException("Label " + labelName + " key " + key + " and value must not be null");
nodeProperties = props(nodeProperties);
Map props = nodeProperties.containsKey(key) ? nodeProperties : MapUtil.copyAndPut(nodeProperties, key, value);
Map<String, Object> params = map("props", props, "value", value);
Iterator<List<Object>> result = query(mergeQuery(labelName, key, labels), params).getData().iterator();
@@ -196,7 +198,7 @@ public class RestAPICypherImpl implements RestAPI {
@Override
public RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map<String, Object> props) {
String statement = MATCH_NODE_QUERY("n") + MATCH_NODE_QUERY("m") + " CREATE (n)-[r:`" + type.name() + "`]->(m) SET r={props} " + _QUERY_RETURN_REL;
Map<String, Object> params = map("id_n", startNode.getId(), "id_m", endNode.getId(), "props", props == null ? Collections.emptyMap() : props);
Map<String, Object> params = map("id_n", startNode.getId(), "id_m", endNode.getId(), "props", props(props));
CypherTransaction.Result result = runQuery(statement, params);
if (!result.hasData())
throw new RuntimeException("Error creating relationship from " + startNode + " to " + endNode + " type " + type.name());
@@ -268,6 +270,7 @@ public class RestAPICypherImpl implements RestAPI {
if (types == null || types.length == 0) return "";
StringBuilder typeString = new StringBuilder();
for (RelationshipType type : types) {
if (type==null) continue;
if (typeString.length() > 0) typeString.append("|");
typeString.append(':').append('`').append(type.name()).append("`");
}
@@ -423,6 +426,18 @@ public class RestAPICypherImpl implements RestAPI {
return new CypherTxResult(runQuery(statement, params));
}
private List<CypherTransaction.Result> runQueries(Collection<Statement> statements) {
if (!txManager.isActive()) {
CypherTransaction tx = newCypherTransaction();
tx.addAll(statements);
return tx.commit();
} else {
CypherTransaction tx = txManager.getCypherTransaction();
tx.addAll(statements);
return tx.send();
}
}
private CypherTransaction.Result runQuery(String statement, Map<String, Object> params) {
if (!txManager.isActive()) {
return newCypherTransaction().commit(statement, params);
@@ -431,7 +446,7 @@ public class RestAPICypherImpl implements RestAPI {
}
public CypherTransaction newCypherTransaction() {
return new CypherTransaction(this, CypherTransaction.ResultType.row);
return new CypherTransaction(this, row);
}
public QueryResult<Map<String, Object>> query(String statement, Map<String, Object> params, ResultConverter resultConverter) {
@@ -471,6 +486,61 @@ public class RestAPICypherImpl implements RestAPI {
restAPI.close();
}
@Override
public Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props) {
/*
final Iterable<Relationship> existingRelationships = start.getRelationships(type, direction);
for (final Relationship existingRelationship : existingRelationships) {
if (existingRelationship != null && existingRelationship.getOtherNode(start).equals(end))
return existingRelationship;
}
if (direction == Direction.INCOMING) {
return end.createRelationshipTo(start, type);
} else {
return start.createRelationshipTo(end, type);
}
*/
String relPattern = relPattern(direction, type);
String statement = MATCH_NODE_QUERY("n") + MATCH_NODE_QUERY("m") + " MERGE (n)"+relPattern+"(m) ON CREATE SET r={props}" + _QUERY_RETURN_REL;
CypherTransaction.Result result = runQuery(statement, map("id_n", start.getId(), "id_m", end.getId(),"props", props(props)));
if (!result.hasData())
throw new RuntimeException("Error creating relationship from " + start + " to " + end + " type " + type.name() +" direction "+direction);
return toRel(result.getRows().iterator().next());
}
@Override
public Iterable<Relationship> updateRelationships(Node start, Collection<Node> endNodes, RelationshipType type, Direction direction, String targetLabel) {
String targetLabelPredicate = targetLabel == null ? "" : " AND (m:`"+targetLabel+"` OR m:`_"+targetLabel+"`)";
String relPattern = relPattern(direction, type);
String statement1 = "MATCH (n)"+relPattern+"(m) WHERE id(n) = {id_n} "+targetLabelPredicate+" AND NOT id(m) IN {ids_m} DELETE r RETURN id(r) as id_r";
String statement2 = MATCH_NODE_QUERY("n") + " MATCH (m) WHERE id(m) IN {ids_m} MERGE (n)"+relPattern+"(m)" + _QUERY_RETURN_REL;
Map<String, Object> params = map("id_n", start.getId(), "ids_m", nodeIds(endNodes));
List<CypherTransaction.Result> results = runQueries(asList(
new Statement(statement1, params, row),
new Statement(statement2, params, row)));
Iterable<List<Object>> mergeResults = results.get(1).getRows();
return new IterableWrapper<Relationship,List<Object>>(mergeResults) {
@Override
protected Relationship underlyingObjectToObject(List<Object> row) {
return toRel(row);
}
};
}
private long[] nodeIds(Collection<Node> nodes) {
long[] ids = new long[nodes.size()];
int i=0;
for (Node node : nodes) {
ids[i++] = node.getId();
}
return ids;
}
public Map<String, Object> props(Map<String, Object> props) {
return props == null ? Collections.<String, Object>emptyMap() : props;
}
@Override
public boolean isAutoIndexingEnabled(Class<? extends PropertyContainer> clazz) {
return restAPI.isAutoIndexingEnabled(clazz);

View File

@@ -21,11 +21,9 @@ package org.neo4j.rest.graphdb;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.helpers.collection.IterableWrapper;
import org.neo4j.helpers.collection.MapUtil;
import org.neo4j.index.lucene.ValueContext;
import org.neo4j.rest.graphdb.batch.BatchRestAPI;
import org.neo4j.rest.graphdb.entity.RestEntityCache;
import org.neo4j.rest.graphdb.query.CypherRestResult;
import org.neo4j.rest.graphdb.converter.RelationshipIterableConverter;
@@ -49,6 +47,8 @@ import org.neo4j.rest.graphdb.traversal.RestTraverser;
import org.neo4j.rest.graphdb.util.JsonHelper;
import org.neo4j.rest.graphdb.util.QueryResult;
import org.neo4j.rest.graphdb.util.ResultConverter;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.support.mapping.Neo4jMappingContext;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@@ -58,6 +58,7 @@ import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static java.lang.String.format;
import static javax.ws.rs.core.Response.Status.CREATED;
import static org.neo4j.helpers.collection.MapUtil.map;
import static org.neo4j.rest.graphdb.ExecutingRestRequest.encode;
@@ -261,6 +262,44 @@ public class RestAPIImpl implements RestAPI {
ExecutingRestRequest.shutdown();
}
// TODO
@Override
public Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props) {
final Iterable<Relationship> existingRelationships = start.getRelationships(type, direction);
for (final Relationship existingRelationship : existingRelationships) {
if (existingRelationship != null && existingRelationship.getOtherNode(start).equals(end))
return existingRelationship;
}
if (direction == Direction.INCOMING) {
return createRelationship(end, start, type, props);
} else {
return createRelationship(start,end,type,props);
}
}
protected Collection<Node> removeMissingRelationships(Node node, Collection<Node> targetNodes,
RelationshipType type, Direction direction, Label targetLabel) {
targetNodes = new ArrayList<>(targetNodes);
for (Relationship relationship : node.getRelationships( type, direction ) ) {
Node otherNode = relationship.getOtherNode(node);
if ( !targetNodes.remove(otherNode) ) {
if (targetLabel != null && !relationship.getOtherNode(node).hasLabel(targetLabel)) continue;
relationship.delete();
}
}
return targetNodes;
}
@Override
public Iterable<Relationship> updateRelationships(final Node start, Collection<Node> endNodes, final RelationshipType type, final Direction direction, String targetLabelName) {
Label targetLabel = targetLabelName==null ? null : DynamicLabel.label(targetLabelName);
Collection<Node> remainingEndNodes = removeMissingRelationships(start, endNodes, type, direction, targetLabel);
List<Relationship> result=new ArrayList<>(remainingEndNodes.size());
for (Node endNode : remainingEndNodes) {
result.add(getOrCreateRelationship(start, endNode,type,direction,null));
}
return result;
}
@Override
public RestNode merge(String labelName, String key, Object value, final Map<String, Object> nodeProperties, Collection<String> labels) {
if (labelName ==null || key == null || value==null) throw new IllegalArgumentException("Label "+ labelName +" key "+key+" and value must not be null");

View File

@@ -1,6 +1,7 @@
package org.neo4j.rest.graphdb.query;
import com.sun.jersey.api.client.ClientResponse;
import org.neo4j.helpers.Pair;
import org.neo4j.helpers.collection.IterableWrapper;
import org.neo4j.helpers.collection.IteratorWrapper;
import org.neo4j.rest.graphdb.*;
@@ -136,6 +137,14 @@ public class CypherTransaction {
private final RestRequest request;
private final List<Statement> statements = new ArrayList<>(10);
public void addAll(Statement...statements) {
this.statements.addAll(asList(statements));
}
public void addAll(Collection<Statement> statements) {
this.statements.addAll(statements);
}
public void add(String statement, Map<String,Object> params) {
statements.add(new Statement(statement,params,type));
}

View File

@@ -76,8 +76,10 @@ public class RestCypherTransactionManager implements TransactionManager, Transac
@Override
public void rollback() throws IllegalStateException, SecurityException, SystemException {
RemoteCypherTransaction tx = getRemoteCypherTransaction();
tx.failure();
tx.close();
if (tx!=null) {
tx.failure();
tx.close();
}
}
@Override

View File

@@ -37,9 +37,10 @@ public enum RestDirection {
}
public static RestDirection from( Direction direction ) {
if (direction == null) return BOTH;
for ( RestDirection restDirection : values() ) {
if ( restDirection.direction == direction ) return restDirection;
}
throw new RuntimeException( "No Rest-Direction for " + direction );
}
}
}

View File

@@ -23,9 +23,6 @@ import org.neo4j.rest.graphdb.RestAPIImpl;
import org.neo4j.rest.graphdb.entity.RestNode;
import org.neo4j.rest.graphdb.index.RestIndex;
import org.neo4j.rest.graphdb.index.RestIndexManager;
import org.neo4j.rest.graphdb.transaction.NullTransaction;
import org.neo4j.rest.graphdb.transaction.NullTransactionManager;
import org.neo4j.rest.graphdb.util.Config;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.neo4j.conversion.DefaultConverter;
import org.springframework.data.neo4j.conversion.ResultConverter;
@@ -138,6 +135,11 @@ public class SpringCypherRestGraphDatabase extends CypherRestGraphDatabase imple
return super.getTxManager();
}
@Override
public Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props) {
return getRestAPI().getOrCreateRelationship(start, end, type, direction, props);
}
@Override
public void remove(Node node) {
removeFromIndexes(node); // todo should we do this by default?

View File

@@ -105,6 +105,11 @@ public class SpringRestGraphDatabase extends org.neo4j.rest.graphdb.RestGraphDat
return getRestAPI().getOrCreateRelationship(relIndex,key,value,(RestNode) startNode,(RestNode) endNode,type, properties);
}
@Override
public Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props) {
return getRestAPI().getOrCreateRelationship(start, end, type, direction,props);
}
@Override
public Relationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map<String, Object> properties) {
return super.getRestAPI().createRelationship(startNode, endNode, type, properties);

View File

@@ -82,7 +82,7 @@ public class RestTestBase {
public void setUp() throws Exception {
System.setProperty(Config.CONFIG_BATCH_TRANSACTION,"false");
neoServer.cleanDb();
restGraphDb = new RestGraphDatabase(SERVER_ROOT_URI);
restGraphDb = createRestGraphDatabase();
GraphDatabaseService db = getGraphDatabase();
try (Transaction tx = db.beginTx()) {
@@ -94,6 +94,10 @@ public class RestTestBase {
}
protected GraphDatabaseService createRestGraphDatabase() {
return new RestGraphDatabase(SERVER_ROOT_URI);
}
@After
public void tearDown() throws Exception {
restGraphDb.shutdown();

View File

@@ -0,0 +1,16 @@
package org.neo4j.rest.graphdb;
import org.neo4j.graphdb.GraphDatabaseService;
/**
* @author mh
* @since 30.09.14
*/
public class UpdateRelationshipRestApiImplTest extends UpdateRelationshipTest {
@Override
protected GraphDatabaseService createRestGraphDatabase() {
restAPI = new RestAPIImpl(SERVER_ROOT_URI);
return new RestGraphDatabase(restAPI);
}
}

View File

@@ -0,0 +1,154 @@
package org.neo4j.rest.graphdb;
import org.junit.Before;
import org.junit.Test;
import org.neo4j.graphdb.*;
import org.neo4j.rest.graphdb.query.CypherResult;
import org.neo4j.rest.graphdb.query.CypherTransactionExecutionException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.neo4j.helpers.collection.IteratorUtil.count;
import static org.neo4j.helpers.collection.MapUtil.map;
public class UpdateRelationshipTest extends RestTestBase {
public static final DynamicRelationshipType KNOWS = DynamicRelationshipType.withName("KNOWS");
public static final Label FRIEND = DynamicLabel.label("Friend");
private DynamicRelationshipType type = KNOWS;
public static final DynamicRelationshipType LIKES = DynamicRelationshipType.withName("LIKES");
private Direction direction = Direction.OUTGOING;
private Node remove;
private Node keep;
private Node add;
protected RestAPI restAPI;
private List<Node> expected;
private List<Node> updateTo;
private String targetLabel = null;
@Override
protected GraphDatabaseService createRestGraphDatabase() {
restAPI = new RestAPICypherImpl(new RestAPIImpl(SERVER_ROOT_URI));
return new CypherRestGraphDatabase(restAPI);
}
@Override
@Before
public void setUp() throws Exception {
super.setUp();
GraphDatabaseService db = getRestGraphDb();
remove = db.createNode();
keep = db.createNode();
add = db.createNode();
}
@Test
public void testUpdateRelationshipsRemoveAddNoDirection() throws Exception {
Node node = node();
remove.createRelationshipTo(node, KNOWS);
node.createRelationshipTo(keep, KNOWS);
direction = null;
updateTo = asList(keep,add);
updateRelationships();
}
@Test
public void testUpdateRelationshipsRemoveAddBothDirection() throws Exception {
Node node = node();
remove.createRelationshipTo(node, KNOWS);
node.createRelationshipTo(keep, KNOWS);
direction = Direction.BOTH;
updateTo = asList(keep,add);
updateRelationships();
}
@Test(expected = CypherTransactionExecutionException.class)
public void testUpdateRelationshipsRemoveAddNoType() throws Exception {
Node node = node();
remove.createRelationshipTo(node, LIKES);
node.createRelationshipTo(keep, KNOWS);
type = null;
updateTo = asList(keep);
updateRelationships();
}
@Test
public void testUpdateRelationshipsRemoveAdd() throws Exception {
Node node = node();
node.createRelationshipTo(remove, KNOWS);
node.createRelationshipTo(keep, KNOWS);
updateTo = asList(keep, add);
updateRelationships();
}
@Test
public void testUpdateRelationshipsRemoveAddKeepOtherType() throws Exception {
Node node = node();
node.createRelationshipTo(remove, LIKES);
node.createRelationshipTo(remove, KNOWS);
node.createRelationshipTo(keep, KNOWS);
updateTo = asList(keep, add);
expected = asList(remove, keep, add);
updateRelationships();
}
@Test
public void testUpdateRelationshipsRemoveAddKeepOtherDirection() throws Exception {
Node node = node();
remove.createRelationshipTo(node, KNOWS);
node.createRelationshipTo(remove, KNOWS);
node.createRelationshipTo(keep, KNOWS);
updateTo = asList(keep, add);
expected = asList(remove, keep, add);
updateRelationships();
}
@Test
public void testUpdateRelationshipsRemoveAddKeepOtherLabel() throws Exception {
Node node = node();
remove.addLabel(FRIEND);
node.createRelationshipTo(remove, KNOWS);
node.createRelationshipTo(keep, KNOWS);
updateTo = asList();
expected = asList(keep);
targetLabel = FRIEND.name();
updateRelationships();
}
@Test
public void testUpdateRelationshipNothing() throws Exception {
updateTo = asList();
updateRelationships();
}
@Test
public void testUpdateRelationshipsAdd() throws Exception {
updateTo = asList(add);
updateRelationships();
}
@Test
public void testUpdateRelationshipsRemove() throws Exception {
Node node = node();
node.createRelationshipTo(remove, KNOWS);
updateTo = asList();
updateRelationships();
}
protected void updateRelationships() {
if (expected==null) expected = updateTo;
Iterable<Relationship> rels = restAPI.updateRelationships(node(), updateTo, type, direction, targetLabel);
CypherResult result = restAPI.query("MATCH (n)--(m) WHERE id(n) = {id} WITH id(m) as id ORDER BY id RETURN collect(id) as ids", map("id", node().getId()));
assertEquals(1, count(result.getData()));
assertEquals(toSortedIdList(expected), result.getData().iterator().next().get(0));
}
private List<Integer> toSortedIdList(List<Node> endNodes) {
List<Integer> expected = new ArrayList<>(endNodes.size());
for (Node node : endNodes) expected.add((int) node.getId());
Collections.sort(expected);
return expected;
}
}

View File

@@ -136,4 +136,6 @@ public interface GraphDatabase {
void shutdown();
Collection<String> getAllLabelNames();
Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props);
}

View File

@@ -24,8 +24,9 @@ import org.springframework.data.neo4j.support.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.support.typerepresentation.LabelBasedNodeTypeRepresentationStrategy;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import static java.lang.String.format;
@@ -47,20 +48,26 @@ public class RelationshipHelper {
}
private Iterable<Node> getOtherNodes(Node node) {
final Set<Node> result = new HashSet<Node>();
final Set<Node> result = new HashSet<>();
for (final Relationship rel : node.getRelationships(type, direction)) {
result.add(rel.getOtherNode(node));
}
return result;
}
protected Relationship obtainSingleRelationship(final Node start, final Node end) {
final Iterable<Relationship> existingRelationships = start.getRelationships(type, direction);
for (final Relationship existingRelationship : existingRelationships) {
if (existingRelationship != null && existingRelationship.getOtherNode(start).equals(end))
return existingRelationship;
private Long getOtherNodeId(Node node, Relationship rel) {
long id = node.getId();
if (rel.getStartNode().getId() == id) return rel.getEndNode().getId();
if (rel.getEndNode().getId() == id) return rel.getStartNode().getId();
throw new IllegalStateException("Node "+node+" is not connected to Relationship "+rel);
}
private Iterable<Long> getOtherNodeIds(Node node) {
final Set<Long> result = new HashSet<>();
for (final Relationship rel : node.getRelationships(type, direction)) {
result.add(getOtherNodeId(node, rel));
}
return start.createRelationshipTo(end, type);
return result;
}
protected Node checkAndGetNode(Object entity) {
@@ -70,20 +77,44 @@ public class RelationshipHelper {
throw new IllegalStateException("Entity must have a backing Node");
}
/*
MATCH (n) WHERE id(n) = {id_n}
OPTIONAL MATCH (n)-[r]-(m)
WITH n, collect(r) as rels,
FILTER(good in collect(r) WHERE id(startNode(r)) IN {ids_m} OR id(endNode(r)) IN {ids_m}) as keep,
FILTER(id in {ids_m} WHERE NOT (id(startNode(r)) = id OR id(endNode(r)) = id) as new
FOREACH (r in rels WHERE NOT r in keep | DELETE r)
// alternative
FOREACH (r in rels WHERE (startNode(r) = n AND id(endNode(r)) NOT IN {ids_m}) OR (endNode(r) = n AND id(startNode(r)) NOT IN {ids_m})) | DELETE r)
WITH n,count(*), new
UNWIND new as id_m
MATCH (m) WHERE id(m) = id_m
CREATE (n)-[r:TYPE]-(m)
RETURN r
MATCH (n) WHERE id(n) = {id_n}
OPTIONAL MATCH (n)-[r]-(m)
WHERE not id(m) IN {ids_m}
DELETE r
MATCH (n) WHERE id(n) = {id_n}
MATCH (m) WHERE id(m) IN {ids_m}
MERGE (n)-[:TYPE]->(m)
*/
protected void removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet( Node node,
Set<Node> targetNodes,
Class<?> targetType ) {
Neo4jMappingContext mappingContext = template.getInfrastructure().getMappingContext();
for ( Relationship relationship : node.getRelationships( type, direction ) ) {
if ( !targetNodes.remove( relationship.getOtherNode( node ) ) ) {
Node otherNode = relationship.getOtherNode(node);
if ( !targetNodes.remove(otherNode) ) {
if ( targetType != null ) {
Object actualTargetType = tryDetermineTypeAssumingIndexBasedStrategy(relationship, node);
if (actualTargetType == null) {
actualTargetType = tryDetermineTypeAssumingLabelBasedStrategy(relationship, node);
}
if (actualTargetType == null) {
throw new RuntimeException("Neither a property or Label could be found to work out what the type of the node is at the other end of the relationship ");
}
Object actualTargetType = determineEndNodeType(otherNode);
try {
Neo4jPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(actualTargetType);
if (! targetType.isAssignableFrom(persistentEntity.getType())) continue;
@@ -97,8 +128,19 @@ public class RelationshipHelper {
}
}
private Object tryDetermineTypeAssumingLabelBasedStrategy(Relationship relationship,Node node) {
for (Label l : relationship.getOtherNode(node).getLabels()) {
private Object determineEndNodeType(Node otherNode) {
Object actualTargetType = otherNode.getProperty("__type__", null);
if (actualTargetType == null) {
actualTargetType = tryDetermineTypeAssumingLabelBasedStrategy(otherNode);
}
if (actualTargetType == null) {
throw new RuntimeException("Neither a property or Label could be found to work out what the type of the node is at the other end of the relationship ");
}
return actualTargetType;
}
private Object tryDetermineTypeAssumingLabelBasedStrategy(Node otherNode) {
for (Label l : otherNode.getLabels()) {
if (l.name().startsWith(LabelBasedNodeTypeRepresentationStrategy.LABELSTRATEGY_PREFIX)) {
return l.name().substring(LabelBasedNodeTypeRepresentationStrategy.LABELSTRATEGY_PREFIX.length());
}
@@ -107,10 +149,6 @@ public class RelationshipHelper {
}
private Object tryDetermineTypeAssumingIndexBasedStrategy(Relationship relationship,Node node) {
return relationship.getOtherNode( node ).getProperty( "__type__" , null);
}
protected void createAddedRelationships(Node node, Set<Node> targetNodes) {
for (Node targetNode : targetNodes) {
createSingleRelationship(node, targetNode);
@@ -134,8 +172,8 @@ public class RelationshipHelper {
}
protected Node getOrCreateState(Object value) {
final Node Node = getNode(value);
if (Node != null) return Node;
final Node node = getNode(value);
if (node != null) return node;
final Object saved = template.save(value);
final Node newState = getNode(saved);
Assert.notNull(newState);
@@ -156,16 +194,8 @@ public class RelationshipHelper {
@SuppressWarnings("unchecked")
protected Relationship createSingleRelationship(Node start, Node end) {
if (end == null) return null;
switch (direction) {
case OUTGOING:
case BOTH: { // TODO both should actually check in both directions, perhaps have the obtain method get the direction instead and figure out what to do itself
return obtainSingleRelationship(start, end);
}
case INCOMING:
return obtainSingleRelationship(end, start);
default:
throw new InvalidDataAccessApiUsageException("invalid direction " + direction);
}
Map<String,Object> props = Collections.emptyMap();
return template.getOrCreateRelationship(start, end, type, direction, props);
}
protected Iterable<Node> getStatesFromEntity(final Object entity) {

View File

@@ -279,6 +279,18 @@ public class DelegatingGraphDatabase implements GraphDatabase {
return labels;
}
@Override
public Relationship getOrCreateRelationship(Node start, Node end, RelationshipType type, Direction direction, Map<String, Object> props) {
final Iterable<Relationship> existingRelationships = start.getRelationships(type, direction);
for (final Relationship existingRelationship : existingRelationships) {
if (existingRelationship != null && existingRelationship.getOtherNode(start).equals(end))
return existingRelationship;
}
Relationship rel = direction == Direction.INCOMING ? end.createRelationshipTo(start, type) : start.createRelationshipTo(end, type);
setProperties(rel, props);
return rel;
}
public GraphDatabaseService getGraphDatabaseService() {
return delegate;
}

View File

@@ -241,15 +241,18 @@ public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware {
return infrastructure.getEntityPersister().projectTo(entity, targetType, mappingPolicy, this);
}
/**
* just sets the persistent state (i.e. Node or id) to the entity, doesn't copy any values/properties.
*/
@Override
public <S extends PropertyContainer> S getPersistentState(Object entity) {
notNull(entity, "entity");
return infrastructure.getEntityPersister().getPersistentState(entity);
}
@Override
public Number getId(Object entity) {
notNull(entity, "entity");
return infrastructure.getEntityPersister().getId(entity);
}
public <S extends PropertyContainer, T> T setPersistentState(T entity, S state) {
notNull(entity, "entity", state, "node or relationship");
infrastructure.getEntityPersister().setPersistentState(entity, state);
@@ -759,4 +762,8 @@ public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware {
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public Relationship getOrCreateRelationship(final Node start, final Node end, RelationshipType type, Direction direction, Map<String, Object> props) {
return getGraphDatabase().getOrCreateRelationship(start, end, type, direction,props);
}
}

View File

@@ -83,7 +83,7 @@ public class EntityStateHandler {
return getId(entity) != null;
}
private Number getId(Object entity) {
public Number getId(Object entity) {
final Class<?> type = entity.getClass();
final Neo4jPersistentEntityImpl<?> persistentEntity = mappingContext.getPersistentEntity(type);
final Object id = persistentEntity.getPersistentId(entity);

View File

@@ -221,6 +221,10 @@ public class Neo4jEntityPersister implements EntityPersister, Neo4jEntityConvert
return entityStateHandler.getPersistentState(entity);
}
public Number getId(Object entity) {
return entityStateHandler.getId(entity);
}
public Object persist( Object entity, final MappingPolicy mappingPolicy, final Neo4jTemplate template,
RelationshipType annotationProvidedRelationshipType ) {

View File

@@ -261,6 +261,8 @@ public interface Neo4jOperations {
*/
<T> T save(T entity);
Number getId(Object entity);
/**
* Removes the given node or relationship entity or node or relationship from the graph, the entity is first removed
* from all indexes and then deleted.