DATAGRAPH-433 Handle Uniqueness for Label based Entities

* added merge to Neo4jTemplate, Database
* lazy determination of the index-label-name
* initialize storedEntityType early on, update with hierarchy information later
* label handling (correctly add additional labels to the node entity on create)
* uniqueness handling (choose merge in case of unique properties for creation)
* collecting of labels of a Neo4jPersistentEntity as getAllLabels()
This commit is contained in:
Michael Hunger
2014-03-10 23:27:20 +01:00
parent 36c951e55d
commit 2f724d607e
30 changed files with 290 additions and 313 deletions

View File

@@ -32,7 +32,7 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-neo4j</dist.id>
<springdata.commons>1.8.0.BUILD-SNAPSHOT</springdata.commons>
<springdata.commons>1.7.0.RELEASE</springdata.commons>
<!-- Neo4j 2.0 now requires JDK 7 as a min -->
<source.level>1.7</source.level>

View File

@@ -34,19 +34,27 @@ import org.springframework.data.neo4j.core.GraphDatabase;
import org.springframework.data.neo4j.support.index.NoSuchIndexException;
import org.springframework.data.neo4j.support.query.ConversionServiceQueryResultConverter;
import org.springframework.data.neo4j.support.query.QueryEngine;
import org.springframework.data.neo4j.support.schema.SchemaIndexProvider;
import javax.transaction.TransactionManager;
import java.util.Collection;
import java.util.Map;
import static org.neo4j.helpers.collection.MapUtil.map;
public class SpringRestGraphDatabase extends org.neo4j.rest.graphdb.RestGraphDatabase implements GraphDatabase {
static {
System.setProperty(Config.CONFIG_BATCH_TRANSACTION,"false");
}
private static final String[] NO_LABELS = new String[0];
private ConversionService conversionService;
private ResultConverter resultConverter;
private SchemaIndexProvider schemaIndexProvider;
public SpringRestGraphDatabase( RestAPI api){
super(api);
schemaIndexProvider = new SchemaIndexProvider(this);
}
public SpringRestGraphDatabase( String uri ) {
@@ -58,8 +66,18 @@ public class SpringRestGraphDatabase extends org.neo4j.rest.graphdb.RestGraphDat
}
@Override
public Node createNode(Map<String, Object> props) {
return super.getRestAPI().createNode(props);
public Node createNode(Map<String, Object> props, Collection<String> labels) {
RestAPI restAPI = super.getRestAPI();
RestNode node = restAPI.createNode(props);
if (labels!=null && !labels.isEmpty()) {
restAPI.addLabels(node, toLabels(labels));
}
return node;
}
private String[] toLabels(Collection<String> labels) {
if (labels==null || labels.isEmpty()) return NO_LABELS;
return labels.toArray(new String[labels.size()]);
}
@Override
@@ -80,6 +98,11 @@ public class SpringRestGraphDatabase extends org.neo4j.rest.graphdb.RestGraphDat
return getRestAPI().getOrCreateNode(nodeIndex, key, value, properties);
}
@Override
public Node merge(String labelName, String key, Object value, final Map<String, Object> nodeProperties, Collection<String> labels) {
return schemaIndexProvider.merge(labelName,key,value,nodeProperties, labels);
}
@Override
public Relationship getOrCreateRelationship(String indexName, String key, Object value, Node startNode, Node endNode, String type, Map<String, Object> properties) {

View File

@@ -242,7 +242,8 @@ public abstract class Neo4jConfiguration {
public EntityIndexCreator entityIndexCreator() throws Exception {
return new EntityIndexCreator(
indexProvider(),
schemaIndexProvider()
schemaIndexProvider(),
nodeTypeRepresentationStrategy().isLabelBased()
);
}

View File

@@ -41,8 +41,13 @@ public interface GraphDatabase {
/**
* creates the node and initializes its properties
*/
Node createNode(Map<String, Object> props);
Node createNode(Map<String, Object> props, Collection<String> labels);
/**
* creates the node uniquely or returns an existing node with the same label-key-value combination.
* properties are used to initialize the node. It needs a unique constraint to work correctly.
*/
Node merge(String labelName, String key, Object value, final Map<String, Object> properties, Collection<String> labels);
/**
* creates the node uniquely or returns an existing node with the same index-key-value combination.
* properties are used to initialize the node.

View File

@@ -39,10 +39,6 @@ public class NodeDelegatingFieldAccessorFactory extends DelegatingFieldAccessorF
template,
new PropertyFieldAccessorFactory(template),
new ConvertingNodePropertyFieldAccessorFactory(template)),
/*new SchemaIndexingPropertyFieldAccessorListenerFactory(
template,
new PropertyFieldAccessorFactory(template),
new ConvertingNodePropertyFieldAccessorFactory(template)), */
new ValidatingNodePropertyFieldAccessorListenerFactory(template)
);
}
@@ -53,7 +49,6 @@ public class NodeDelegatingFieldAccessorFactory extends DelegatingFieldAccessorF
new IdFieldAccessorFactory(template),
new TransientFieldAccessorFactory(),
//TODO Labels new LabelFieldAccessorFactory(template),
new SchemaIndexingFieldAccessorFactory(template),
new TraversalFieldAccessorFactory(template),
new QueryFieldAccessorFactory(template),
new PropertyFieldAccessorFactory(template),

View File

@@ -16,8 +16,10 @@
package org.springframework.data.neo4j.fieldaccess;
import org.neo4j.graphdb.ConstraintViolationException;
import org.neo4j.graphdb.PropertyContainer;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.Neo4jTemplate;
@@ -67,10 +69,14 @@ public class PropertyFieldAccessorFactory implements FieldAccessorFactory {
@Override
public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) {
final PropertyContainer propertyContainer = template.getPersistentState(entity);
if (newVal==null) {
propertyContainer.removeProperty(propertyName);
} else {
propertyContainer.setProperty(propertyName, newVal);
try {
if (newVal==null) {
propertyContainer.removeProperty(propertyName);
} else {
propertyContainer.setProperty(propertyName, newVal);
}
} catch(ConstraintViolationException cve) {
throw new DataIntegrityViolationException("Unique constraint violated "+property.getOwner().getName()+"."+property.getName()+" new value "+newVal,cve);
}
return newVal;
}

View File

@@ -1,111 +0,0 @@
/**
* 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.fieldaccess;
import org.neo4j.graphdb.DynamicLabel;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.neo4j.support.mapping.StoredEntityType;
import java.util.Set;
import java.util.TreeSet;
import static org.springframework.data.neo4j.support.DoReturn.doReturn;
/**
* @author Nicki Watt
* @since 01.03.2014
*/
public class SchemaIndexingFieldAccessorFactory implements FieldAccessorFactory {
private final Neo4jTemplate template;
public SchemaIndexingFieldAccessorFactory(Neo4jTemplate template) {
this.template = template;
}
@Override
public boolean accept(final Neo4jPersistentProperty property) {
return property.isIndexed() && property.getIndexInfo().isLabelBased();
}
@Override
public FieldAccessor forField(final Neo4jPersistentProperty property) {
return new SchemaIndexedFieldAccessor(template,property);
}
public static class SchemaIndexedFieldAccessor extends PropertyFieldAccessorFactory.PropertyFieldAccessor {
public SchemaIndexedFieldAccessor(Neo4jTemplate template,Neo4jPersistentProperty property) {
super(template,property);
}
@Override
public boolean isWriteable(Object entity) {
return super.isWriteable(entity);
}
@Override
public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) {
final PropertyContainer state = template.getPersistentState(entity);
if (!(state instanceof Node)) {
throw new IllegalArgumentException("not expecting to deal with non node property");
}
applyMissingSchemaIndexLabels(entity,(Node)state);
checkForUniqueViolation(entity, newVal, (Node)state);
return super.setValue(entity,newVal,mappingPolicy);
}
private void checkForUniqueViolation(Object entity,Object newVal, Node stateToBeSaved) {
StoredEntityType set = template.getStoredEntityType(entity);
if (newVal != null && property.isUnique()) {
Object existingUniqueEntity = template.findUniqueEntity(set.getEntity().getType(),property.getNeo4jPropertyName(),newVal);
if (existingUniqueEntity == null) return;
final Node existingUniqueState = (Node)template.getPersistentState(existingUniqueEntity);
if (existingUniqueState.equals(stateToBeSaved)) return;
throw new DataIntegrityViolationException("Unique property "+property+" was to be set to duplicate value "+newVal);
}
}
private void applyMissingSchemaIndexLabels(Object entity,Node state) {
// TODO - This logic should rather be done once when the
// entity is persisted for the first time rather than
// on each update ....
StoredEntityType set = template.getStoredEntityType(entity);
if (set != null) {
applyMissingSchemaIndexLabels(state, set);
}
}
private void applyMissingSchemaIndexLabels(Node node, StoredEntityType set) {
for (StoredEntityType ancestorSet : set.getSuperTypes()) {
applyMissingSchemaIndexLabels(node, ancestorSet);
}
Label label = DynamicLabel.label( (String)set.getAlias());
if (!node.hasLabel(label))
node.addLabel(label);
}
}
}

View File

@@ -1,122 +0,0 @@
/**
* Copyright 2014 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.fieldaccess;
import org.neo4j.graphdb.DynamicLabel;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.schema.IndexDefinition;
import org.neo4j.index.lucene.ValueContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.neo4j.support.mapping.StoredEntityType;
import java.util.Arrays;
public class SchemaIndexingPropertyFieldAccessorListenerFactory<S extends PropertyContainer, T> implements FieldAccessorListenerFactory {
private final PropertyFieldAccessorFactory propertyFieldAccessorFactory;
private final ConvertingNodePropertyFieldAccessorFactory convertingNodePropertyFieldAccessorFactory;
private final Neo4jTemplate template;
public SchemaIndexingPropertyFieldAccessorListenerFactory(final Neo4jTemplate template, final PropertyFieldAccessorFactory propertyFieldAccessorFactory, final ConvertingNodePropertyFieldAccessorFactory convertingNodePropertyFieldAccessorFactory) {
this.template = template;
this.propertyFieldAccessorFactory = propertyFieldAccessorFactory;
this.convertingNodePropertyFieldAccessorFactory = convertingNodePropertyFieldAccessorFactory;
}
@Override
public boolean accept(final Neo4jPersistentProperty property) {
return isPropertyField(property) && property.isIndexed() && property.getIndexInfo().isLabelBased();
}
private boolean isPropertyField(final Neo4jPersistentProperty property) {
return propertyFieldAccessorFactory.accept(property) || convertingNodePropertyFieldAccessorFactory.accept(property);
}
@Override
public FieldAccessListener forField(Neo4jPersistentProperty property) {
return new SchemaIndexingPropertyFieldAccessorListener(property, template);
}
/**
* @author Nicki Watt
* @since 09.02.2014
*/
public static class SchemaIndexingPropertyFieldAccessorListener<T extends PropertyContainer> implements FieldAccessListener {
private final static Logger log = LoggerFactory.getLogger(SchemaIndexingPropertyFieldAccessorListener.class);
private final Neo4jPersistentProperty property;
private final Neo4jTemplate template;
public SchemaIndexingPropertyFieldAccessorListener(final Neo4jPersistentProperty property, Neo4jTemplate template) {
this.property = property;
this.template = template;
}
@Override
public void valueChanged(Object entity, Object oldVal, Object newVal) {
final PropertyContainer state = template.getPersistentState(entity);
if (!(state instanceof Node)) {
throw new IllegalArgumentException("not expecting to deal with non node property");
}
applyMissingSchemaIndexLabels(entity,(Node)state);
checkForUniqueViolation(entity, newVal, (Node)state);
}
private void checkForUniqueViolation(Object entity,Object newVal, Node stateToBeSaved) {
StoredEntityType set = template.getStoredEntityType(entity);
if (newVal != null && property.isUnique()) {
Object existingUniqueEntity = template.findUniqueEntity(set.getEntity().getType(),property.getNeo4jPropertyName(),newVal);
if (existingUniqueEntity == null) return;
final Node existingUniqueState = (Node)template.getPersistentState(existingUniqueEntity);
if (existingUniqueState.equals(stateToBeSaved)) return;
throw new DataIntegrityViolationException("Unique property "+property+" was to be set to duplicate value "+newVal);
}
}
private void applyMissingSchemaIndexLabels(Object entity,Node state) {
// TODO - This logic should rather be done once when the
// entity is persisted for the first time rather than
// on each update ....
StoredEntityType set = template.getStoredEntityType(entity);
if (set != null) {
applyMissingSchemaIndexLabels(state, set);
}
}
private void applyMissingSchemaIndexLabels(Node node, StoredEntityType set) {
for (StoredEntityType ancestorSet : set.getSuperTypes()) {
applyMissingSchemaIndexLabels(node, ancestorSet);
}
Label label = DynamicLabel.label( (String)set.getAlias());
if (!node.hasLabel(label))
node.addLabel(label);
}
}
}

View File

@@ -32,23 +32,19 @@ public class IndexInfo {
private String indexKey;
private final boolean unique;
private boolean numeric;
private Indexed annotation;
private Neo4jPersistentProperty property;
public IndexInfo(Indexed annotation,
Neo4jPersistentProperty property) {
this.annotation = annotation;
this.property = property;
this.indexType = annotation.indexType();
this.indexName = isLabelBased() ? determineLabelIndexName(annotation, property) : determineIndexName(annotation, property);
fieldName = annotation.fieldName();
this.indexKey = fieldName.isEmpty() ? property.getNeo4jPropertyName() : fieldName;
unique = annotation.unique();
level = annotation.level();
numeric = annotation.numeric();
verify(property);
}
private void verify(Neo4jPersistentProperty property) {
// if (isLabelBased() && numeric) {
// throw new MappingException("No numeric indexing and range queries currently supported for label based indexes, property: " + property.getOwner().getName()+"."+property.getName());
// }
}
private String determineLabelIndexName(Indexed annotation, Neo4jPersistentProperty property) {
@@ -94,7 +90,11 @@ public class IndexInfo {
return indexType.isLabelBased();
}
// lazy because of deferred persistent entity hierarchy determination on registration
public String getIndexName() {
if (indexName == null) {
this.indexName = isLabelBased() ? determineLabelIndexName(annotation, property) : determineIndexName(annotation, property);
}
return indexName;
}

View File

@@ -20,6 +20,8 @@ import org.neo4j.graphdb.PropertyContainer;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.neo4j.support.mapping.StoredEntityType;
import java.util.Collection;
/**
* Interface for Neo4J specific {@link PersistentEntity}.
*
@@ -46,4 +48,6 @@ public interface Neo4jPersistentEntity<T> extends PersistentEntity<T, Neo4jPersi
Neo4jPersistentProperty getUniqueProperty();
boolean isUnique();
Collection<String> getAllLabels();
}

View File

@@ -274,7 +274,7 @@ public abstract class AbstractGraphRepository<S extends PropertyContainer, T> im
Neo4jPersistentEntity persistentEntity = template.getEntityType(clazz).getEntity();
Neo4jPersistentProperty persistentProperty = (Neo4jPersistentProperty)persistentEntity.getPersistentProperty(property);
if (persistentProperty.getIndexInfo() == null || !persistentProperty.getIndexInfo().isLabelBased() ) {
throw new IllegalArgumentException(format("property {} is not schema indexed",property));
throw new IllegalArgumentException(format("property %s.%s is not schema indexed",persistentEntity.getName(),property));
}
Map<String,Object> params = new HashMap<String,Object>();

View File

@@ -40,16 +40,16 @@ import org.springframework.data.neo4j.support.index.NoSuchIndexException;
import org.springframework.data.neo4j.support.query.ConversionServiceQueryResultConverter;
import org.springframework.data.neo4j.support.query.CypherQueryEngine;
import org.springframework.data.neo4j.support.query.QueryEngine;
import org.springframework.data.neo4j.support.schema.SchemaIndexProvider;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
import static org.neo4j.helpers.collection.MapUtil.map;
/**
* @author mh
@@ -58,12 +58,13 @@ import java.util.Set;
public class DelegatingGraphDatabase implements GraphDatabase {
private static final Logger log = LoggerFactory.getLogger(DelegatingGraphDatabase.class);
private static final Label[] NO_LABELS = new Label[0];
private final SchemaIndexProvider schemaIndexProvider;
protected GraphDatabaseService delegate;
private ConversionService conversionService;
private ResultConverter resultConverter;
private volatile QueryEngine<Object> cypherQueryEngine;
private long referenceNode = -2;
public DelegatingGraphDatabase(final GraphDatabaseService delegate) {
this(delegate,null);
@@ -71,6 +72,7 @@ public class DelegatingGraphDatabase implements GraphDatabase {
public DelegatingGraphDatabase(final GraphDatabaseService delegate, ResultConverter resultConverter) {
this.delegate = delegate;
this.resultConverter = resultConverter;
this.schemaIndexProvider = new SchemaIndexProvider(this);
}
public void setConversionService(ConversionService conversionService) {
@@ -102,8 +104,18 @@ public class DelegatingGraphDatabase implements GraphDatabase {
}
@Override
public Node createNode(Map<String, Object> props) {
return setProperties(delegate.createNode(), props);
public Node createNode(Map<String, Object> props, Collection<String> labels) {
return setProperties(delegate.createNode(toLabels(labels)), props);
}
private Label[] toLabels(Collection<String> labels) {
if (labels==null || labels.isEmpty()) return NO_LABELS;
Label[] labelArray = new Label[labels.size()];
int i=0;
for (String label : labels) {
labelArray[i++]= DynamicLabel.label(label);
}
return labelArray;
}
private <T extends PropertyContainer> T setProperties(T primitive, Map<String, Object> properties) {
@@ -309,6 +321,10 @@ public class DelegatingGraphDatabase implements GraphDatabase {
}
}
public Node merge(String labelName, String key, Object value, final Map<String, Object> nodeProperties, Collection<String> labels) {
return schemaIndexProvider.merge(labelName,key,value,nodeProperties,labels);
}
public Node getOrCreateNode(String indexName, String key, Object value, final Map<String,Object> nodeProperties) {
if (indexName ==null || key == null || value==null) throw new IllegalArgumentException("Unique index "+ indexName +" key "+key+" value must not be null");
if (value instanceof Number) value= ValueContext.numeric((Number)value);

View File

@@ -65,6 +65,7 @@ import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.validation.Validator;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
@@ -271,16 +272,23 @@ public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware {
*/
@Override
public Node createNode() {
return getGraphDatabase().createNode(null);
return createNode(null, null);
}
/**
* creates the node uniquely or returns an existing node with the same index-key-value combination.
* properties are used to initialize the node.
*/
@Override
public Node createNode(final Map<String, Object> properties) {
return getGraphDatabase().createNode(properties);
return createNode(properties, null);
}
/**
* properties are used to initialize the node.
*/
@Override
public Node createNode(final Map<String, Object> properties,Collection<String> labels) {
return getGraphDatabase().createNode(properties, labels);
}
/**
@@ -292,6 +300,15 @@ public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware {
return getGraphDatabase().getOrCreateNode(index, key, value, properties);
}
/**
* creates the node uniquely or returns an existing node with the same label-key-value combination.
* properties are used to initialize the node.
*/
@Override
public Node merge(String label, String key, Object value, final Map<String, Object> properties, Collection<String> labels) {
return getGraphDatabase().merge(label, key, value, properties, labels);
}
@Override
public <T> T createNodeAs(Class<T> target, Map<String, Object> properties) {
final Node node = createNode(properties);
@@ -718,8 +735,12 @@ public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware {
Object value = uniqueProperty.getValueFromEntity(entity, MappingPolicy.MAP_FIELD_DIRECT_POLICY);
if (value == null) return createNode();
final IndexInfo indexInfo = uniqueProperty.getIndexInfo();
if (value instanceof Number && indexInfo.isNumeric()) value = ValueContext.numeric((Number) value);
return getGraphDatabase().getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.<String, Object>emptyMap());
if (indexInfo.isLabelBased()) {
return 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());
}
}
@Override

View File

@@ -29,10 +29,15 @@ import org.springframework.data.neo4j.support.schema.SchemaIndexProvider;
public class EntityIndexCreator {
private IndexProvider indexProvider;
private SchemaIndexProvider schemaIndexProvider;
private boolean labelBased = true;
public EntityIndexCreator(IndexProvider indexProvider, SchemaIndexProvider schemaIndexProvider) {
this(indexProvider, schemaIndexProvider,true);
}
public EntityIndexCreator(IndexProvider indexProvider, SchemaIndexProvider schemaIndexProvider, boolean labelBased) {
this.indexProvider = indexProvider;
this.schemaIndexProvider = schemaIndexProvider;
this.labelBased = labelBased;
}
public void ensureEntityIndexes(Neo4jPersistentEntity<?> entity) {
@@ -48,7 +53,7 @@ public class EntityIndexCreator {
});
// Pass 2 - do everything else
indexProvider.getIndex(entity, null, IndexType.SIMPLE);
if (!labelBased) indexProvider.getIndex(entity, null, IndexType.SIMPLE);
entity.doWithProperties(new PropertyHandler<Neo4jPersistentProperty>() {
@Override
public void doWithPersistentProperty(Neo4jPersistentProperty property) {

View File

@@ -132,8 +132,8 @@ public class EntityStateHandler {
final MappingPolicy mappingPolicy = persistentEntity.getMappingPolicy();
// todo observe load policy
if (persistentEntity.isNodeEntity()) {
if (persistentEntity.isUnique()) return (S)createUniqueNode(persistentEntity.getUniqueProperty(),entity);
return (S) graphDatabase.createNode(null);
if (persistentEntity.isUnique()) return (S)createUniqueNode(persistentEntity,entity);
return createNode(persistentEntity);
}
if (persistentEntity.isRelationshipEntity()) {
return getOrCreateRelationship(entity, persistentEntity, annotationProvidedRelationshipType );
@@ -141,11 +141,20 @@ public class EntityStateHandler {
throw new IllegalArgumentException("The entity " + persistentEntity.getEntityName() + " has to be either annotated with @NodeEntity or @RelationshipEntity");
}
private Node createUniqueNode(Neo4jPersistentProperty uniqueProperty, Object entity) {
private <S extends PropertyContainer> S createNode(Neo4jPersistentEntityImpl<?> persistentEntity) {
return (S) graphDatabase.createNode(null,persistentEntity.getAllLabels());
}
private Node createUniqueNode(Neo4jPersistentEntityImpl<?> persistentEntity, Object entity) {
Neo4jPersistentProperty uniqueProperty = persistentEntity.getUniqueProperty();
final IndexInfo indexInfo = uniqueProperty.getIndexInfo();
final Object value = uniqueProperty.getValueFromEntity(entity, MappingPolicy.MAP_FIELD_DIRECT_POLICY);
if (value==null) throw new MappingException("Error creating "+uniqueProperty.getOwner().getName()+" with "+entity+" unique property "+uniqueProperty.getName()+" has null value");
return graphDatabase.getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.<String,Object>emptyMap());
if (indexInfo.isLabelBased()) {
return 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());
}
}
@SuppressWarnings("unchecked")

View File

@@ -56,10 +56,10 @@ public class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersistentE
protected <T> Neo4jPersistentEntityImpl<?> createPersistentEntity(TypeInformation<T> typeInformation) {
final Class<T> type = typeInformation.getType();
if (type.isAnnotationPresent(NodeEntity.class)) {
return new Neo4jPersistentEntityImpl<T>(typeInformation);
return new Neo4jPersistentEntityImpl<T>(typeInformation,entityAlias);
}
if (type.isAnnotationPresent(RelationshipEntity.class)) {
return new Neo4jPersistentEntityImpl<T>(typeInformation);
return new Neo4jPersistentEntityImpl<T>(typeInformation,entityAlias);
}
throw new InvalidEntityTypeException("Type " + type + " is neither a @NodeEntity nor a @RelationshipEntity");
}
@@ -73,16 +73,21 @@ public class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersistentE
}
private void updateStoredEntityType(Neo4jPersistentEntityImpl<?> entity, Collection<Neo4jPersistentEntity<?>> superTypeEntities) {
entity.updateStoredType(new StoredEntityType(entity, superTypeEntities, entityAlias));
entity.updateStoredType(superTypeEntities);
if (entityIndexCreator!=null) entityIndexCreator.ensureEntityIndexes(entity);
}
private List<Neo4jPersistentEntity<?>> addSuperTypes(Neo4jPersistentEntity<?> entity) {
List<Neo4jPersistentEntity<?>> entities=new ArrayList<Neo4jPersistentEntity<?>>();
final Class<?> type = entity.getType();
entities.addAll(addPersistentEntityWithCheck(type.getSuperclass()));
for (Class<?> anInterface : type.getInterfaces()) {
entities.addAll(addPersistentEntityWithCheck(anInterface));
List<Neo4jPersistentEntity<?>> entities=new ArrayList<>();
Class<?> type = entity.getType();
Collection<Class> typesToAdd = new LinkedHashSet<>();
while (type != null) {
typesToAdd.add(type.getSuperclass());
typesToAdd.addAll(Arrays.asList(type.getInterfaces()));
type = type.getSuperclass();
}
for (Class<?> superType : typesToAdd) {
entities.addAll(addPersistentEntityWithCheck(superType));
}
return entities;
}

View File

@@ -17,14 +17,15 @@
package org.springframework.data.neo4j.support.mapping;
import java.lang.annotation.Annotation;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.*;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.neo4j.annotation.NodeEntity;
@@ -47,23 +48,29 @@ public class Neo4jPersistentEntityImpl<T> extends BasicPersistentEntity<T, Neo4j
private StoredEntityType storedType;
private Neo4jPersistentProperty uniqueProperty;
private final boolean shouldUseShortNames;
private final EntityAlias entityAlias;
private Set<String> labels;
/**
* Creates a new {@link Neo4jPersistentEntityImpl} instance.
*
*
* @param information must not be {@literal null}.
* @param entityAlias
*/
public Neo4jPersistentEntityImpl(TypeInformation<T> information) {
public Neo4jPersistentEntityImpl(TypeInformation<T> information, EntityAlias entityAlias) {
super(information);
this.entityAlias = entityAlias;
for (Annotation annotation : information.getType().getAnnotations()) {
annotations.put(annotation.annotationType(),annotation);
}
managed = ManagedEntity.class.isAssignableFrom(information.getType());
shouldUseShortNames = shouldUseShortNames();
updateStoredType(null);
}
void updateStoredType(StoredEntityType storedType) {
this.storedType = storedType;
void updateStoredType(Collection<Neo4jPersistentEntity<?>> superTypeEntities) {
this.storedType = new StoredEntityType(this,superTypeEntities,entityAlias);
this.labels = computeLabels();
}
@Override
@@ -234,4 +241,36 @@ public class Neo4jPersistentEntityImpl<T> extends BasicPersistentEntity<T, Neo4j
public Neo4jPersistentProperty getUniqueProperty() {
return uniqueProperty;
}
@Override
public Collection<String> getAllLabels() {
return labels;
}
private Set<String> computeLabels() {
String alias = storedType.getAlias().toString();
final Set<String> labels = collectSuperTypeLabels(storedType, new LinkedHashSet<String>());
labels.add(alias);
doWithProperties(new PropertyHandler<Neo4jPersistentProperty>() {
@Override
public void doWithPersistentProperty(Neo4jPersistentProperty persistentProperty) {
if (persistentProperty.isIndexed()) {
IndexInfo indexInfo = persistentProperty.getIndexInfo();
if (indexInfo.isLabelBased()) {
labels.add(indexInfo.getIndexName());
}
}
}
});
return labels;
}
private Set<String> collectSuperTypeLabels(StoredEntityType type, Set<String> labels) {
if (type==null) return labels;
for (StoredEntityType superType : type.getSuperTypes()) {
labels.add(superType.getAlias().toString());
collectSuperTypeLabels(superType, labels);
}
return labels;
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.data.util.TypeInformation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/**
* @author mh
@@ -59,7 +60,8 @@ public class StoredEntityType {
}
private Collection<StoredEntityType> collectSuperTypes(Collection<Neo4jPersistentEntity<?>> superTypeEntities) {
Collection<StoredEntityType> result=new ArrayList<StoredEntityType>(superTypeEntities.size());
if (superTypeEntities==null) return Collections.emptyList();
Collection<StoredEntityType> result=new ArrayList<>(superTypeEntities.size());
for (Neo4jPersistentEntity<?> superTypeEntity : superTypeEntities) {
result.add(superTypeEntity.getEntityType());
}

View File

@@ -1,6 +1,7 @@
package org.springframework.data.neo4j.support.schema;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.Node;
import org.neo4j.helpers.collection.MapUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.neo4j.annotation.QueryType;
@@ -11,6 +12,9 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.query.QueryEngine;
import java.util.Collection;
import java.util.Map;
import static org.neo4j.helpers.collection.MapUtil.map;
/**
@@ -31,7 +35,12 @@ public class SchemaIndexProvider {
public void createIndex(Neo4jPersistentProperty property) {
String label = getLabel(property);
String prop = getName(property);
String query = indexQuery(label, prop, property.getIndexInfo().isUnique());
boolean unique = property.getIndexInfo().isUnique();
createIndex(label, prop, unique);
}
public void createIndex(String label, String prop, boolean unique) {
String query = createIndexQuery(label, prop, unique);
if (logger.isDebugEnabled()) logger.debug(query);
cypher.query(query, null);
}
@@ -62,11 +71,30 @@ public class SchemaIndexProvider {
return "MATCH (n:`"+label+"`) RETURN n";
}
public Node merge(String labelName, String key, Object value, final Map<String, Object> nodeProperties, Collection<String> labels) {
if (labelName ==null || key == null || value==null) throw new IllegalArgumentException("Label "+ labelName +" key "+key+" and value must not be null");
Map props = nodeProperties.containsKey(key) ? nodeProperties : MapUtil.copyAndPut(nodeProperties, key, value);
Map<String, Object> params = map("props", props, "value", value);
return cypher.query(mergeQuery(labelName, key,labels), params).to(Node.class).single();
}
private String mergeQuery(String labelName, String key, Collection<String> labels) {
StringBuilder setLabels = new StringBuilder();
if (labels!=null) {
for (String label : labels) {
if (label.equals(labelName)) continue;
setLabels.append("SET n:").append(label).append(" ");
}
}
return "MERGE (n:`"+labelName+"` {`"+key+"`: {value}}) ON CREATE SET n={props} "+setLabels+" return n";
}
private String findByLabelAndPropertyQuery(String label, String prop) {
return "MATCH (n:`"+label+"` {`"+prop+"`:{value}}) RETURN n";
}
private String indexQuery(String label, String prop, boolean unique) {
private String createIndexQuery(String label, String prop, boolean unique) {
if (unique) {
return "CREATE CONSTRAINT ON (n:`"+ label +"`) ASSERT n.`"+ prop +"` IS UNIQUE";
}

View File

@@ -30,6 +30,7 @@ import org.springframework.data.neo4j.support.mapping.WrappedIterableClosableIte
import org.springframework.data.neo4j.support.query.QueryEngine;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
/**
@@ -76,7 +77,7 @@ public class LabelBasedNodeTypeRepresentationStrategy implements NodeTypeReprese
*/
private void addLabelsForEntityHierarchy(Node state, StoredEntityType type) {
String alias = type.getAlias().toString();
Set<String> labels = collectSuperTypeLabels(type, new HashSet<String>());
Set<String> labels = collectSuperTypeLabels(type, new LinkedHashSet<String>());
labels.add(alias);
labels.add(LABELSTRATEGY_PREFIX + alias);
cypherHelper.setLabelsOnNode(state.getId(), labels);

View File

@@ -248,7 +248,7 @@ public class SubReferenceNodeTypeRepresentationStrategy implements NodeTypeRepre
return singleRelationship.getOtherNode(fromNode);
}
Node otherNode = graphDatabase.createNode(null);
Node otherNode = graphDatabase.createNode(null,null);
if (direction == Direction.OUTGOING)
fromNode.createRelationshipTo(otherNode, type);

View File

@@ -32,6 +32,7 @@ import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.data.neo4j.support.query.QueryEngine;
import java.util.Collection;
import java.util.Map;
/**
@@ -70,6 +71,11 @@ public interface Neo4jOperations {
*/
Node createNode(Map<String, Object> properties);
/**
* Creates a node with the given properties and labels
*/
Node createNode(final Map<String, Object> properties,Collection<String> labels);
Node createNode();
/**
@@ -78,6 +84,12 @@ public interface Neo4jOperations {
*/
Node getOrCreateNode(String index, String key, Object value, Map<String, Object> properties);
/**
* creates the node uniquely or returns an existing node with the same label-key-value combination.
* properties are used to initialize the node.
*/
Node merge(String label, String key, Object value, Map<String, Object> properties, Collection<String> labels);
/**
* Creates a node mapped by the given entity class
* @param target mapped entity class or Node.class

View File

@@ -134,7 +134,7 @@ public class Neo4jPersistentTestBase {
factoryBean.setNodeEntityStateFactory(nodeEntityStateFactory);
factoryBean.setRelationshipEntityStateFactory(relationshipEntityStateFactory);
mappingContext.setEntityIndexCreator(new EntityIndexCreator(new IndexProviderImpl(graphDatabase), new SchemaIndexProvider(graphDatabase)));
mappingContext.setEntityIndexCreator(new EntityIndexCreator(new IndexProviderImpl(graphDatabase), new SchemaIndexProvider(graphDatabase),true));
mappingContext.setSimpleTypeHolder(null);
setBasePackage(mappingContext);

View File

@@ -18,13 +18,14 @@ package org.springframework.data.neo4j.support;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.*;
import org.neo4j.helpers.collection.IteratorUtil;
import org.neo4j.test.TestGraphDatabaseFactory;
import org.springframework.data.neo4j.support.schema.SchemaIndexProvider;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.neo4j.helpers.collection.IteratorUtil.singleOrNull;
import static org.neo4j.helpers.collection.MapUtil.map;
/**
@@ -59,11 +60,45 @@ public class DelegatingGraphDatabaseTests {
}
}
@Test
public void mergeNode() throws Exception {
try (Transaction tx = graphDatabase.beginTx()) {
new SchemaIndexProvider(graphDatabase).createIndex("user","name",true);
tx.success();
}
try (Transaction tx = graphDatabase.beginTx()) {
final Node node = graphDatabase.merge("user", "name", "David", map("name", "David"), null);
final Node node2 = graphDatabase.merge("user", "name", "David", map("name", "David"), null);
assertEquals("David",node.getProperty("name"));
assertEquals(node,node2);
assertEquals(node,singleOrNull(gdb.findNodesByLabelAndProperty(DynamicLabel.label("user"), "name", "David")));
tx.success();
}
}
@Test
public void mergeNodeWithLabel() throws Exception {
try (Transaction tx = graphDatabase.beginTx()) {
new SchemaIndexProvider(graphDatabase).createIndex("user","name",true);
tx.success();
}
try (Transaction tx = graphDatabase.beginTx()) {
final Node node = graphDatabase.merge("user", "name", "David", map("name", "David"), asList("person"));
assertEquals("David",node.getProperty("name"));
assertEquals(2, IteratorUtil.count(node.getLabels()));
for (Label label : node.getLabels()) {
assertEquals(true, asList("user", "person").contains(label.name()));
}
assertEquals(node,singleOrNull(gdb.findNodesByLabelAndProperty(DynamicLabel.label("user"), "name", "David")));
tx.success();
}
}
@Test
public void testGetOrCreateRelationship() throws Exception {
try (Transaction tx = graphDatabase.beginTx()) {
final Node david = graphDatabase.createNode(map("name", "David"));
final Node michael = graphDatabase.createNode(map("name", "Michael"));
final Node david = graphDatabase.createNode(map("name", "David"), asList("Person"));
final Node michael = graphDatabase.createNode(map("name", "Michael"), asList("Person"));
final Relationship rel1 = graphDatabase.getOrCreateRelationship("knows", "whom", "david_michael", david, michael, "KNOWS", map("whom", "david_michael"));
final Relationship rel2 = graphDatabase.getOrCreateRelationship("knows", "whom", "david_michael", david, michael, "KNOWS", map("whom", "david_michael"));
assertEquals("david_michael",rel1.getProperty("whom"));

View File

@@ -47,6 +47,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.util.Iterator;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.*;
@@ -96,9 +97,9 @@ public class FullNeo4jTemplateTests {
new TransactionTemplate(neo4jTransactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
node0 = graphDatabase.createNode(map("name", "node0"));
node0 = graphDatabase.createNode(map("name", "node0"), asList("Node"));
graphDatabase.createIndex(Node.class, "node", IndexType.SIMPLE).add(node0, "name", "node0");
node1 = graphDatabase.createNode(map("name", "node1"));
node1 = graphDatabase.createNode(map("name", "node1"), asList("Node"));
relationship1 = node0.createRelationshipTo(node1, KNOWS);
relationship1.setProperty("name", "rel1");
graphDatabase.createIndex(Relationship.class, "relationship", IndexType.SIMPLE).add(relationship1, "name", "rel1");
@@ -185,7 +186,7 @@ public class FullNeo4jTemplateTests {
template.exec(new GraphCallback.WithoutResult() {
@Override
public void doWithGraphWithoutResult(GraphDatabase graph) throws Exception {
graph.createNode(null);
graph.createNode(null, labels);
}
});
}

View File

@@ -42,6 +42,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.io.IOException;
import java.util.Iterator;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.*;
@@ -89,9 +90,9 @@ public class Neo4jTemplateApiTests {
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
node0 = graphDatabase.createNode(map("name", "node0"));
node0 = graphDatabase.createNode(map("name", "node0"), asList("Node"));
graphDatabase.createIndex(Node.class, "node", IndexType.SIMPLE).add(node0, "name", "node0");
node1 = graphDatabase.createNode(map("name", "node1"));
node1 = graphDatabase.createNode(map("name", "node1"), asList("Node"));
relationship1 = node0.createRelationshipTo(node1, KNOWS);
relationship1.setProperty("name", "rel1");
graphDatabase.createIndex(Relationship.class, "relationship", IndexType.SIMPLE).add(relationship1, "name", "rel1");

View File

@@ -35,6 +35,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.io.IOException;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.*;
@@ -108,9 +109,9 @@ public class Neo4jTemplateApiTransactionTests {
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
node0 = graphDatabase.createNode(map("name", "node0"));
node0 = graphDatabase.createNode(map("name", "node0"), asList("Node"));
graphDatabase.createIndex(Node.class, "node", IndexType.SIMPLE).add(node0, "name", "node0");
node1 = graphDatabase.createNode(map("name", "node1"));
node1 = graphDatabase.createNode(map("name", "node1"), asList("Node"));
relationship1 = node0.createRelationshipTo(node1, KNOWS);
relationship1.setProperty("name", "rel1");
graphDatabase.createIndex(Relationship.class, "relationship", IndexType.SIMPLE).add(relationship1, "name", "rel1");
@@ -197,7 +198,7 @@ public class Neo4jTemplateApiTransactionTests {
template.exec(new GraphCallback.WithoutResult() {
@Override
public void doWithGraphWithoutResult(GraphDatabase graph) throws Exception {
graph.createNode(null);
graph.createNode(null, labels);
}
});
}

View File

@@ -56,7 +56,7 @@ public class Neo4jTemplateTests extends NeoApiTests {
template.exec(new GraphCallback.WithoutResult() {
@Override
public void doWithGraphWithoutResult(GraphDatabase graph) throws Exception {
Node node = graph.createNode(map("name", "Test", "size", 100));
Node node = graph.createNode(map("name", "Test", "size", 100), null);
refNode.createRelationshipTo(node, HAS);
final Relationship toTestNode = refNode.getSingleRelationship(HAS, Direction.OUTGOING);
@@ -102,7 +102,7 @@ public class Neo4jTemplateTests extends NeoApiTests {
super.setUp();
refNode = new Neo4jTemplate(graph, transactionManager).exec(new GraphCallback<Node>() {
public Node doWithGraph(GraphDatabase graph) throws Exception {
return graph.createNode(map());
return graph.createNode(map(), null);
}
});

View File

@@ -70,8 +70,8 @@ public class UniqueLegacyIndexBasedEntityTests extends CommonUniqueEntityTestBas
@Override
@Test(expected = DataIntegrityViolationException.class)
@Ignore("This method now throws a DataIntegrityViolationException for legacy indexes" +
" - verify if this is correct")
// @Ignore("This method now throws a DataIntegrityViolationException for legacy indexes" +
// " - verify if this is correct")
public void shouldOnlyCreateSingleInstanceForUniqueNumericNodeEntity() {
CommonUniqueNumericIdClub club1 = createUniqueNumericClub(100L);
CommonUniqueNumericIdClub club2 = createUniqueNumericClub(100L);

View File

@@ -70,7 +70,7 @@ public class UniqueSchemaBasedEntityTests extends CommonUniqueEntityTestBase {
}
@Override
@Ignore("This scenario does not currently work")
// @Ignore("This scenario does not currently work")
@Test
public void updatingToANewValueShouldKeepTheEntityUniqueAndOldValueShouldBeReusableThereafter() {
super.updatingToANewValueShouldKeepTheEntityUniqueAndOldValueShouldBeReusableThereafter();