From caed15e2b728779ba1d111f571b386ecbc95aa1d Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Thu, 19 Apr 2012 11:15:00 +0200 Subject: [PATCH] DATAGRAPH-219, DATAGRAPH-210 unique relationship, and documentation for unique entities --- .../org/neo4j/cineasts/domain/Person.java | 2 +- .../org/neo4j/cineasts/domain/Person.java | 2 +- .../org/neo4j/cineasts/domain/Person.java | 2 +- .../neo4j/fieldaccess/RelationshipHelper.java | 2 +- .../support/mapping/EntityStateHandler.java | 21 +++- .../support/node/NodeEntityStateFactory.java | 1 + .../mapping/Neo4jEntityPersisterTest.java | 9 ++ .../data/neo4j/model/BestFriend.java | 84 ++++++++++++++ .../data/neo4j/model/Person.java | 15 +++ .../data/neo4j/unique/UniqueEntityTest.java | 15 +++ .../neo4j/unique/UniqueRelationshipTest.java | 107 ++++++++++++++++++ .../reference/programming-model/indexing.xml | 14 ++- src/docbkx/tutorial/indexing.xml | 3 +- src/docbkx/tutorial/relationships.xml | 6 +- src/docbkx/tutorial/running.xml | 3 +- src/docbkx/tutorial/social.xml | 2 +- 16 files changed, 268 insertions(+), 20 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/BestFriend.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueRelationshipTest.java diff --git a/spring-data-neo4j-examples/cineasts-aspects/src/main/java/org/neo4j/cineasts/domain/Person.java b/spring-data-neo4j-examples/cineasts-aspects/src/main/java/org/neo4j/cineasts/domain/Person.java index c0d38c4b0..c047e3bd8 100644 --- a/spring-data-neo4j-examples/cineasts-aspects/src/main/java/org/neo4j/cineasts/domain/Person.java +++ b/spring-data-neo4j-examples/cineasts-aspects/src/main/java/org/neo4j/cineasts/domain/Person.java @@ -15,7 +15,7 @@ import java.util.Set; */ @NodeEntity public class Person { - @Indexed + @Indexed(unique=true) String id; @Indexed(indexType=IndexType.FULLTEXT, indexName = "people") String name; diff --git a/spring-data-neo4j-examples/cineasts-rest/src/main/java/org/neo4j/cineasts/domain/Person.java b/spring-data-neo4j-examples/cineasts-rest/src/main/java/org/neo4j/cineasts/domain/Person.java index 5fcd3d704..650e65f92 100644 --- a/spring-data-neo4j-examples/cineasts-rest/src/main/java/org/neo4j/cineasts/domain/Person.java +++ b/spring-data-neo4j-examples/cineasts-rest/src/main/java/org/neo4j/cineasts/domain/Person.java @@ -8,7 +8,7 @@ import java.util.Set; @NodeEntity public class Person { - @Indexed + @Indexed(unique=true) String id; @Indexed(indexType=IndexType.FULLTEXT, indexName = "people") String name; diff --git a/spring-data-neo4j-examples/cineasts/src/main/java/org/neo4j/cineasts/domain/Person.java b/spring-data-neo4j-examples/cineasts/src/main/java/org/neo4j/cineasts/domain/Person.java index eee115e6a..7784bee76 100644 --- a/spring-data-neo4j-examples/cineasts/src/main/java/org/neo4j/cineasts/domain/Person.java +++ b/spring-data-neo4j-examples/cineasts/src/main/java/org/neo4j/cineasts/domain/Person.java @@ -15,7 +15,7 @@ import java.util.Set; @NodeEntity public class Person { @GraphId Long nodeId; - @Indexed + @Indexed(unique=true) String id; @Indexed(indexType=IndexType.FULLTEXT, indexName = "people") String name; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java index 3702b8f0a..e3a79a3ed 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java @@ -70,7 +70,7 @@ public class RelationshipHelper { protected void removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet(Node node, Set targetNodes) { for (Relationship relationship : node.getRelationships(type, direction)) { if (!targetNodes.remove(relationship.getOtherNode(node))) - relationship.delete(); + template.delete(relationship); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java index 3a29e29da..4b89e3035 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java @@ -131,7 +131,7 @@ public class EntityStateHandler { return (S) graphDatabase.createNode(null); } if (persistentEntity.isRelationshipEntity()) { - return createRelationship(entity, persistentEntity); + return getOrCreateRelationship(entity, persistentEntity); } throw new IllegalArgumentException("The entity " + persistentEntity.getEntityName() + " has to be either annotated with @NodeEntity or @RelationshipEntity"); } @@ -139,19 +139,28 @@ public class EntityStateHandler { private Node createUniqueNode(Neo4jPersistentProperty uniqueProperty, Object entity) { final IndexInfo indexInfo = uniqueProperty.getIndexInfo(); final Object value = uniqueProperty.getValueFromEntity(entity, MappingPolicy.MAP_FIELD_DIRECT_POLICY); - if (value==null) return graphDatabase.createNode(null); - return graphDatabase.getOrCreateNode(indexInfo.getIndexName(),indexInfo.getIndexKey(), value, Collections.emptyMap()); + if (value==null) throw new MappingException("Error creating "+uniqueProperty.getOwner().getName()+" with "+entity+" unique property "+uniqueProperty.getName()+" has null value"); + return graphDatabase.getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.emptyMap()); } @SuppressWarnings("unchecked") - private S createRelationship(Object entity, Neo4jPersistentEntity persistentEntity) { + private S getOrCreateRelationship(Object entity, Neo4jPersistentEntity persistentEntity) { final RelationshipProperties relationshipProperties = persistentEntity.getRelationshipProperties(); final Neo4jPersistentProperty startNodeProperty = relationshipProperties.getStartNodeProperty(); Node startNode = (Node) getPersistentState(startNodeProperty.getValue(entity, startNodeProperty.getMappingPolicy())); final Neo4jPersistentProperty endNodeProperty = relationshipProperties.getEndNodeProperty(); Node endNode = (Node) getPersistentState(endNodeProperty.getValue(entity, endNodeProperty.getMappingPolicy())); RelationshipType relationshipType = getRelationshipType(persistentEntity,entity); - return (S) startNode.createRelationshipTo(endNode, relationshipType); + if (persistentEntity.isUnique()) { + final Neo4jPersistentProperty uniqueProperty = persistentEntity.getUniqueProperty(); + final IndexInfo indexInfo = uniqueProperty.getIndexInfo(); + final Object value = uniqueProperty.getValueFromEntity(entity, MappingPolicy.MAP_FIELD_DIRECT_POLICY); + if (value == null) { + throw new MappingException("Error creating "+uniqueProperty.getOwner().getName()+" with "+entity+" unique property "+uniqueProperty.getName()+" has null value"); + } + return (S) graphDatabase.getOrCreateRelationship(indexInfo.getIndexName(),indexInfo.getIndexKey(), value, startNode,endNode,relationshipType.name(), Collections.emptyMap()); + } + return (S) graphDatabase.createRelationship(startNode, endNode, relationshipType, Collections.emptyMap()); } private RelationshipType getRelationshipType(Neo4jPersistentEntity persistentEntity, Object entity) { @@ -206,7 +215,7 @@ public class EntityStateHandler { public RelationshipResult removeRelationshipTo(Object source, Object target, String relationshipType) { final Relationship relationship = getRelationshipBetween(source, target, relationshipType); if (relationship!=null) { - relationship.delete(); + graphDatabase.remove(relationship); return new RelationshipResult(relationship, RelationshipResult.Type.DELETED); } return null; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityStateFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityStateFactory.java index cc9130ad3..bc0708d53 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityStateFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityStateFactory.java @@ -37,6 +37,7 @@ public class NodeEntityStateFactory implements EntityStateFactory { final Class entityType = entity.getClass(); @SuppressWarnings("unchecked") final Neo4jPersistentEntity persistentEntity = (Neo4jPersistentEntity) mappingContext.getPersistentEntity(entityType); + NodeEntityState nodeEntityState = new NodeEntityState(null, entity, entityType, template, nodeDelegatingFieldAccessorFactory, persistentEntity); if (!detachable) { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/mapping/Neo4jEntityPersisterTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/mapping/Neo4jEntityPersisterTest.java index a5bbfabc2..9e8eee32e 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/mapping/Neo4jEntityPersisterTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/mapping/Neo4jEntityPersisterTest.java @@ -17,13 +17,22 @@ package org.springframework.data.neo4j.mapping; import org.junit.Test; import org.mockito.Mockito; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.DynamicRelationshipType; import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.Relationship; +import org.springframework.data.neo4j.model.BestFriend; import org.springframework.data.neo4j.model.Friendship; import org.springframework.data.neo4j.model.Person; import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; + import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; /** * @author mh diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/BestFriend.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/BestFriend.java new file mode 100644 index 000000000..6814b8aa3 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/BestFriend.java @@ -0,0 +1,84 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.model; + + +import org.springframework.data.neo4j.annotation.EndNode; +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.RelationshipEntity; +import org.springframework.data.neo4j.annotation.RelationshipType; +import org.springframework.data.neo4j.annotation.StartNode; +import org.springframework.data.neo4j.fieldaccess.DynamicProperties; + +import java.util.Date; + +@RelationshipEntity(type = "BEST_FRIEND") +public class BestFriend { + @GraphId + private Long id; + + @Indexed(unique = true) + private String secretName; + + public BestFriend() { } + + public BestFriend(Person p1, Person p2, String secretName) { + this.p1 = p1; + this.p2 = p2; + this.secretName = secretName; + } + + @StartNode + private Person p1; + + @EndNode + private Person p2; + + public Person getPerson1() { + return p1; + } + + public Person getPerson2() { + return p2; + } + + public String getSecretName() { + return secretName; + } + + public Long getId() { + return id; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BestFriend friendship = (BestFriend) o; + if (id == null) return super.equals(o); + return id.equals(friendship.id); + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : super.hashCode(); + } + +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Person.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Person.java index 60ab77d71..f6b39e568 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Person.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Person.java @@ -74,6 +74,9 @@ public class Person implements Being { @RelatedTo(type = "boss", direction = Direction.INCOMING) private Person boss; + @RelatedToVia + private BestFriend bestFriend; + @Fetch @RelatedToVia(type = "knows", elementClass = Friendship.class) private Iterable friendships; @@ -292,4 +295,16 @@ public class Person implements Being { public Person(Long graphId) { this.graphId = graphId; } + + public void setBestFriend(Person p2, String secret) { + if (p2==null) { + this.bestFriend = null; + } else { + this.bestFriend = new BestFriend(this, p2,secret); + } + } + + public BestFriend getBestFriend() { + return bestFriend; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueEntityTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueEntityTest.java index 1d7821644..8c81e9531 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueEntityTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueEntityTest.java @@ -74,6 +74,14 @@ public class UniqueEntityTest { assertEquals(1, uniqueClubRepository.count()); } + + @Test(expected = MappingException.class) + public void shouldFailOnNullPropertyValue() { + UniqueClub club = new UniqueClub(); + club.setName(null); + uniqueClubRepository.save(club); + } + @Test public void shouldOnlyCreateSingleInstanceForUniqueNumericNodeEntity() { UniqueNumericIdClub club = new UniqueNumericIdClub(); @@ -86,6 +94,13 @@ public class UniqueEntityTest { assertEquals(1, uniqueNumericIdClubRepository.count()); } + @Test(expected = MappingException.class) + public void shouldFailOnNullNumericPropertyValue() { + UniqueNumericIdClub club = new UniqueNumericIdClub(); + club.setClubId(null); + uniqueNumericIdClubRepository.save(club); + } + @Test public void shouldCreateMultipleInstancesForNonUniqueNodeEntity() { Club club = new Club(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueRelationshipTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueRelationshipTest.java new file mode 100644 index 000000000..f03c19c19 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/unique/UniqueRelationshipTest.java @@ -0,0 +1,107 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.unique; + +import org.junit.Test; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.DynamicRelationshipType; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.springframework.data.neo4j.mapping.Neo4jPersistentTestBase; +import org.springframework.data.neo4j.model.BestFriend; +import org.springframework.data.neo4j.model.Person; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; + +/** + * @author mh + * @since 18.04.12 + */ +public class UniqueRelationshipTest extends Neo4jPersistentTestBase { + + @Test + public void testCreateUniqueRelationship() { + final Person p1 = storeInGraph(michael); + final Person p2 = storeInGraph(andres); + final BestFriend bestFriend = new BestFriend(p1, p2, "cypher"); + template.save(bestFriend); + + final Relationship bestFriendRel = template.getPersistentState(bestFriend); + assertEquals(bestFriendRel,((Node)template.getPersistentState(michael)).getSingleRelationship(DynamicRelationshipType.withName("BEST_FRIEND"), Direction.OUTGOING)); + assertEquals(bestFriendRel.getEndNode(), template.getPersistentState(andres)); + assertEquals("cypher",bestFriendRel.getProperty("secretName")); + assertEquals(bestFriendRel,template.getIndex(BestFriend.class).get("secretName","cypher").getSingle()); + + final Person p3 = storeInGraph(emil); + final BestFriend bestFriend2 = new BestFriend(p1, p3, "cypher"); + template.save(bestFriend2); + + final Relationship bestFriend2Rel = template.getPersistentState(bestFriend2); + assertEquals(bestFriend2Rel, bestFriendRel); + assertEquals(bestFriend2Rel.getEndNode(), template.getPersistentState(andres)); + assertEquals(bestFriendRel,template.getIndex(BestFriend.class).get("secretName","cypher").getSingle()); + } + + @Test + public void testCreateUniqueRelationshipRelatedToVia() { + final Person p1 = storeInGraph(michael); + final Person p2 = storeInGraph(andres); + p1.setBestFriend(p2,"cypher"); + template.save(p1); + final BestFriend bestFriend = p1.getBestFriend(); + + final Relationship bestFriendRel = template.getPersistentState(bestFriend); + assertEquals(bestFriendRel,((Node)template.getPersistentState(michael)).getSingleRelationship(DynamicRelationshipType.withName("BEST_FRIEND"), Direction.OUTGOING)); + assertEquals(bestFriendRel.getEndNode(), template.getPersistentState(andres)); + assertEquals("cypher",bestFriendRel.getProperty("secretName")); + assertEquals(bestFriendRel,template.getIndex(BestFriend.class).get("secretName","cypher").getSingle()); + + final Person p3 = storeInGraph(emil); + p1.setBestFriend(p3,"cypher"); + final BestFriend bestFriend2 = p1.getBestFriend(); + template.save(bestFriend2); + + final Relationship bestFriend2Rel = template.getPersistentState(bestFriend2); + assertEquals(bestFriend2Rel, bestFriendRel); + assertEquals(bestFriend2Rel.getEndNode(), template.getPersistentState(andres)); + assertEquals(bestFriendRel,template.getIndex(BestFriend.class).get("secretName","cypher").getSingle()); + + p1.setBestFriend(null,null); + template.save(p1); + assertEquals(null, ((Node) template.getPersistentState(michael)).getSingleRelationship(DynamicRelationshipType.withName("BEST_FRIEND"), Direction.OUTGOING)); + p1.setBestFriend(p3, "cypher"); + template.save(p1); + final BestFriend bestFriend3 = p1.getBestFriend(); + + final Relationship bestFriend3Rel = template.getPersistentState(bestFriend3); + assertNotSame(bestFriend3Rel, bestFriendRel); + assertEquals(bestFriend3Rel.getEndNode(), template.getPersistentState(emil)); + assertEquals(bestFriend3Rel, template.getIndex(BestFriend.class).get("secretName", "cypher").getSingle()); + } + + @Test + public void testDeleteAndRecreateUniqueRelationship() { + final Node n1 = template.createNode(); + final Node n2 = template.createNode(); + final Relationship r1 = template.getOrCreateRelationship("test", "key", "value", n1, n2, "type", null); + template.delete(r1); + final Node n3 = template.createNode(); + final Relationship r2 = template.getOrCreateRelationship("test", "key", "value", n1, n3, "type", null); + assertFalse("r1 is returned although being deleted", r1.equals(r2)); + } +} diff --git a/src/docbkx/reference/programming-model/indexing.xml b/src/docbkx/reference/programming-model/indexing.xml index 78905d541..535476f59 100644 --- a/src/docbkx/reference/programming-model/indexing.xml +++ b/src/docbkx/reference/programming-model/indexing.xml @@ -118,13 +118,21 @@ Person mark = graphRepository.findAllByQuery("people-search", "name", "ma*"); Unique indexes Unique indexing with index.putIfAbsent and UniqueFactory was introduced in Neo4j 1.6. - In Spring Data Neo4j this is made available via Neo4jTemplate.getOrCreateNode. - + It is also available via the REST API. + In Spring Data Neo4j this is made available via Neo4jTemplate.getOrCreateNode and + Neo4jTemplate.getOrCreateRelationship. In an entity at most one field can be annotated with @Indexed(unique=true) regardless of the index-type used. The uniqueness will be taken into account when creating the entity by reusing an existing entity if that unique key-combination already exists. On saving of the field it will be cross-checked against the index and fail with a DataIntegrityViolationException - if the field was changed to an already existing unique value. + if the field was changed to an already existing unique value. Null values are no longer allowed for these properties. + + + + This works for both Node-Entities as well as Relationship-Entities. Relationship-Uniqueness in Neo4j is global so that + a existing unique instance of this relationship may connect two completely different nodes and might also have a + different type. + diff --git a/src/docbkx/tutorial/indexing.xml b/src/docbkx/tutorial/indexing.xml index 61a731f94..b19fa5a41 100644 --- a/src/docbkx/tutorial/indexing.xml +++ b/src/docbkx/tutorial/indexing.xml @@ -7,13 +7,14 @@ There is an @Indexed annotation for fields. We wanted to try this out, and use it to guide the next test. We added @Indexed to the id field of the Movie class. This field is intended to represent the external ID that will be used in URIs and will be stable across database imports and updates. + That's why we also declare it as unique. This time we went with a simple GraphRepository to retrieve the indexed movie. Exact Indexing for Movie id @RelatedTo usage movies; @@ -168,7 +168,7 @@ class Actor { @RelatedToVia usage Populating the database Before we opened the gates we needed to add some movie data. So we wrote a small class for - populating the database which could be called from our controller. To make it safe to call - several times we added index lookups to check for existing entries. A simple /populate endpoint + populating the database which could be called from our controller. A simple /populate endpoint for the controller that called it would be enough for now. diff --git a/src/docbkx/tutorial/social.xml b/src/docbkx/tutorial/social.xml index b6eb3d5e7..2085fdb25 100644 --- a/src/docbkx/tutorial/social.xml +++ b/src/docbkx/tutorial/social.xml @@ -27,7 +27,7 @@ Social entities