diff --git a/spring-data-neo4j-aspects/pom.xml b/spring-data-neo4j-aspects/pom.xml
index 283cb6054..d69319412 100644
--- a/spring-data-neo4j-aspects/pom.xml
+++ b/spring-data-neo4j-aspects/pom.xml
@@ -90,12 +90,12 @@
org.springframework.data
spring-data-neo4j
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
org.springframework.data
spring-data-neo4j
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
test-jar
test
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..ecd8f621e 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
@@ -17,21 +17,22 @@
package org.springframework.data.neo4j.aspects.support;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.springframework.dao.DataIntegrityViolationException;
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 +41,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 +125,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 = DataIntegrityViolationException.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..53f420534 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,13 +20,17 @@ 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;
+import org.springframework.dao.DataIntegrityViolationException;
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 +49,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 +275,45 @@ 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 = DataIntegrityViolationException.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-cross-store/pom.xml b/spring-data-neo4j-cross-store/pom.xml
index f3869281d..fdf07bc3a 100644
--- a/spring-data-neo4j-cross-store/pom.xml
+++ b/spring-data-neo4j-cross-store/pom.xml
@@ -48,25 +48,25 @@
org.springframework.data
spring-data-neo4j-aspects
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
org.springframework.data
spring-data-neo4j-tx
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
org.springframework.data
spring-data-neo4j
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
org.springframework.data
spring-data-neo4j
test-jar
test
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml
index 09b8f77a3..7767efc4d 100644
--- a/spring-data-neo4j-rest/pom.xml
+++ b/spring-data-neo4j-rest/pom.xml
@@ -92,7 +92,7 @@
org.springframework.data
spring-data-neo4j
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
org.neo4j
@@ -223,14 +223,14 @@
org.springframework.data
spring-data-neo4j-aspects
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
test
org.springframework.data
spring-data-neo4j
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
test
test-jar
@@ -238,7 +238,7 @@
org.springframework.data
spring-data-neo4j-aspects
- 3.1.0.BUILD-SNAPSHOT
+ ${project.version}
test
test-jar
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..30293832c 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,21 @@ 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
+ /*
+ Unfortunately the REST scenario does not provide us with enough info to work out that this
+ was a constaint violation (other than parsing the message itself which is not great) as it
+ merely throws an IllegalStateException when no content is found for a response when adding
+ labels. The org.neo4j.rest.graphdb.ExecutingRestAPI.addLabels(RestNode node, String...labels)
+ method is the current offender in this case. The REST API itself would probably need to change for
+ us to be able to deal with this appropriately in SDN
+ */
+ @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/Neo4jExceptionTranslator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jExceptionTranslator.java
index 194cbdff8..3adaf9067 100644
--- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jExceptionTranslator.java
+++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jExceptionTranslator.java
@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.support;
+import org.neo4j.graphdb.ConstraintViolationException;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.NotInTransactionException;
import org.neo4j.graphdb.TransactionFailureException;
@@ -45,6 +46,8 @@ public class Neo4jExceptionTranslator implements PersistenceExceptionTranslator
throw (InvalidEntityTypeException)iae.getCause();
}
throw new InvalidDataAccessApiUsageException(iae.getMessage(),iae);
+ } catch(ConstraintViolationException cve) {
+ throw new DataIntegrityViolationException(cve.getMessage(),cve);
} catch(DataAccessException dae) {
throw dae;
} catch(NotInTransactionException nit) {
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/main/java/org/springframework/data/neo4j/support/node/NodeEntityState.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityState.java
index 72eedad27..66d7a13e9 100644
--- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityState.java
+++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/NodeEntityState.java
@@ -16,8 +16,11 @@
package org.springframework.data.neo4j.support.node;
+import org.neo4j.graphdb.ConstraintViolationException;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotInTransactionException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.data.neo4j.fieldaccess.DefaultEntityState;
@@ -61,6 +64,8 @@ public class NodeEntityState extends DefaultEntityState {
template.postEntityCreation(node, type);
} catch (NotInTransactionException e) {
throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e);
+ } catch (ConstraintViolationException e) {
+ throw new DataIntegrityViolationException(e.getMessage(),e);
}
}
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..ca22e89ce 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
@@ -26,6 +26,7 @@ import org.neo4j.helpers.collection.IteratorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
@@ -63,7 +64,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
-import static org.junit.Assert.assertEquals;
import static org.neo4j.helpers.collection.IteratorUtil.addToCollection;
import static org.neo4j.helpers.collection.IteratorUtil.asCollection;
import static org.neo4j.helpers.collection.MapUtil.map;
@@ -86,6 +86,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 +159,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 = DataIntegrityViolationException.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);