DATAGRAPH-219, DATAGRAPH-210 unique relationship, and documentation for unique entities
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -70,7 +70,7 @@ public class RelationshipHelper {
|
||||
protected void removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet(Node node, Set<Node> targetNodes) {
|
||||
for (Relationship relationship : node.getRelationships(type, direction)) {
|
||||
if (!targetNodes.remove(relationship.getOtherNode(node)))
|
||||
relationship.delete();
|
||||
template.delete(relationship);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.<String,Object>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.<String,Object>emptyMap());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <S extends PropertyContainer> S createRelationship(Object entity, Neo4jPersistentEntity<?> persistentEntity) {
|
||||
private <S extends PropertyContainer> 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.<String,Object>emptyMap());
|
||||
}
|
||||
return (S) graphDatabase.createRelationship(startNode, endNode, relationshipType, Collections.<String,Object>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;
|
||||
|
||||
@@ -37,6 +37,7 @@ public class NodeEntityStateFactory implements EntityStateFactory<Node> {
|
||||
final Class<?> entityType = entity.getClass();
|
||||
@SuppressWarnings("unchecked") final Neo4jPersistentEntity<Object> persistentEntity =
|
||||
(Neo4jPersistentEntity<Object>) mappingContext.getPersistentEntity(entityType);
|
||||
|
||||
NodeEntityState nodeEntityState = new NodeEntityState(null, entity, entityType, template,
|
||||
nodeDelegatingFieldAccessorFactory, persistentEntity);
|
||||
if (!detachable) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Friendship> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -118,13 +118,21 @@ Person mark = graphRepository.findAllByQuery("people-search", "name", "ma*");
|
||||
<title>Unique indexes</title>
|
||||
<para>
|
||||
Unique indexing with <code>index.putIfAbsent</code> and <code>UniqueFactory</code> was introduced in Neo4j 1.6.
|
||||
In Spring Data Neo4j this is made available via <code>Neo4jTemplate.getOrCreateNode</code>.
|
||||
|
||||
It is also available via the REST API.
|
||||
In Spring Data Neo4j this is made available via <code>Neo4jTemplate.getOrCreateNode</code> and
|
||||
<code>Neo4jTemplate.getOrCreateRelationship</code>.
|
||||
</para>
|
||||
<para>In an entity at most one field can be annotated with <code>@Indexed(unique=true)</code> 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.
|
||||
</para>
|
||||
<para>
|
||||
<note>
|
||||
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.
|
||||
</note>
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
|
||||
@@ -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 <code>id</code> 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.
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
<title>Exact Indexing for Movie id</title>
|
||||
<programlisting language="java" ><![CDATA[@NodeEntity class Movie {
|
||||
@Indexed String id;
|
||||
@Indexed(unique=true) String id;
|
||||
String title;
|
||||
int year;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class Actor {
|
||||
<title>@RelatedTo usage</title>
|
||||
<programlisting language="java" ><![CDATA[@NodeEntity
|
||||
class Movie {
|
||||
@Indexed int id;
|
||||
@Indexed(unique=true) String id;
|
||||
String title;
|
||||
int year;
|
||||
@RelatedTo(type = "ACTS_IN", direction = Direction.INCOMING)
|
||||
@@ -117,7 +117,7 @@ class Movie {
|
||||
|
||||
@NodeEntity
|
||||
class Actor {
|
||||
@Indexed int id;
|
||||
@Indexed(unique=true) int id;
|
||||
String name;
|
||||
@RelatedTo(type = "ACTS_IN")
|
||||
Set<Movie> movies;
|
||||
@@ -168,7 +168,7 @@ class Actor {
|
||||
<title>@RelatedToVia usage</title>
|
||||
<programlisting language="java" ><![CDATA[@NodeEntity
|
||||
class Movie {
|
||||
@Indexed int id;
|
||||
@Indexed(unique=true) String id;
|
||||
String title;
|
||||
int year;
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
<title>Populating the database</title>
|
||||
<para>
|
||||
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 <code>/populate</code> endpoint
|
||||
populating the database which could be called from our controller. A simple <code>/populate</code> endpoint
|
||||
for the controller that called it would be enough for now.
|
||||
</para>
|
||||
<para>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<title>Social entities</title>
|
||||
<programlisting language="java" ><![CDATA[@NodeEntity
|
||||
class User {
|
||||
@Indexed String login;
|
||||
@Indexed(unique=true) String login;
|
||||
String name;
|
||||
String password;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user