DATAGRAPH-113 - Implemented derived repository queries for Cypher.

Added query method derivation into Cypher queries according to the wiki. Implemented query execution of derived methods.
This commit is contained in:
Oliver Gierke
2011-10-09 15:01:26 +02:00
parent e487521131
commit f7619671c9
25 changed files with 1487 additions and 228 deletions

View File

@@ -52,7 +52,7 @@ class Neo4jPersistentPropertyImpl extends AbstractPersistentProperty<Neo4jPersis
super(field, propertyDescriptor, owner, simpleTypeHolder);
this.annotations = extractAnnotations(field);
this.relationshipInfo = extractRelationshipInfo(field);
this.indexInfo = extractIndexInfo(field);
this.indexInfo = extractIndexInfo();
this.isIdProperty = annotations.containsKey(GraphId.class);
}
@@ -64,9 +64,9 @@ class Neo4jPersistentPropertyImpl extends AbstractPersistentProperty<Neo4jPersis
return result;
}
private IndexInfo extractIndexInfo(Field field) {
private IndexInfo extractIndexInfo() {
final Indexed annotation = getAnnotation(Indexed.class);
return annotation!=null ? new IndexInfo(annotation) : null;
return annotation!=null ? new IndexInfo(annotation,this) : null;
}
@SuppressWarnings("unchecked")
@@ -196,13 +196,21 @@ class Neo4jPersistentPropertyImpl extends AbstractPersistentProperty<Neo4jPersis
private final String fieldName;
private final Indexed.Level level;
public IndexInfo(Indexed annotation) {
this.indexName = annotation.indexName();
public IndexInfo(Indexed annotation, Neo4jPersistentPropertyImpl property) {
this.indexName = determineIndexName(annotation,property);
this.fulltext = annotation.fulltext();
fieldName = annotation.fieldName();
level = annotation.level();
}
private String determineIndexName(Indexed annotation, Neo4jPersistentPropertyImpl property) {
final String providedIndexName = annotation.indexName().isEmpty() ? null : annotation.indexName();
final Class<?> declaringClass = property.getField().getDeclaringClass();
final Class<?> instanceType = property.getOwner().getType();
return Indexed.Name.get(annotation.level(), declaringClass, providedIndexName, instanceType);
}
public String getIndexName() {
return indexName;
}

View File

@@ -16,6 +16,13 @@
package org.springframework.data.neo4j.repository;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
@@ -23,12 +30,11 @@ import org.neo4j.helpers.collection.IteratorUtil;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.annotation.QueryType;
import org.springframework.data.neo4j.annotation.RelationshipEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.repository.query.DerivedCypherRepositoryQuery;
import org.springframework.data.neo4j.support.GenericTypeExtractor;
import org.springframework.data.neo4j.support.GraphDatabaseContext;
import org.springframework.data.neo4j.support.conversion.EntityResultConverter;
@@ -38,32 +44,36 @@ import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.*;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.springframework.data.neo4j.annotation.QueryType.Cypher;
import static org.springframework.data.neo4j.annotation.QueryType.Gremlin;
/**
* @author mh
* @since 28.03.11
*/
public class GraphRepositoryFactory extends RepositoryFactorySupport {
private final GraphDatabaseContext graphDatabaseContext;
private final MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> mappingContext;
/**
* Creates a new {@link GraphRepositoryFactory} from the given {@link GraphDatabaseContext} and
* {@link MappingContext}.
*
* @param graphDatabaseContext must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
*/
public GraphRepositoryFactory(GraphDatabaseContext graphDatabaseContext, MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> mappingContext) {
public GraphRepositoryFactory(GraphDatabaseContext graphDatabaseContext) {
Assert.notNull(graphDatabaseContext);
Assert.notNull(mappingContext);
this.graphDatabaseContext = graphDatabaseContext;
this.mappingContext = mappingContext;
}
@@ -82,7 +92,6 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Object getTargetRepository(RepositoryMetadata metadata, GraphDatabaseContext graphDatabaseContext) {
Class<?> repositoryInterface = metadata.getRepositoryInterface();
Class<?> type = metadata.getDomainClass();
GraphEntityInformation entityInformation = (GraphEntityInformation)getEntityInformation(type);
// todo entityInformation.isGraphBacked();
@@ -96,14 +105,20 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata repositoryMetadata) {
Class<?> domainClass = repositoryMetadata.getDomainClass();
@SuppressWarnings("rawtypes")
final GraphEntityInformation entityInformation = (GraphEntityInformation) getEntityInformation(domainClass);
if (entityInformation.isNodeEntity()) return NodeGraphRepository.class;
if (entityInformation.isRelationshipEntity()) return RelationshipGraphRepository.class;
if (entityInformation.isNodeEntity()) {
return NodeGraphRepository.class;
}
if (entityInformation.isRelationshipEntity()) {
return RelationshipGraphRepository.class;
}
throw new IllegalArgumentException("Invalid Domain Class "+ domainClass+" neither Node- nor RelationshipEntity");
}
@Override
@SuppressWarnings({"unchecked"})
@SuppressWarnings({"unchecked", "rawtypes"})
public <T, ID extends Serializable> EntityInformation<T, ID> getEntityInformation(Class<T> type) {
return new GraphMetamodelEntityInformation(type,graphDatabaseContext);
}
@@ -116,12 +131,17 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, NamedQueries namedQueries) {
final GraphQueryMethod queryMethod = new GraphQueryMethod(method, repositoryMetadata,namedQueries);
if (!queryMethod.hasAnnotation() && !namedQueries.hasQuery(queryMethod.getNamedQueryName())) {
return new DerivedCypherRepositoryQuery(mappingContext, queryMethod, graphDatabaseContext);
}
return queryMethod.createQuery(repositoryMetadata, GraphRepositoryFactory.this.graphDatabaseContext);
}
};
}
static class GraphQueryMethod extends QueryMethod {
public static class GraphQueryMethod extends QueryMethod {
private final Method method;
private final Query queryAnnotation;
@@ -132,7 +152,6 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
this.method = method;
queryAnnotation = method.getAnnotation(Query.class);
this.query = queryAnnotation != null ? queryAnnotation.value() : getNamedQuery(namedQueries);
if (this.query==null) throw new IllegalArgumentException("Could not extract a query from "+method);
}
public boolean isValid() {
@@ -179,19 +198,28 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
private Pageable getPageable(Object[] args) {
Parameters parameters = getParameters();
if (parameters.hasPageableParameter()) return (Pageable) args[parameters.getPageableIndex()];
if (parameters.hasPageableParameter()) {
return (Pageable) args[parameters.getPageableIndex()];
}
return null;
}
private String addPaging(String baseQuery, Pageable pageable) {
if (pageable==null) return baseQuery;
if (pageable==null) {
return baseQuery;
}
return baseQuery + " skip "+pageable.getOffset() + " limit " + pageable.getPageSize();
}
private String addSorting(String baseQuery, Sort sort) {
if (sort==null) return baseQuery; // || sort.isEmpty()
if (sort==null)
{
return baseQuery; // || sort.isEmpty()
}
final String sortOrder = getSortOrder(sort);
if (sortOrder.isEmpty()) return baseQuery;
if (sortOrder.isEmpty()) {
return baseQuery;
}
return baseQuery + " order by " + sortOrder;
}
@@ -217,7 +245,9 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
private Class<?> getCompoundType() {
final Class<?> elementClass = getElementClass();
if (elementClass!=null) return elementClass;
if (elementClass!=null) {
return elementClass;
}
return GenericTypeExtractor.resolveReturnedType(method);
}
@@ -241,17 +271,19 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
}
private RepositoryQuery createQuery(RepositoryMetadata repositoryMetadata, final GraphDatabaseContext context) {
if (!isValid()) return null;
if (!isValid()) {
return null;
}
if (queryAnnotation == null) {
return new CypherGraphRepositoryQuery(this, repositoryMetadata, context); // cypher is default for named queries
}
switch (queryAnnotation.type()) {
case Cypher:
return new CypherGraphRepositoryQuery(this, repositoryMetadata, context);
case Gremlin:
return new GremlinGraphRepositoryQuery(this, repositoryMetadata, context);
default:
throw new IllegalStateException("@Query Annotation has to be configured as Cypher or Gremlin Query");
case Cypher:
return new CypherGraphRepositoryQuery(this, repositoryMetadata, context);
case Gremlin:
return new GremlinGraphRepositoryQuery(this, repositoryMetadata, context);
default:
throw new IllegalStateException("@Query Annotation has to be configured as Cypher or Gremlin Query");
}
}
}
@@ -266,6 +298,7 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
queryExecutor = new CypherQueryExecutor(graphDatabaseContext);
}
@Override
protected Object dispatchQuery(String queryString, Map<String, Object> params, Pageable pageable) {
GraphQueryMethod queryMethod = getQueryMethod();
final Class<?> compoundType = queryMethod.getCompoundType();
@@ -273,7 +306,9 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
return queryPaged(queryString,params,pageable);
}
if (queryMethod.isIterableResult()) {
if (compoundType.isAssignableFrom(Map.class)) return queryExecutor.queryForList(queryString,params);
if (compoundType.isAssignableFrom(Map.class)) {
return queryExecutor.queryForList(queryString,params);
}
return queryExecutor.query(queryString, queryMethod.getCompoundType(),params);
}
return queryExecutor.queryForObject(queryString, queryMethod.getReturnType(),params);
@@ -290,9 +325,10 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
public GremlinGraphRepositoryQuery(GraphQueryMethod queryMethod, RepositoryMetadata metadata, final GraphDatabaseContext graphDatabaseContext) {
super(queryMethod, metadata, graphDatabaseContext);
queryExecutor = new GremlinQueryEngine(graphDatabaseContext.getGraphDatabaseService(), new EntityResultConverter(graphDatabaseContext));
queryExecutor = new GremlinQueryEngine(graphDatabaseContext.getGraphDatabaseService(), new EntityResultConverter<Object, Object>(graphDatabaseContext));
}
@Override
protected Object dispatchQuery(String queryString, Map<String, Object> params, Pageable pageable) {
GraphQueryMethod queryMethod = getQueryMethod();
if (queryMethod.isPageQuery()) {
@@ -334,10 +370,13 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport {
return queryMethod;
}
@SuppressWarnings({"unchecked"})
@SuppressWarnings({"unchecked", "rawtypes"})
protected Object createPage(Iterable<?> result, Pageable pageable) {
final List resultList = IteratorUtil.addToCollection(result, new ArrayList());
if (pageable==null) return new PageImpl(resultList);
if (pageable==null) {
return new PageImpl(resultList);
}
final int currentTotal = pageable.getOffset() + pageable.getPageSize();
return new PageImpl(resultList, pageable, currentTotal);
}

View File

@@ -17,8 +17,10 @@
package org.springframework.data.neo4j.repository;
import org.neo4j.graphdb.PropertyContainer;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.GraphDatabaseContext;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport;
@@ -28,29 +30,44 @@ import org.springframework.util.Assert;
* @author mh
* @since 28.03.11
*/
public class GraphRepositoryFactoryBean<S extends PropertyContainer, R extends CRUDRepository<T>, T>
extends TransactionalRepositoryFactoryBeanSupport<R, T, Long> {
public class GraphRepositoryFactoryBean<S extends PropertyContainer, R extends CRUDRepository<T>, T> extends
TransactionalRepositoryFactoryBeanSupport<R, T, Long> {
private GraphDatabaseContext graphDatabaseContext;
private MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> mappingContext;
public void setGraphDatabaseContext(GraphDatabaseContext graphDatabaseContext) {
this.graphDatabaseContext = graphDatabaseContext;
}
/**
* @param mappingContext the mappingContext to set
*/
public void setMappingContext(
MappingContext<Neo4jPersistentEntity<?>, Neo4jPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
return createRepositoryFactory(graphDatabaseContext);
}
protected RepositoryFactorySupport createRepositoryFactory(GraphDatabaseContext graphDatabaseContext) {
return new GraphRepositoryFactory(graphDatabaseContext);
return new GraphRepositoryFactory(graphDatabaseContext, mappingContext);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(graphDatabaseContext, "GraphDatabaseContext must not be null!");
if (mappingContext == null) {
Neo4jMappingContext context = new Neo4jMappingContext();
context.afterPropertiesSet();
this.mappingContext = context;
}
super.afterPropertiesSet();
}
}

View File

@@ -1,66 +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.repository.query;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Value object to create {@link CypherQuery} instances.
*
* @author Oliver Gierke
*/
public class CypherQuery {
private final String TEMPLATE = "start %s match % where %";
private final Neo4jMappingContext context;
private final TypeInformation<?> rootType;
private final Set<MatchClause> matchClauses;
public CypherQuery(TypeInformation<?> rootType, Neo4jMappingContext context) {
Assert.notNull(rootType);
Assert.notNull(context);
this.rootType = rootType;
this.context = context;
this.matchClauses = new HashSet<MatchClause>();
}
public CypherQuery addRestriction(Part part) {
matchClauses.add(new MatchClause(context, part.getProperty()));
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format(TEMPLATE, "TODO", StringUtils.collectionToCommaDelimitedString(matchClauses), "TODO");
}
}

View File

@@ -0,0 +1,178 @@
/**
* 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.repository.query;
import static org.springframework.util.StringUtils.*;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.util.Assert;
/**
* Value object to create Cypher queries.
*
* @author Oliver Gierke
*/
class CypherQueryBuilder implements CypherQueryDefinition {
private final MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> context;
private final VariableContext variableContext = new VariableContext();
private final List<MatchClause> matchClauses = new ArrayList<MatchClause>();
private final List<StartClause> startClauses = new ArrayList<StartClause>();
private final List<WhereClause> whereClauses = new ArrayList<WhereClause>();
private int index = 0;
private final Neo4jPersistentEntity<?> entity;
/**
* Creates a new {@link CypherQueryBuilder}.
*
* @param context must not be {@literal null}.
* @param type must not be {@literal null}.
*/
public CypherQueryBuilder(MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> context, Class<?> type) {
Assert.notNull(context);
Assert.notNull(type);
this.context = context;
this.entity = context.getPersistentEntity(type);
}
private String defaultStartClause() {
return String.format(QueryTemplates.DEFAULT_START_CLAUSE, this.variableContext.getVariableFor(entity), entity
.getType().getName());
}
/**
* Adds the given {@link Part} to the restrictions for the query.
*
* @param part
* @return
*/
public CypherQueryBuilder addRestriction(Part part) {
PersistentPropertyPath<Neo4jPersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
String variable = variableContext.getVariableFor(path);
Neo4jPersistentProperty leafProperty = path.getLeafProperty();
if (!leafProperty.isRelationship()) {
if (leafProperty.isIndexed()) {
startClauses.add(new StartClause(path, variable, index++));
} else {
whereClauses.add(new WhereClause(path, variable, part.getType(), index++));
}
}
MatchClause matchClause = new MatchClause(path);
if (matchClause.hasRelationship()) {
matchClauses.add(matchClause);
}
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String startClauses = collectionToDelimitedString(this.startClauses, ", ");
String matchClauses = toString(this.matchClauses);
String whereClauses = collectionToDelimitedString(this.whereClauses, ", ");
StringBuilder builder = new StringBuilder("start ");
if (hasText(startClauses)) {
builder.append(startClauses);
} else {
builder.append(defaultStartClause());
}
if (hasText(matchClauses)) {
builder.append(" match ").append(matchClauses);
}
if (hasText(whereClauses)) {
builder.append(" where ").append(whereClauses);
}
builder.append(" return ").append(variableContext.getVariableFor(entity));
return builder.toString();
}
/* (non-Javadoc)
* @see org.springframework.data.neo4j.repository.query.CypherQueryDefinition#toString(org.springframework.data.domain.Pageable)
*/
@Override
public String toString(Pageable pageable) {
StringBuilder builder = new StringBuilder(toString(pageable.getSort()));
if (pageable != null) {
builder.append(String.format(QueryTemplates.SKIP_LIMIT, pageable.getOffset(), pageable.getPageSize()));
}
return builder.toString();
}
/* (non-Javadoc)
* @see org.springframework.data.neo4j.repository.query.CypherQueryDefinition#toString(org.springframework.data.domain.Sort)
*/
@Override
public String toString(Sort sort) {
StringBuilder builder = new StringBuilder(toString());
builder.append(addSorts(sort));
return builder.toString();
}
private String addSorts(Sort sort) {
final List<String> sorts = formatSorts(sort);
return !sorts.isEmpty() ? String
.format(QueryTemplates.ORDER_BY_CLAUSE, collectionToCommaDelimitedString(sorts)) : "";
}
private List<String> formatSorts(Sort sort) {
List<String> result = new ArrayList<String>();
if (sort == null) {
return result;
}
for (Sort.Order order : sort) {
result.add(String.format(QueryTemplates.SORT_CLAUSE, order.getProperty(), order.getDirection()));
}
return result;
}
private String toString(List<MatchClause> matchClauses) {
List<String> result = new ArrayList<String>(matchClauses.size());
for (MatchClause matchClause : matchClauses) {
result.add(matchClause.toString(variableContext));
}
return collectionToDelimitedString(result, ", ");
}
}

View File

@@ -0,0 +1,99 @@
/**
* 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.repository.query;
import java.util.Iterator;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;
/**
* {@link AbstractQueryCreator} implementation to build {@link CypherQueryDefinition}s.
*
* @author Oliver Gierke
*/
class CypherQueryCreator extends AbstractQueryCreator<CypherQueryDefinition, CypherQueryBuilder> {
private final MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> context;
private final Class<?> domainClass;
/**
* Creates a new {@link CypherQueryCreator} using the given {@link PartTree}, {@link Neo4jMappingContext} and domain
* class.
*
* @param tree must not be {@literal null}.
* @param context must not be {@literal null}.
* @param domainClass must not be {@literal null}.
*/
public CypherQueryCreator(PartTree tree, MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> context, Class<?> domainClass) {
super(tree);
Assert.notNull(context);
Assert.notNull(domainClass);
this.context = context;
this.domainClass = domainClass;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator)
*/
@Override
protected CypherQueryBuilder create(Part part, Iterator<Object> iterator) {
CypherQueryBuilder builder = new CypherQueryBuilder(context, domainClass);
builder.addRestriction(part);
return builder;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator)
*/
@Override
protected CypherQueryBuilder and(Part part, CypherQueryBuilder base, Iterator<Object> iterator) {
return base.addRestriction(part);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object)
*/
@Override
protected CypherQueryBuilder or(CypherQueryBuilder base, CypherQueryBuilder criteria) {
throw new UnsupportedOperationException("Or is not supported currently!");
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort)
*/
@Override
protected CypherQueryDefinition complete(CypherQueryBuilder criteria, Sort sort) {
return criteria;
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.repository.query;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* Interface to abstract Cypher query creation.
*
*
* @author Oliver Gierke
*/
interface CypherQueryDefinition {
/**
* Returns a Cypher query without adding any sort or pagination.
*
* @return
*/
String toString();
/**
* Returns a Cypher query adding the given {@link Sort}.
*
* @param sort
* @return
*/
String toString(Sort sort);
/**
* Returns a Cypher query restricting the result to the given {@link Pageable} and applying the {@link Sort}
* contained in it.
*
* @param pageable
* @return
*/
String toString(Pageable pageable);
}

View File

@@ -0,0 +1,122 @@
/**
* 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.repository.query;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.repository.GraphRepositoryFactory.GraphQueryMethod;
import org.springframework.data.neo4j.support.GraphDatabaseContext;
import org.springframework.data.neo4j.support.query.CypherQueryExecutor;
import org.springframework.data.repository.core.EntityMetadata;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;
/**
* {@link RepositoryQuery} implementation that derives a Cypher query from the {@link GraphQueryMethod}'s method name.
*
* @author Oliver Gierke
*/
public class DerivedCypherRepositoryQuery implements RepositoryQuery {
private final GraphQueryMethod method;
private final CypherQueryExecutor executor;
private final CypherQueryDefinition query;
/**
* Creates a new {@link DerivedCypherRepositoryQuery} from the given {@link MappingContext},
* {@link GraphQueryMethod} and {@link GraphDatabaseContext}.
*
* @param context must not be {@literal null}.
* @param method must not be {@literal null}.
* @param database must not be {@literal null}.
*/
public DerivedCypherRepositoryQuery(MappingContext<? extends Neo4jPersistentEntity<?>, Neo4jPersistentProperty> context, GraphQueryMethod method, GraphDatabaseContext database) {
Assert.notNull(context);
Assert.notNull(method);
Assert.notNull(database);
EntityMetadata<?> info = method.getEntityInformation();
PartTree tree = new PartTree(method.getName(), info.getJavaType());
this.query = new CypherQueryCreator(tree, context, info.getJavaType()).createQuery();
this.method = method;
this.executor = new CypherQueryExecutor(database);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
@Override
public Object execute(Object[] parameters) {
ParameterAccessor accessor = new ParametersParameterAccessor(method.getParameters(), parameters);
Map<String, Object> paramMap = new HashMap<String, Object>();
int counter = 0;
for (Object parameter : accessor) {
paramMap.put(String.format(QueryTemplates.PARAMETER, counter++), parameter);
}
Class<?> type = method.getEntityInformation().getJavaType();
String query = getQuery(this.query, accessor);
if (method.isCollectionQuery()) {
return executor.query(query, type, paramMap);
} else {
return executor.queryForObject(query, type, paramMap);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
*/
@Override
public QueryMethod getQueryMethod() {
return method;
}
/**
* Returns the actual Cypher query applying {@link Pageable} or {@link Sort} instances.
*
* @param query
* @param accessor
* @return
*/
private String getQuery(CypherQueryDefinition query, ParameterAccessor accessor) {
if (accessor.getPageable() != null) {
return query.toString(accessor.getPageable());
} else if (accessor.getSort() != null) {
return query.toString(accessor.getSort());
} else {
return query.toString();
}
}
}

View File

@@ -13,94 +13,63 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.query;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.mapping.RelationshipInfo;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Value object to build the {@code match} clause of a Cypher query.
* Value object to build the {@literal match} clause of a Cypher query.
*
* @author Oliver Gierke
*/
class MatchClause {
private final Iterable<Neo4jPersistentProperty> properties;
private final PersistentPropertyPath<Neo4jPersistentProperty> path;
/**
* Creates a new {@link MatchClause} using the given
* {@link org.springframework.data.neo4j.mapping.Neo4jMappingContext} and {@link PropertyPath}.
* Creates a new {@link MatchClause} using the given {@link PersistentPropertyPath}.
*
* @param context must not be {@literal null}.
* @param property must not be {@literal null}.
* @param path must not be {@literal null}.
*/
public MatchClause(Neo4jMappingContext context, PropertyPath property) {
Assert.notNull(context);
Assert.notNull(property);
this.properties = context.getPersistentPropertyPath(property);
public MatchClause(PersistentPropertyPath<Neo4jPersistentProperty> path) {
Assert.notNull(path);
this.path = relationshipPath(path);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
private PersistentPropertyPath<Neo4jPersistentProperty> relationshipPath(
PersistentPropertyPath<Neo4jPersistentProperty> path) {
return (path.getLength() == 1 || path.getLeafProperty().isRelationship()) ? path : relationshipPath(path
.getParentPath());
}
/**
* Returns whether the match clause actually deals with a relationship.
*/
@Override
public String toString() {
String intermediate = null;
for (Neo4jPersistentProperty property : properties) {
if (!property.isRelationship()) {
return intermediate;
public boolean hasRelationship() {
for (Neo4jPersistentProperty property : path) {
if (property.isRelationship()) {
return true;
}
RelationshipInfo info = property.getRelationshipInfo();
Class<?> ownerType = property.getOwner().getType();
intermediate = intermediate == null ? asVariableReference(StringUtils.uncapitalize(ownerType
.getSimpleName())) : intermediate;
intermediate = String.format(getPattern(info), intermediate, info.getType(),
asVariableReference(property.getName()));
}
return intermediate.toString();
return false;
}
/**
* Returns the given value as variable reference.
*
* @param value
* @return
*/
private static String asVariableReference(String value) {
return String.format("(%s)", value);
public String toString(VariableContext variableContext) {
return matchPattern(variableContext, path);
}
/**
* Returns the clause pattern for the given {@link RelationshipInfo}.
*
* @param info must not be {@literal null}.
* @return
*/
private static String getPattern(RelationshipInfo info) {
switch (info.getDirection()) {
case OUTGOING:
return "%s-[:%s]->%s";
case INCOMING:
return "%s<-[:%s]-%s";
case BOTH:
return "%s-[:%s]-%s";
default:
throw new IllegalArgumentException("Unsupported direction!");
private String matchPattern(VariableContext variableContext, PersistentPropertyPath<Neo4jPersistentProperty> relPath) {
if (relPath.getLength() == 1) {
final Neo4jPersistentProperty property = relPath.getBaseProperty();
return variableContext.getVariableFor(property.getOwner()) + QueryTemplates.getArrow(property.getRelationshipInfo())
+ variableContext.getVariableFor(relPath);
}
final RelationshipInfo info = relPath.getLeafProperty().getRelationshipInfo();
return matchPattern(variableContext, relPath.getParentPath()) + QueryTemplates.getArrow(info)
+ variableContext.getVariableFor(relPath);
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.repository.query;
import org.neo4j.graphdb.Direction;
import org.springframework.data.neo4j.mapping.RelationshipInfo;
/**
* String templates to build Cypher queries.
*
* @author Oliver Gierke
*/
abstract class QueryTemplates {
static final String PARAMETER = "_%d";
private static final String PLACEHOLDER = String.format("{%s}", PARAMETER);
private static final String DIRECTION_INCOMING = "<-[:%s]-";
private static final String DIRECTION_OUTGOING = "-[:%s]->";
private static final String DIRECTION_BOTH = "-[:%s]-";
static final String DEFAULT_START_CLAUSE = "%s=node:__types__(className=\"%s\")";
static final String SKIP_LIMIT = " skip %d limit %d";
static final String START_CLAUSE = "%s=node:%s(%s=" + PLACEHOLDER + ")";
static final String WHERE_CLAUSE = "%s.%s %s " + PLACEHOLDER;
static final String SORT_CLAUSE = "%s %s";
static final String ORDER_BY_CLAUSE = " order by %s";
static String getArrow(RelationshipInfo info) {
return String.format(getTemplate(info.getDirection()), info.getType());
}
private static String getTemplate(Direction direction) {
switch (direction) {
case OUTGOING:
return DIRECTION_OUTGOING;
case INCOMING:
return DIRECTION_INCOMING;
case BOTH:
return DIRECTION_BOTH;
default:
throw new IllegalArgumentException("Unsupported direction!");
}
}
private QueryTemplates() {
}
}

View File

@@ -13,9 +13,52 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.query;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.util.Assert;
/**
* Representation of a Cypher {@literal start} clause.
*
* @author Oliver Gierke
*/
class StartClause {
private final PersistentPropertyPath<Neo4jPersistentProperty> path;
private final String variable;
private final int index;
/**
* Creates a new {@link StartClause} from the given {@link Neo4jPersistentProperty}, variable and the given
* parameter index.
*
* @param property must not be {@literal null}.
* @param variable must not be {@literal null} or empty.
* @param index
*/
public StartClause(PersistentPropertyPath<Neo4jPersistentProperty> property, String variable, int index) {
Assert.notNull(property);
Assert.hasText(variable);
this.path = property;
this.variable = variable;
this.index = index;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
Neo4jPersistentProperty leafProperty = path.getLeafProperty();
String indexName = leafProperty.getIndexInfo().getIndexName();
String propertyName = leafProperty.getNeo4jPropertyName();
return String.format(QueryTemplates.START_CLAUSE, variable, indexName, propertyName, index);
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.repository.query;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.util.StringUtils;
/**
*
* @author Oliver Gierke
*/
public class VariableContext {
private Map<PersistentPropertyPath<Neo4jPersistentProperty>, String> variables;
public VariableContext() {
this.variables = new HashMap<PersistentPropertyPath<Neo4jPersistentProperty>, String>();
}
public String getVariableFor(PersistentPropertyPath<Neo4jPersistentProperty> path) {
if (variables.containsKey(path)) {
return variables.get(path);
}
Neo4jPersistentProperty baseProperty = path.getBaseProperty();
List<String> parts = new ArrayList<String>();
parts.add(getVariableFor(baseProperty.getOwner()));
final Neo4jPersistentProperty leaf = path.getLeafProperty();
for (Neo4jPersistentProperty property : path) {
if (leaf.isRelationship() || !leaf.equals(property)) {
parts.add(property.getName());
}
}
String variable = StringUtils.collectionToDelimitedString(parts, "_");
variables.put(path, variable);
return variable;
}
public String getVariableFor(Neo4jPersistentEntity<?> entity) {
return StringUtils.uncapitalize(entity.getType().getSimpleName());
}
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.query;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.util.Assert;
/**
* Representation of a Cypher {@literal where} clause.
*
* @author Oliver Gierke
*/
class WhereClause {
private static final Map<Type, String> SYMBOLS;
static {
Map<Type, String> symbols = new HashMap<Type, String>();
symbols.put(Type.GREATER_THAN, ">");
symbols.put(Type.GREATER_THAN_EQUAL, ">=");
symbols.put(Type.LESS_THAN, "<");
symbols.put(Type.LESS_THAN_EQUAL, "<=");
symbols.put(Type.NEGATING_SIMPLE_PROPERTY, "!=");
symbols.put(Type.SIMPLE_PROPERTY, "=");
SYMBOLS = Collections.unmodifiableMap(symbols);
}
private final PersistentPropertyPath<Neo4jPersistentProperty> path;
private final String variable;
private final Type type;
private final int index;
/**
* Creates a new {@link WhereClause} for the given {@link Neo4jPersistentProperty}, variable, type and parameter
* index.
*
* @param path must not be {@literal null}.
* @param variable must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @param index
*/
public WhereClause(PersistentPropertyPath<Neo4jPersistentProperty> path, String variable, Type type, int index) {
Assert.notNull(path);
Assert.hasText(variable);
Assert.notNull(type);
this.path = path;
this.variable = variable;
this.type = type;
this.index = index;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format(QueryTemplates.WHERE_CLAUSE, variable, path.getLeafProperty().getNeo4jPropertyName(),
SYMBOLS.get(type), index);
}
}

View File

@@ -16,27 +16,33 @@
package org.springframework.data.neo4j.model;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.DynamicRelationshipType;
import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.kernel.Traversal;
import org.neo4j.kernel.impl.traversal.TraversalDescriptionImpl;
import org.springframework.data.neo4j.annotation.*;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.GraphProperty;
import org.springframework.data.neo4j.annotation.GraphTraversal;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;
import org.springframework.data.neo4j.core.FieldTraversalDescriptionBuilder;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import java.util.Collection;
import java.util.Set;
import org.springframework.util.ObjectUtils;
@NodeEntity
public class Group {
public final static String OTHER_NAME_INDEX="other_name";
public final static String OTHER_NAME_INDEX = "other_name";
public static final String SEARCH_GROUPS_INDEX = "search-groups";
@RelatedTo(direction = Direction.OUTGOING)
@Fetch
private Collection<Person> persons;
private Collection<Person> persons = new HashSet<Person>();
@RelatedTo(type = "persons", elementClass = Person.class)
private Iterable<Person> readOnlyPersons;
@@ -64,13 +70,13 @@ public class Group {
@Indexed(fieldName = OTHER_NAME_INDEX)
private String otherName;
@Indexed(level=Indexed.Level.GLOBAL)
@Indexed(level = Indexed.Level.GLOBAL)
private String globalName;
@Indexed(level=Indexed.Level.CLASS)
@Indexed(level = Indexed.Level.CLASS)
private String classLevelName;
@Indexed(level=Indexed.Level.INSTANCE)
@Indexed(level = Indexed.Level.INSTANCE)
private String indexLevelName;
@GraphId
@@ -127,10 +133,9 @@ public class Group {
private static class PeopleTraversalBuilder implements FieldTraversalDescriptionBuilder {
@SuppressWarnings("deprecation")
@Override
public TraversalDescription build(Object start, Neo4jPersistentProperty property, String...params) {
return new TraversalDescriptionImpl()
.relationships(DynamicRelationshipType.withName(params[0]))
.filter(Traversal.returnAllButStartNode());
public TraversalDescription build(Object start, Neo4jPersistentProperty property, String... params) {
return new TraversalDescriptionImpl().relationships(DynamicRelationshipType.withName(params[0])).filter(
Traversal.returnAllButStartNode());
}
}
@@ -170,4 +175,30 @@ public class Group {
public void setAdmin(Boolean admin) {
this.admin = admin;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
Group that = (Group) obj;
return ObjectUtils.nullSafeEquals(this.id, that.id);
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.id);
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.data.neo4j.model;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -25,8 +27,6 @@ import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.data.neo4j.repository.NamedIndexRepository;
import org.springframework.data.repository.query.Param;
import java.util.Map;
/**
* @author mh
* @since 29.03.11

View File

@@ -0,0 +1,146 @@
/**
* 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.repository;
import static java.util.Arrays.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.internal.matchers.IsCollectionContaining.*;
import static org.neo4j.helpers.collection.IteratorUtil.*;
import java.util.HashSet;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.model.Group;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.neo4j.support.GraphDatabaseContext;
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;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners({CleanContextCacheTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class})
@Transactional
public class GraphRepositoryTest {
protected final Log log = LogFactory.getLog(getClass());
@Autowired
private GraphDatabaseContext graphDatabaseContext;
@Autowired
private PersonRepository personRepository;
@Autowired
GroupRepository groupRepository;
private TestTeam testTeam;
@BeforeTransaction
public void cleanDb() {
Neo4jHelper.cleanDb(graphDatabaseContext);
}
@Before
public void setUp() throws Exception {
testTeam = new TestTeam();
testTeam.createSDGTeam(personRepository, groupRepository);
}
@Test
public void testFindIterableOfPersonWithQueryAnnotation() {
Iterable<Person> teamMembers = personRepository.findAllTeamMembers(testTeam.sdg);
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david, testTeam.emil));
}
@Test
public void testFindIterableOfPersonWithQueryAnnotationAndGremlin() {
Iterable<Person> teamMembers = personRepository.findAllTeamMembersGremlin(testTeam.sdg);
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david, testTeam.emil));
}
@Test
public void testFindPersonWithQueryAnnotation() {
Person boss = personRepository.findBoss(testTeam.michael);
assertThat(boss, is(testTeam.emil));
}
@Test
public void testFindIterableMapsWithQueryAnnotation() {
Iterable<Map<String,Object>> teamMembers = personRepository.findAllTeamMemberData(testTeam.sdg);
assertThat(asCollection(teamMembers), hasItems(testTeam.simpleRowFor(testTeam.michael, "member"), testTeam.simpleRowFor(testTeam.david, "member"), testTeam.simpleRowFor(testTeam.emil, "member")));
}
@Test
public void testFindPaged() {
final PageRequest page = new PageRequest(0, 1, Sort.Direction.ASC, "member.name");
Page<Person> teamMemberPage1 = personRepository.findAllTeamMembersPaged(testTeam.sdg,page);
assertThat(teamMemberPage1, hasItem(testTeam.david));
}
@Test
public void testFindPagedDescending() {
final PageRequest page = new PageRequest(0, 2, Sort.Direction.DESC, "member.name");
Page<Person> teamMemberPage1 = personRepository.findAllTeamMembersPaged(testTeam.sdg,page);
assertEquals(asList(testTeam.michael, testTeam.emil), asCollection(teamMemberPage1));
assertThat(teamMemberPage1.isFirstPage(), is(true));
}
@Test
public void testFindPagedNull() {
Page<Person> teamMemberPage1 = personRepository.findAllTeamMembersPaged(testTeam.sdg,null);
assertEquals(new HashSet(asList(testTeam.david, testTeam.emil, testTeam.michael)), addToCollection(teamMemberPage1, new HashSet()));
assertThat(teamMemberPage1.isFirstPage(), is(true));
assertThat(teamMemberPage1.isLastPage(), is(false));
}
@Test
public void testFindSortedDescending() {
final Sort sort = new Sort(Sort.Direction.DESC, "member.name");
Iterable<Person> teamMembers = personRepository.findAllTeamMembersSorted(testTeam.sdg, sort);
assertEquals(asList(testTeam.michael, testTeam.emil, testTeam.david), asCollection(teamMembers));
}
@Test
public void testFindSortedNull() {
Iterable<Person> teamMembers = personRepository.findAllTeamMembersSorted(testTeam.sdg, null);
assertThat(teamMembers, hasItems(testTeam.michael, testTeam.emil, testTeam.david));
}
@Test
public void testFindByNamedQuery() {
Group team = personRepository.findTeam(testTeam.michael);
assertThat(team, is(testTeam.sdg));
}
@Test
public void findByName() {
Iterable<Person> findByName = personRepository.findByName(testTeam.michael.getName());
assertThat(findByName, hasItem(testTeam.michael));
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.repository;
import org.springframework.data.neo4j.model.Group;
/**
* @author mh
* @since 29.03.11
*/
public interface GroupRepository extends GraphRepository<Group>, NamedIndexRepository<Group> {
}

View File

@@ -0,0 +1,61 @@
/**
* 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.repository;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.annotation.QueryType;
import org.springframework.data.neo4j.model.Group;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.repository.query.Param;
/**
* Sample repository interface to manage {@link Person}s.
*
* @author Michael Hunger
* @author Oliver Gierke
* @since 29.03.11
*/
public interface PersonRepository extends GraphRepository<Person>, NamedIndexRepository<Person> {
@Query("start team=node({p_team}) match (team)-[:persons]->(member) return member")
Iterable<Person> findAllTeamMembers(@Param("p_team") Group team);
@Query(value = "g.v(team).out('persons')", type = QueryType.Gremlin)
Iterable<Person> findAllTeamMembersGremlin(@Param("team") Group team);
@Query("start team=node({p_team}) match (team)-[:persons]->(member) return member.name,member.age")
Iterable<Map<String,Object>> findAllTeamMemberData(@Param("p_team") Group team);
@Query("start person=node({p_person}) match (boss)-[:boss]->(person) return boss")
Person findBoss(@Param("p_person") Person person);
Group findTeam(@Param("p_person") Person person);
@Query("start team=node({p_team}) match (team)-[:persons]->(member) return member")
Page<Person> findAllTeamMembersPaged(@Param("p_team") Group team, Pageable page);
@Query("start team=node({p_team}) match (team)-[:persons]->(member) return member")
Iterable<Person> findAllTeamMembersSorted(@Param("p_team") Group team, Sort sort);
// Derived queries
Iterable<Person> findByName(String name);
}

View File

@@ -0,0 +1,64 @@
/**
* 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.repository;
import java.util.Arrays;
import java.util.Map;
import org.neo4j.helpers.collection.MapUtil;
import org.springframework.data.neo4j.model.Group;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.neo4j.model.Personality;
/**
* @author mh
* @since 13.06.11
*/
public class TestTeam {
public Person michael;
public Person emil;
public Person david;
public Group sdg;
public TestTeam() {
}
public void createSDGTeam(PersonRepository repo, GroupRepository groupRepo) {
emil = new Person("Emil", 30);
michael = new Person("Michael", 36);
michael.setBoss(emil);
michael.setPersonality(Personality.EXTROVERT);
david = new Person("David", 25);
david.setBoss(emil);
sdg = new Group();
sdg.setName("SDG");
sdg.addPerson(michael);
sdg.addPerson(emil);
sdg.addPerson(david);
repo.save(Arrays.asList(emil, david, michael));
groupRepo.save(sdg);
}
public Map<String, Object> simpleRowFor(final Person person, String prefix) {
return MapUtil.map(prefix+".name", person.getName(), prefix+".age", person.getAge());
}
}

View File

@@ -0,0 +1,124 @@
/**
* 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.repository.query;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
import org.springframework.data.repository.query.parser.Part;
/**
* Unit tests for {@link CypherQueryBuilder}.
*
* @author Oliver Gierke
*/
public class CypherQueryBuilderUnitTests {
CypherQueryBuilder query;
private final String className = Person.class.getName();
@Before
public void setUp() {
Neo4jMappingContext context = new Neo4jMappingContext();
query = new CypherQueryBuilder(context, Person.class);
}
@Test
public void createsQueryForSimplePropertyReference() {
Part part = new Part("name", Person.class);
query.addRestriction(part);
assertThat(query.toString(), is("start person=node:Person(name={_0}) return person"));
}
@Test
public void createsQueryForPropertyOnRelationShipReference() {
Part part = new Part("group.name", Person.class);
query.addRestriction(part);
assertThat(query.toString(), is("start person_group=node:Group(name={_0}) match person<-[:members]-person_group return person"));
}
@Test
public void createsQueryForMultipleStartClauses() {
query.addRestriction(new Part("name", Person.class));
query.addRestriction(new Part("group.name", Person.class));
assertThat(query.toString(),
is("start person=node:Person(name={_0}), person_group=node:Group(name={_1}) match person<-[:members]-person_group return person"));
}
@Test
public void createsSimpleWhereClauseCorrectly() {
query.addRestriction(new Part("age", Person.class));
final String className = Person.class.getName();
assertThat(query.toString(), is("start person=node:__types__(className=\"" + className + "\") where person.age = {_0} return person"));
}
@Test
public void createsSimpleTraversalClauseCorrectly() {
query.addRestriction(new Part("group", Person.class));
assertThat(query.toString(), is("start person=node:__types__(className=\"" + className + "\") match person<-[:members]-person_group return person"));
}
@Test
public void buildsComplexQueryCorrectly() {
query.addRestriction(new Part("name", Person.class));
query.addRestriction(new Part("groupName", Person.class));
query.addRestriction(new Part("ageGreaterThan", Person.class));
query.addRestriction(new Part("groupMembersAge", Person.class));
System.out.println(query.toString());
assertThat(query.toString(), is(
"start person=node:Person(name={_0}), person_group=node:Group(name={_1}) " +
"match person<-[:members]-person_group, person<-[:members]-person_group-[:members]->person_group_members " +
"where person.age > {_2}, person_group_members.age = {_3} " +
"return person"
));
}
@Test
public void buildsQueryWithSort() {
query.addRestriction(new Part("name",Person.class));
assertThat(query.toString(new Sort("person.name")), is("start person=node:Person(name={_0}) return person order by person.name ASC"));
}
@Test
public void buildsQueryWithTwoSorts() {
query.addRestriction(new Part("name",Person.class));
Sort sort = new Sort(new Sort.Order("person.name"),new Sort.Order(Sort.Direction.DESC, "person.age"));
assertThat(query.toString(sort), is("start person=node:Person(name={_0}) return person order by person.name ASC,person.age DESC"));
}
@Test
public void buildsQueryWithPage() {
query.addRestriction(new Part("name",Person.class));
Pageable pageable = new PageRequest(3,10,new Sort("person.name"));
assertThat(query.toString(pageable), is("start person=node:Person(name={_0}) return person order by person.name ASC skip 30 limit 10"));
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.repository.query;
import java.util.Set;
import org.neo4j.graphdb.Direction;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;
@NodeEntity
class Group {
@Indexed
String name;
@RelatedTo(type = "members", direction = Direction.OUTGOING)
Set<Person> members;
}

View File

@@ -20,19 +20,15 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.Collections;
import java.util.Set;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.neo4j.graphdb.Direction;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
/**
* Unit tests for {@link MatchClause}.
*
* @author Oliver Gierke
*/
@@ -40,51 +36,36 @@ import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
public class MatchClauseUnitTest {
Neo4jMappingContext context;
private VariableContext variableContext;
@Before
public void setUp() {
context = new Neo4jMappingContext();
context.setInitialEntitySet(Collections.singleton(Person.class));
context.afterPropertiesSet();
variableContext = new VariableContext();
}
@Test
public void buildsMatchExpressionForSimpleTraversalCorrectly() {
MatchClause clause = new MatchClause(context, PropertyPath.from("group", Person.class));
assertThat(clause.toString(), is("(person)<-[:members]-(group)"));
PropertyPath path = PropertyPath.from("group", Person.class);
MatchClause clause = new MatchClause(context.getPersistentPropertyPath(path));
assertThat(clause.toString(variableContext), is("person<-[:members]-person_group"));
}
@Test
public void createsMatchClassForDeepTraversal() {
MatchClause clause = new MatchClause(context, PropertyPath.from("group.members.age", Person.class));
assertThat(clause.toString(), is("(person)<-[:members]-(group)-[:members]->(members)"));
PropertyPath path = PropertyPath.from("group.members.age", Person.class);
MatchClause clause = new MatchClause(context.getPersistentPropertyPath(path));
assertThat(clause.toString(variableContext), is("person<-[:members]-person_group-[:members]->person_group_members"));
}
@Test
public void stopsAtNonRelationShipPropertyPath() {
MatchClause clause = new MatchClause(context, PropertyPath.from("group.name", Person.class));
assertThat(clause.toString(), is("(person)<-[:members]-(group)"));
}
@NodeEntity
class Person {
private int age;
@RelatedTo(type = "members", direction = Direction.INCOMING)
private Group group;
}
@NodeEntity
class Group {
@Indexed
private String name;
@RelatedTo(type = "members", direction = Direction.OUTGOING)
private Set<Person> members;
PropertyPath path = PropertyPath.from("group.name", Person.class);
MatchClause clause = new MatchClause(context.getPersistentPropertyPath(path));
assertThat(clause.toString(variableContext), is("person<-[:members]-person_group"));
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.repository.query;
import org.neo4j.graphdb.Direction;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;
@NodeEntity
class Person {
@Indexed
String name;
int age;
@RelatedTo(type = "members", direction = Direction.INCOMING)
Group group;
}

View File

@@ -0,0 +1,70 @@
/**
* 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.repository.query;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.neo4j.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
/**
* Unit tests for {@link VariableContext}.
*
* @author Oliver Gierke
*/
public class VariableContextUnitTests {
Neo4jMappingContext mappingContext;
VariableContext context;
@Before
public void setUp() {
mappingContext = new Neo4jMappingContext();
context = new VariableContext();
}
@Test
public void nameForSimplePropertyIsOwner() {
assertThat(context.getVariableFor(getPath("age")), is("person"));
}
@Test
public void nameForPathViaEntityIsOwnerAndEntity() {
assertThat(context.getVariableFor(getPath("group.members")), is("person_group_members"));
}
@Test
public void nameForEntityIsLowercaseSimpleClassName() {
assertThat(context.getVariableFor(mappingContext.getPersistentEntity(Person.class)),is("person"));
}
@Test
public void nameForEntityPropertyIsOwnerAndEntity() {
final PersistentPropertyPath<Neo4jPersistentProperty> gropPath = getPath("group");
assertThat(context.getVariableFor(gropPath),is("person_group"));
}
private PersistentPropertyPath<Neo4jPersistentProperty> getPath(String expression) {
PropertyPath path = PropertyPath.from(expression, Person.class);
return mappingContext.getPersistentPropertyPath(path);
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:neo4j="http://www.springframework.org/schema/data/neo4j"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/data/neo4j http://www.springframework.org/schema/data/neo4j/spring-neo4j-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:annotation-config/>
<neo4j:config graphDatabaseService="graphDatabaseService"/>
<neo4j:repositories base-package="org.springframework.data.neo4j.repository"/>
<bean id="graphDatabaseService" class="org.neo4j.test.ImpermanentGraphDatabase" destroy-method="shutdown"/>
</beans>