DATAGRAPH-219, DATAGRAPH-210 unique relationship, and documentation for unique entities

This commit is contained in:
Michael Hunger
2012-04-19 11:15:00 +02:00
parent 3221213e02
commit caed15e2b7
16 changed files with 268 additions and 20 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;