diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Account1Repository.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Account1Repository.java new file mode 100644 index 000000000..739ac8339 --- /dev/null +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Account1Repository.java @@ -0,0 +1,11 @@ +package org.springframework.data.neo4j.aspects; + +import org.springframework.data.neo4j.aspects.support.domain.Account1; +import org.springframework.data.neo4j.repository.GraphRepository; + +/** + * @author Nicki Watt + * @since 06-04-2014 + */ +public interface Account1Repository extends GraphRepository { +} diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Account2Repository.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Account2Repository.java new file mode 100644 index 000000000..8b2e50d74 --- /dev/null +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Account2Repository.java @@ -0,0 +1,12 @@ +package org.springframework.data.neo4j.aspects; + + +import org.springframework.data.neo4j.aspects.support.domain.Account2; +import org.springframework.data.neo4j.repository.GraphRepository; + +/** + * @author Nicki Watt + * @since 06-04-2014 + */ +public interface Account2Repository extends GraphRepository { +} diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/EntityTestBase.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/EntityTestBase.java index 1c99cb5b1..6f4a2fea2 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/EntityTestBase.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/EntityTestBase.java @@ -24,9 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.ConversionService; -import org.springframework.data.neo4j.aspects.FriendshipRepository; -import org.springframework.data.neo4j.aspects.GroupRepository; -import org.springframework.data.neo4j.aspects.PersonRepository; +import org.springframework.data.neo4j.aspects.*; import org.springframework.data.neo4j.support.Neo4jTemplate; import org.springframework.data.neo4j.support.mapping.StoredEntityType; import org.springframework.data.neo4j.support.node.Neo4jHelper; @@ -48,6 +46,8 @@ public class EntityTestBase { @Autowired protected GraphDatabaseService graphDatabaseService; + @Autowired protected Account1Repository account1Repository; + @Autowired protected Account2Repository account2Repository; @Autowired protected PersonRepository personRepository; @Autowired protected GroupRepository groupRepository; @Autowired protected FriendshipRepository friendshipRepository; diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/GraphRepositoryTests.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/GraphRepositoryTests.java index 3fbc082bd..777d79b7b 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/GraphRepositoryTests.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/GraphRepositoryTests.java @@ -20,18 +20,20 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.neo4j.graphdb.ConstraintViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.aspects.Group; import org.springframework.data.neo4j.aspects.Person; -import org.springframework.test.annotation.DirtiesContext; +import org.springframework.data.neo4j.aspects.core.NodeBacked; +import org.springframework.data.neo4j.aspects.support.domain.Account1; +import org.springframework.data.neo4j.aspects.support.domain.Account2; import org.springframework.test.context.CleanContextCacheTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.test.context.transaction.BeforeTransaction; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.transaction.annotation.Transactional; @@ -40,9 +42,10 @@ import java.util.Map; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.hamcrest.Matchers.*; import static org.neo4j.helpers.collection.IteratorUtil.addToCollection; import static org.neo4j.helpers.collection.IteratorUtil.asCollection; @@ -123,4 +126,66 @@ public class GraphRepositoryTests extends EntityTestBase { Group team = personRepository.findTeam(testTeam.michael); assertThat(team, is(testTeam.sdg)); } + + + @Test + @Transactional + public void testSaveWhenFailOnDuplicateSetToFalse() { + // Account1 + // @Indexed(unique = true, failOnDuplicate = false) + // private String accountNumber; + Account1 acc1 = new Account1("111-222-333", "Mr George - Current Account 1"); + Account1 acc2 = new Account1("111-222-333", "Mr George - Current Account 2"); + Account1 savedAcc1 = account1Repository.save(acc1); + Account1 savedAcc2 = account1Repository.save(acc2); + assertEquals("expecting the saving of the same entity result in a merge of nodes", ((NodeBacked)savedAcc1).getNodeId(), ((NodeBacked)savedAcc2).getNodeId()); + assertEquals("Mr George - Current Account 2", savedAcc2.getName() ); + + Account1 loadedAcc1 = account1Repository.findBySchemaPropertyValue("accountNumber", "111-222-333"); + assertEquals("Mr George - Current Account 2", loadedAcc1.getName() ); + + } + + @Test(expected = ConstraintViolationException.class) + @Transactional + public void testSaveWhenFailOnDuplicateSetToTrue() { + // Account2 + // @Indexed(unique = true, failOnDuplicate = true) + // private String accountNumber; + Account2 acc1 = new Account2("111-222-333", "Mr George - Current Account 1"); + Account2 acc2 = new Account2("111-222-333", "Mr George - Current Account 2"); + Account2 savedAcc1 = account2Repository.save(acc1); + Account2 savedAcc2 = account2Repository.save(acc2); + } + + @Test + @Transactional + public void testSaveWhenDefaultFailOnDuplicateSetToTrueAllowsUpdates() { + // Account2 + // @Indexed(unique = true, failOnDuplicate = true) + // private String accountNumber; + Account2 acc1 = new Account2("111-222-333", "Mr George - Current Account 1"); + Account2 savedAcc1 = account2Repository.save(acc1); + + acc1.setName("Mr George - Current Account 2"); + account2Repository.save(savedAcc1); + // No exception expected! + } + + /* + @Ignore("Not catering for explicit overrides at present") + @Test(expected = ConstraintViolationException.class) + @Transactional + public void testSaveWithOverrideFailOnDuplicateSetToTrue() { + // Account1 + // @Indexed(unique = true, failOnDuplicate = false) + // private String accountNumber; + Account1 acc1 = new Account1("111-222-333", "Mr George - Current Account 1"); + Account1 acc2 = new Account1("111-222-333", "Mr George - Current Account 2"); + Account1 savedAcc1 = account1Repository.save(acc1,true); + Account1 savedAcc2 = account1Repository.save(acc2,true); + } + */ + + } diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTests.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTests.java index 87a9665cb..945ee8726 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTests.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTests.java @@ -20,6 +20,7 @@ import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.neo4j.graphdb.ConstraintViolationException; import org.neo4j.graphdb.Label; import org.neo4j.graphdb.Transaction; import org.neo4j.helpers.collection.IteratorUtil; @@ -27,6 +28,8 @@ import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.neo4j.aspects.Attribute; import org.springframework.data.neo4j.aspects.Group; import org.springframework.data.neo4j.aspects.Person; +import org.springframework.data.neo4j.aspects.support.domain.Account1; +import org.springframework.data.neo4j.aspects.support.domain.Account2; import org.springframework.test.context.CleanContextCacheTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -45,6 +48,7 @@ import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.hasItems; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import static org.springframework.data.neo4j.aspects.Person.persistedPerson; @RunWith(SpringJUnit4ClassRunner.class) @@ -270,4 +274,44 @@ public class NodeEntityTests extends EntityTestBase { g.getReadOnlyPersons(); g.persist(); } + + @Test @Transactional + public void testDefaultFailOnDuplicateSetToTrueSavesCorrectlyWhenUpdating() { + final String UPDATED_NAME = "Mr George - Current Account 2"; + Account2 acc1 = new Account2("111-222-333", "Mr George - Current Account 1"); + acc1.persist(); + acc1.setName(UPDATED_NAME); + acc1.persist(); // This should save fine + + Account2 loadedAcc1 = account2Repository.findBySchemaPropertyValue("accountNumber","111-222-333"); + assertNotNull(loadedAcc1); + assertEquals("Expected name to have been updated (merged) with last value",UPDATED_NAME,loadedAcc1.getName()); + } + + @Test(expected = ConstraintViolationException.class) @Transactional + public void testDefaultFailOnDuplicateSetToTrueCausesExceptionWhenAnotherDuplicateEntityCreated() { + Account2 acc1 = new Account2("111-222-333", "Mr George - Current Account 1"); + Account2 acc2 = new Account2("111-222-333", "Mr George - Current Account 2"); + acc1.persist(); + acc2.persist(); // This should cause an exception + } + + @Test @Transactional + public void testDefaultFailOnDuplicateSetToFalseCausesMergeWhenAnotherDuplicateEntityCreated() { + final String UPDATED_NAME = "Mr George - Current Account 2"; + Account1 acc1 = new Account1("111-222-333", "Mr George - Current Account 1"); + Account1 acc2 = new Account1("111-222-333", UPDATED_NAME); + + acc1.persist(); + Long acc1NodeId = acc1.getNodeId(); + acc2.persist(); // This should be merged + Long acc2NodeId = acc2.getNodeId(); + + assertEquals("Ids should be the same",acc1NodeId,acc2NodeId); + Account1 loadedAcc1 = account1Repository.findBySchemaPropertyValue("accountNumber","111-222-333"); + assertNotNull(loadedAcc1); + assertEquals("Expected name to have been updated (merged) with last value",UPDATED_NAME,loadedAcc1.getName()); + } + + } diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/domain/Account1.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/domain/Account1.java new file mode 100644 index 000000000..6c389819f --- /dev/null +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/domain/Account1.java @@ -0,0 +1,59 @@ +/** + * 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.aspects.support.domain; + +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.NodeEntity; + + +@NodeEntity +public class Account1 { + + private static final long serialVersionUID = 1L; + + @Indexed(unique = true, failOnDuplicate = false) + private String accountNumber; + + private String name; + + public Account1() { + } + + public Account1(String accountNumber, String name) { + this.accountNumber = accountNumber; + this.name = name; + } + + public String getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + +} diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/domain/Account2.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/domain/Account2.java new file mode 100644 index 000000000..867d4e5c8 --- /dev/null +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/domain/Account2.java @@ -0,0 +1,81 @@ +/** + * 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.aspects.support.domain; + +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.NodeEntity; + + +@NodeEntity +public class Account2 { + + private static final long serialVersionUID = 1L; + + @GraphId + private Long graphId; + + @Indexed(unique = true, failOnDuplicate = true) + private String accountNumber; + + private String name; + + public Account2() { + } + + public Account2(String accountNumber, String name) { + this.accountNumber = accountNumber; + this.name = name; + } + + public Long getGraphId() { + return graphId; + } + + public String getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Account2 person = (Account2) o; + if (graphId == null) return super.equals(o); + return graphId.equals(person.graphId); + + } + + @Override + public int hashCode() { + return graphId != null ? graphId.hashCode() : super.hashCode(); + } + +} diff --git a/spring-data-neo4j-aspects/src/test/resources/org/springframework/data/neo4j/aspects/support/Neo4jGraphPersistenceTests-context.xml b/spring-data-neo4j-aspects/src/test/resources/org/springframework/data/neo4j/aspects/support/Neo4jGraphPersistenceTests-context.xml index 12a504bb8..9d3e58113 100644 --- a/spring-data-neo4j-aspects/src/test/resources/org/springframework/data/neo4j/aspects/support/Neo4jGraphPersistenceTests-context.xml +++ b/spring-data-neo4j-aspects/src/test/resources/org/springframework/data/neo4j/aspects/support/Neo4jGraphPersistenceTests-context.xml @@ -115,6 +115,8 @@ + org.springframework.data.neo4j.aspects.support.domain.Account1 + org.springframework.data.neo4j.aspects.support.domain.Account2 org.springframework.data.neo4j.aspects.Developer org.springframework.data.neo4j.aspects.Person org.springframework.data.neo4j.aspects.Group @@ -168,6 +170,14 @@ + + + + + + + + diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java index 047a8b6e7..65efa0806 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java @@ -17,12 +17,11 @@ package org.springframework.data.neo4j.rest.integration; import org.junit.AfterClass; -import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.neo4j.aspects.support.GraphRepositoryTests; import org.springframework.data.neo4j.rest.support.RestTestBase; -import org.springframework.data.neo4j.support.node.Neo4jHelper; import org.springframework.test.context.CleanContextCacheTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -53,7 +52,13 @@ public class RestGraphRepositoryTests extends GraphRepositoryTests { @AfterClass public static void shutdownDb() { RestTestBase.shutdownDb(); + } + // TODO - Change REST to have a better (more descriptive) + // exception thrown when duplicate violations occur + @Test(expected = IllegalStateException.class) + public void testSaveWhenFailOnDuplicateSetToTrue() { + super.testSaveWhenFailOnDuplicateSetToTrue(); } } diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java index 5fa1ab354..b11498373 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java @@ -16,11 +16,9 @@ package org.springframework.data.neo4j.rest.integration; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; +import org.junit.*; import org.junit.runner.RunWith; +import org.neo4j.graphdb.ConstraintViolationException; import org.springframework.data.neo4j.aspects.support.NodeEntityTests; import org.springframework.data.neo4j.rest.support.RestTestBase; import org.springframework.test.context.CleanContextCacheTestExecutionListener; @@ -60,4 +58,11 @@ public class RestNodeEntityTests extends NodeEntityTests { public void testSetShortProperty() { // super.testSetShortProperty(); } + + // TODO - Change REST to have a better (more descriptive) + // exception thrown when duplicate violations occur + @Test(expected = IllegalStateException.class) + public void testDefaultFailOnDuplicateSetToTrueCausesExceptionWhenAnotherDuplicateEntityCreated() { + super.testDefaultFailOnDuplicateSetToTrueCausesExceptionWhenAnotherDuplicateEntityCreated(); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Indexed.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Indexed.java index 1e22cd7d6..6666cab1d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Indexed.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Indexed.java @@ -39,10 +39,22 @@ public @interface Indexed { String fieldName() default ""; + /** + * Indicates whether to apply a unique constraint on this property, defaults to false. + */ boolean unique() default false; boolean numeric() default false; + /** + * Only applicable when indexType is LABEL and unique=true, indicates how to handle attempts to save + * entities where this unique property already exists, defaults to false. When set to false, default entity + * saving behaviour resorts to merge type behaviour whilst when set to true, results in an exception being + * thrown when attempting to save another entity where the same unique indexed property already exists under + * a different node id. + */ + boolean failOnDuplicate() default false; + // FQN is a fix for javac compiler bug http://bugs.sun.com/view_bug.do?bug_id=6512707 org.springframework.data.neo4j.annotation.Indexed.Level level() default org.springframework.data.neo4j.annotation.Indexed.Level.CLASS; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/IndexInfo.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/IndexInfo.java index 6b4fcd5b3..9f866c5da 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/IndexInfo.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/IndexInfo.java @@ -31,6 +31,7 @@ public class IndexInfo { private final Indexed.Level level; private String indexKey; private final boolean unique; + private final boolean failOnDuplicate; private boolean numeric; private Indexed annotation; private Neo4jPersistentProperty property; @@ -43,6 +44,7 @@ public class IndexInfo { fieldName = annotation.fieldName(); this.indexKey = fieldName.isEmpty() ? property.getNeo4jPropertyName() : fieldName; unique = annotation.unique(); + failOnDuplicate = annotation.failOnDuplicate(); level = annotation.level(); numeric = annotation.numeric(); } @@ -114,6 +116,10 @@ public class IndexInfo { return unique; } + public boolean isFailOnDuplicate() { + return failOnDuplicate; + } + public boolean isNumeric() { return numeric; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/SchemaIndexRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/SchemaIndexRepository.java index 532bd0f86..e6e4e438c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/SchemaIndexRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/SchemaIndexRepository.java @@ -26,9 +26,22 @@ import org.springframework.transaction.annotation.Transactional; */ public interface SchemaIndexRepository { + /** + * Finds an entity based on the provided schema indexed property value if one exists. + * @param property The name of the schema indexed property + * @param value The value of the schema indexed property + * @return The single entity associated with this property value setting, or + * null if one does not exist. + */ @Transactional T findBySchemaPropertyValue(String property, Object value); + /** + * Finds all entities which have a schema indexed property set to specified value. + * @param property The name of the schema indexed property + * @param value The value of the schema indexed property + * @return A result of all entities which match indexed property value. + */ @Transactional Result findAllBySchemaPropertyValue(String property, Object value); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java index f4f65106e..2c470a1e1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java @@ -69,6 +69,7 @@ import java.util.Collections; import java.util.Map; import static java.lang.String.format; +import static org.neo4j.helpers.collection.MapUtil.map; import static org.springframework.data.neo4j.support.ParameterCheck.notNull; /** @@ -738,14 +739,16 @@ public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware { return infrastructure.getMappingContext(); } - public Node createUniqueNode(Object entity) { + public Node createUniqueNode(Object entity) { final Neo4jPersistentEntityImpl persistentEntity = getPersistentEntity(entity.getClass()); final Neo4jPersistentProperty uniqueProperty = persistentEntity.getUniqueProperty(); Object value = uniqueProperty.getValueFromEntity(entity, MappingPolicy.MAP_FIELD_DIRECT_POLICY); if (value == null) return createNode(); final IndexInfo indexInfo = uniqueProperty.getIndexInfo(); if (indexInfo.isLabelBased()) { - return getGraphDatabase().merge(indexInfo.getIndexName(),indexInfo.getIndexKey(),value, Collections.emptyMap(), persistentEntity.getAllLabels()); + return (indexInfo.isFailOnDuplicate()) + ? getGraphDatabase().createNode(map(uniqueProperty.getName(),value),persistentEntity.getAllLabels()) + : getGraphDatabase().merge(indexInfo.getIndexName(),indexInfo.getIndexKey(),value, Collections.emptyMap(), persistentEntity.getAllLabels()); } else { if (value instanceof Number && indexInfo.isNumeric()) value = ValueContext.numeric((Number) value); return getGraphDatabase().getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.emptyMap(), persistentEntity.getAllLabels()); 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 634c24cd7..1480fa29f 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 @@ -35,6 +35,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.neo4j.helpers.collection.MapUtil.map; + /** * @author mh * @since 02.10.11 @@ -151,7 +153,9 @@ public class EntityStateHandler { 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"); if (indexInfo.isLabelBased()) { - return graphDatabase.merge(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.emptyMap(), persistentEntity.getAllLabels()); + return (indexInfo.isFailOnDuplicate()) + ? graphDatabase.createNode(map(uniqueProperty.getName(),value),persistentEntity.getAllLabels()) + : graphDatabase.merge(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.emptyMap(), persistentEntity.getAllLabels()); } else { return graphDatabase.getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.emptyMap(),persistentEntity.getAllLabels()); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Account1.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Account1.java new file mode 100644 index 000000000..98c2def43 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Account1.java @@ -0,0 +1,81 @@ +/** + * 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.GraphId; +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.NodeEntity; + + +@NodeEntity +public class Account1 { + + private static final long serialVersionUID = 1L; + + @GraphId + private Long graphId; + + @Indexed(unique = true, failOnDuplicate = false) + private String accountNumber; + + private String name; + + public Account1() { + } + + public Account1(String accountNumber, String name) { + this.accountNumber = accountNumber; + this.name = name; + } + + public Long getGraphId() { + return graphId; + } + + public String getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Account1 person = (Account1) o; + if (graphId == null) return super.equals(o); + return graphId.equals(person.graphId); + + } + + @Override + public int hashCode() { + return graphId != null ? graphId.hashCode() : super.hashCode(); + } + +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Account2.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Account2.java new file mode 100644 index 000000000..c069a6cc7 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/model/Account2.java @@ -0,0 +1,81 @@ +/** + * 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.GraphId; +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.NodeEntity; + + +@NodeEntity +public class Account2 { + + private static final long serialVersionUID = 1L; + + @GraphId + private Long graphId; + + @Indexed(unique = true, failOnDuplicate = true) + private String accountNumber; + + private String name; + + public Account2() { + } + + public Account2(String accountNumber, String name) { + this.accountNumber = accountNumber; + this.name = name; + } + + public Long getGraphId() { + return graphId; + } + + public String getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Account2 person = (Account2) o; + if (graphId == null) return super.equals(o); + return graphId.equals(person.graphId); + + } + + @Override + public int hashCode() { + return graphId != null ? graphId.hashCode() : super.hashCode(); + } + +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repositories/Account1Repository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repositories/Account1Repository.java new file mode 100644 index 000000000..ed6f6a7ad --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repositories/Account1Repository.java @@ -0,0 +1,11 @@ +package org.springframework.data.neo4j.repositories; + +import org.springframework.data.neo4j.model.Account1; +import org.springframework.data.neo4j.repository.GraphRepository; + +/** + * @author Nicki Watt + * @since 06-04-2014 + */ +public interface Account1Repository extends GraphRepository { +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repositories/Account2Repository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repositories/Account2Repository.java new file mode 100644 index 000000000..f8a6110ab --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repositories/Account2Repository.java @@ -0,0 +1,11 @@ +package org.springframework.data.neo4j.repositories; + +import org.springframework.data.neo4j.model.Account2; +import org.springframework.data.neo4j.repository.GraphRepository; + +/** + * @author Nicki Watt + * @since 06-04-2014 + */ +public interface Account2Repository extends GraphRepository { +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTests.java index e55d7201d..1fdcbdcfa 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTests.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTests.java @@ -20,6 +20,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.neo4j.graphdb.ConstraintViolationException; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.helpers.collection.IteratorUtil; @@ -86,6 +87,10 @@ public class GraphRepositoryTests { @Autowired private BeingRepository beingRepository; @Autowired + private Account1Repository account1Repository; + @Autowired + private Account2Repository account2Repository; + @Autowired org.springframework.data.neo4j.repositories.GroupRepository groupRepository; @Autowired @@ -155,6 +160,45 @@ public class GraphRepositoryTests { }); } + @Test + @Transactional + public void testSaveWithDefaultFailOnDuplicateSetToFalse() { + // Account1 + // @Indexed(unique = true, failOnDuplicate = false) + // private String accountNumber; + Account1 acc1 = new Account1("111-222-333", "Mr George - Current Account 1"); + Account1 acc2 = new Account1("111-222-333", "Mr George - Current Account 2"); + Account1 savedAcc1 = account1Repository.save(acc1); + Account1 savedAcc2 = account1Repository.save(acc2); + assertEquals("expecting the saving of the same entity result in a merge of nodes", savedAcc1.getGraphId(), savedAcc2.getGraphId()); + } + + @Test(expected = ConstraintViolationException.class) + @Transactional + public void testSaveWithDefaultFailOnDuplicateSetToTrue() { + // Account2 + // @Indexed(unique = true, failOnDuplicate = true) + // private String accountNumber; + Account2 acc1 = new Account2("111-222-333", "Mr George - Current Account 1"); + Account2 acc2 = new Account2("111-222-333", "Mr George - Current Account 2"); + Account2 savedAcc1 = account2Repository.save(acc1); + Account2 savedAcc2 = account2Repository.save(acc2); + } + + @Test + @Transactional + public void testSaveWithDefaultFailOnDuplicateSetToTrueAllowsUpdates() { + // Account2 + // @Indexed(unique = true, failOnDuplicate = true) + // private String accountNumber; + Account2 acc1 = new Account2("111-222-333", "Mr George - Current Account 1"); + Account2 savedAcc1 = account2Repository.save(acc1); + + acc1.setName("Mr George - Current Account 2"); + account2Repository.save(savedAcc1); + // No exception expected! + } + @Test @Transactional public void findAll() { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTests.java index ecc075292..8ab832930 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTests.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTests.java @@ -30,12 +30,10 @@ import org.neo4j.helpers.collection.MapUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.neo4j.conversion.Result; import org.springframework.data.neo4j.mapping.ManagedEntity; -import org.springframework.data.neo4j.model.Friendship; -import org.springframework.data.neo4j.model.Group; -import org.springframework.data.neo4j.model.Named; -import org.springframework.data.neo4j.model.Person; +import org.springframework.data.neo4j.model.*; import org.springframework.data.neo4j.repository.GraphRepository; import org.springframework.data.neo4j.support.query.CypherQueryEngine; import org.springframework.data.neo4j.template.Neo4jOperations; @@ -265,6 +263,33 @@ public class EntityNeo4jTemplateTests extends EntityTestBase { assertEquals(false, template.isRelationshipEntity(Object.class)); } + @Test @Transactional + public void testDefaultCreateUniqueNodeWithFailOnDuplicateSetToFalse() throws Exception { + final Account1 acc = new Account1("111-222-333", "Mr George - Current Account"); + Node acc1Node = template.createUniqueNode(acc); + Node acc2Node = template.createUniqueNode(acc); + assertEquals("When failOnDuplicate is set to false, duplicate creations should be merged to same node", acc1Node.getId(), acc2Node.getId()); + } + + @Test(expected = ConstraintViolationException.class) @Transactional + public void testDefaultCreateUniqueNodeWithFailOnDuplicateSetToTrue() throws Exception { + final Account2 acc = new Account2("111-222-333", "Mr George - Current Account"); + Node acc1Node = template.createUniqueNode(acc); + Node acc2Node = template.createUniqueNode(acc); + } + + + @Ignore("Not able to take failOnDuplicate into account") + @Test @Transactional + public void testCreateNodeAsIgnoresFailOnDuplicateValue() throws Exception { + Account1 acc1 = template.createNodeAs(Account1.class,map("accountNumber","111-222-333","name","Mr George - Current Account")); + Account1 acc2 = template.createNodeAs(Account1.class,map("accountNumber","111-222-333","name","Mr George - Current Account")); + // When failOnDuplicate is set to false, createNodeAs should not be + // throwing an exception, but it currently does as it does not take + // this indo into account? + } + + @Test @Transactional public void testSave() throws Exception { final Person thomas = new Person("Thomas", 30);