Merge pull request #192 from nickithewatt/DATAGRAPH-445

DATAGRAPH-445 Implement create_or_fail for GraphRepository
This commit is contained in:
Michael Hunger
2014-06-21 19:59:29 +02:00
26 changed files with 630 additions and 31 deletions

View File

@@ -90,12 +90,12 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>

View File

@@ -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<Account1> {
}

View File

@@ -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<Account2> {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -115,6 +115,8 @@
<bean id="mappingContext" class="org.springframework.data.neo4j.support.mapping.Neo4jMappingContext">
<property name="initialEntitySet">
<set>
<value>org.springframework.data.neo4j.aspects.support.domain.Account1</value>
<value>org.springframework.data.neo4j.aspects.support.domain.Account2</value>
<value>org.springframework.data.neo4j.aspects.Developer</value>
<value>org.springframework.data.neo4j.aspects.Person</value>
<value>org.springframework.data.neo4j.aspects.Group</value>
@@ -168,6 +170,14 @@
</bean>
</property>
</bean>
<bean id="account2Repository" class="org.springframework.data.neo4j.repository.GraphRepositoryFactoryBean">
<property name="repositoryInterface" value="org.springframework.data.neo4j.aspects.Account2Repository" />
<property name="neo4jTemplate" ref="template"/>
</bean>
<bean id="account1Repository" class="org.springframework.data.neo4j.repository.GraphRepositoryFactoryBean">
<property name="repositoryInterface" value="org.springframework.data.neo4j.aspects.Account1Repository" />
<property name="neo4jTemplate" ref="template"/>
</bean>
<bean id="groupRepository" class="org.springframework.data.neo4j.repository.GraphRepositoryFactoryBean">
<property name="repositoryInterface" value="org.springframework.data.neo4j.aspects.GroupRepository" />
<property name="neo4jTemplate" ref="template"/>

View File

@@ -48,25 +48,25 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-aspects</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-tx</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<type>test-jar</type>
<scope>test</scope>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -92,7 +92,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>org.neo4j</groupId>
@@ -223,14 +223,14 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-aspects</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
@@ -238,7 +238,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-aspects</artifactId>
<version>3.1.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,9 +26,22 @@ import org.springframework.transaction.annotation.Transactional;
*/
public interface SchemaIndexRepository<T> {
/**
* 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<T> findAllBySchemaPropertyValue(String property, Object value);

View File

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

View File

@@ -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.<String,Object>emptyMap(), persistentEntity.getAllLabels());
return (indexInfo.isFailOnDuplicate())
? getGraphDatabase().createNode(map(uniqueProperty.getName(),value),persistentEntity.getAllLabels())
: getGraphDatabase().merge(indexInfo.getIndexName(),indexInfo.getIndexKey(),value, Collections.<String,Object>emptyMap(), persistentEntity.getAllLabels());
} else {
if (value instanceof Number && indexInfo.isNumeric()) value = ValueContext.numeric((Number) value);
return getGraphDatabase().getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.<String, Object>emptyMap(), persistentEntity.getAllLabels());

View File

@@ -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.<String,Object>emptyMap(), persistentEntity.getAllLabels());
return (indexInfo.isFailOnDuplicate())
? graphDatabase.createNode(map(uniqueProperty.getName(),value),persistentEntity.getAllLabels())
: graphDatabase.merge(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.<String,Object>emptyMap(), persistentEntity.getAllLabels());
} else {
return graphDatabase.getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.<String,Object>emptyMap(),persistentEntity.getAllLabels());
}

View File

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

View File

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

View File

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

View File

@@ -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<Account1> {
}

View File

@@ -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<Account2> {
}

View File

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

View File

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