DATAGRAPH-389 : Derived Queries for Labels - part 1

This commit is contained in:
Nicki Watt
2013-10-14 16:52:04 +01:00
committed by Michael Hunger
parent 7ca8959caf
commit 4c1be9925a
9 changed files with 318 additions and 93 deletions

View File

@@ -23,15 +23,13 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.neo4j.support.typerepresentation.LabelBasedNodeTypeRepresentationStrategy;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategyFactory;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.parser.Part;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import static org.springframework.util.StringUtils.*;
import static org.springframework.util.StringUtils.hasText;
public class CypherQuery implements CypherQueryDefinition {
private final VariableContext variableContext = new VariableContext();
@@ -79,15 +77,8 @@ public class CypherQuery implements CypherQueryDefinition {
if (!addedStartClause(partInfo)) {
whereClauses.add(new WhereClause(partInfo,template));
}
} else if (leafProperty.isRelationship()) {
startClauses.add(new NodeEntityMatchingStartClause(partInfo));
if (useLabels) {
whereClauses.add(new LabelBasedTypeRestrictingWhereClause(new PartInfo(path, variableContext.getVariableFor(entity), part, -1), entity, template));
} else {
whereClauses.add(new IndexBasedTypeRestrictingWhereClause(new PartInfo(path, variableContext.getVariableFor(entity), part, -1), entity, template));
}
} else if (leafProperty.isIdProperty()) {
startClauses.add(new NodeEntityMatchingStartClause(partInfo));
} else if (leafProperty.isRelationship() || leafProperty.isIdProperty()) {
startClauses.add(new GraphIdStartClause(partInfo));
if (useLabels) {
whereClauses.add(new LabelBasedTypeRestrictingWhereClause(new PartInfo(path, variableContext.getVariableFor(entity), part, -1), entity, template));
} else {
@@ -128,11 +119,19 @@ public class CypherQuery implements CypherQueryDefinition {
private boolean addedStartClause(PartInfo partInfo) {
if (!partInfo.isIndexed()) return false;
for (StartClause startClause : startClauses) {
// only merge when same index and same variable
ListIterator<StartClause> it = startClauses.listIterator();
while (it.hasNext()) {
StartClause startClause = it.next();
if (startClause.sameIdentifier(partInfo)) {
if (startClause.sameIndex(partInfo)) {
startClause.merge(partInfo);
if (startClause.hasMultipleParts()) {
// Replace it as we could not detect this initially ...
List<PartInfo> newList = new ArrayList<PartInfo>();
newList.addAll(startClause.getPartInfos());
it.set(StartClauseFactory.create(newList));
}
return true;
} else {
// must stop b/c of invalid combination (same identifier but different index)
@@ -140,7 +139,7 @@ public class CypherQuery implements CypherQueryDefinition {
}
}
}
startClauses.add(new StartClause(partInfo));
startClauses.add(StartClauseFactory.create(partInfo));
return true;
}
@@ -174,52 +173,82 @@ public class CypherQuery implements CypherQueryDefinition {
}
private String render() {
String startClauses = collectionToDelimitedString(this.startClauses, ", ");
String matchClauses = toQueryString(this.matchClauses);
String whereClauses = collectionToDelimitedString(this.whereClauses, " AND ");
StringBuilder builder = new StringBuilder("");
boolean startClauseInUse = renderStartClauses(builder, startClauses);
renderMatchClauses(builder, matchClauses, startClauseInUse);
renderWhereClauses(builder, whereClauses);
renderReturnClauses(builder);
boolean matchKeyWordUsed = false;
boolean startClauseUsed = buildStartClauseIfRequired(builder, startClauses);
if (!startClauseUsed && useLabels) {
matchKeyWordUsed = true;
builder.append(" MATCH ").append(defaultMatchBasedStartClause(entity));
}
if (hasText(matchClauses)) {
builder.append(matchKeyWordUsed ? " , " : " MATCH ");
builder.append(matchClauses);
}
if (hasText(whereClauses)) {
builder.append(" WHERE ").append(whereClauses);
}
return builder.toString();
}
private void renderReturnClauses(StringBuilder builder) {
String returnEntity = String.format(QueryTemplates.VARIABLE,getEntityName(entity));
if (isCountQuery) {
builder.append(" RETURN ").append("count(").append(returnEntity).append(")");
} else {
builder.append(" RETURN ").append(returnEntity);
}
return builder.toString();
}
private void renderWhereClauses(StringBuilder builder, String whereClauses) {
if (hasText(whereClauses)) {
builder.append(" WHERE ").append(whereClauses);
}
}
private void renderMatchClauses(StringBuilder builder, String matchClauses, boolean startClauseInUse) {
if (hasText(matchClauses)) {
builder.append(" MATCH ").append(matchClauses);
return;
}
if (useLabelBasedTRS() && !startClauseInUse) {
builder.append(" MATCH ").append(defaultMatchBasedStartClause(entity));
return;
}
}
/**
* Note: This will change to get rid of the start clauses completely but
* for now we just get it to work!
* From Neo4j 2.0 onwards the start clause is optional (although still needs to be
* used for certain cases where index lookups are required etc. This method takes
* the currently built up query in builder (which should be empty at this point)
* and begins to add in any currently defined start clauses. In the absence of
* any start clauses, it will add in the default start clause if using an indexing
* strtegy.
*
* @param builder current query which has been built up
* @param startClauses List of current start clauses
* @return true if this method resulted in the query having a START clause
* applied to it otherwise false
*/
private boolean buildStartClauseIfRequired(StringBuilder builder, String startClauses) {
private boolean renderStartClauses(StringBuilder builder, String startClauses) {
if (hasText(startClauses)) {
builder.append("START ").append(startClauses);
return true;
} else if (!useLabels) {
// TODO: Need to change index based stuff to also not use START
}
if (useIndexBasedTRS()) {
builder.append("START ").append(defaultIndexBasedStartClause(entity));
return true;
}
// For a label based strategy, we do not use a start clause
// but rather begin with a MATCH clause
return false;
}
private boolean useIndexBasedTRS() {
return !useLabels;
}
private boolean useLabelBasedTRS() {
return useLabels;
}
private String addSorts(Sort sort) {
final List<String> sorts = formatSorts(sort);

View File

@@ -0,0 +1,60 @@
/**
* Copyright 2013 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.helpers.collection.IteratorUtil;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.repository.query.Parameter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Represents a start clause which makes use of an exact index
* match.
*
* @author Nicki Watt
*/
public class ExactIndexBasedStartClause extends IndexBasedStartClause {
public ExactIndexBasedStartClause(PartInfo partInfo) {
super(partInfo);
}
@Override
public String toString() {
final PartInfo partInfo = getPartInfo();
final String identifier = partInfo.getIdentifier();
final String indexName = partInfo.getIndexName();
final int parameterIndex = partInfo.getParameterIndex();
return String.format(QueryTemplates.START_CLAUSE_INDEX_LOOKUP, identifier, indexName, partInfo.getNeo4jPropertyName(), parameterIndex);
}
@Override
public Map<Parameter, Object> resolveParameters(Map<Parameter, Object> parameters, Neo4jTemplate template) {
Map<Parameter, PartInfo> myParameters = findMyParameters(parameters.keySet());
Map<Parameter, Object> result = new LinkedHashMap<Parameter, Object>(parameters);
result.keySet().removeAll(myParameters.keySet());
final Map<PartInfo, Object> values = matchToPartsAndConvert(myParameters, parameters,template);
Parameter firstParam = IteratorUtil.first(myParameters.keySet());
Object value=IteratorUtil.first(values.values());
result.put(firstParam, value);
return result;
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright 2013 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.helpers.collection.IteratorUtil;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.repository.query.Parameter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a start clause which makes use of a full text index
* based match against one or more parts.
*
* @author Nicki Watt
*/
public class FullTextIndexBasedStartClause extends IndexBasedStartClause {
public FullTextIndexBasedStartClause(PartInfo partInfo) {
super(partInfo);
}
public FullTextIndexBasedStartClause(List<PartInfo> partInfos) {
super(partInfos.get(0));
int i = 0;
for (PartInfo partInfo: partInfos) {
if (i != 0) {
this.merge(partInfo);
}
i++;
}
}
@Override
public String toString() {
final PartInfo partInfo = getPartInfo();
final String identifier = partInfo.getIdentifier();
final String indexName = partInfo.getIndexName();
final int parameterIndex = partInfo.getParameterIndex();
return String.format(QueryTemplates.START_CLAUSE_INDEX_QUERY, identifier, indexName, parameterIndex);
}
@Override
public Map<Parameter, Object> resolveParameters(Map<Parameter, Object> parameters, Neo4jTemplate template) {
Map<Parameter, PartInfo> myParameters = findMyParameters(parameters.keySet());
Map<Parameter, Object> result = new LinkedHashMap<Parameter, Object>(parameters);
result.keySet().removeAll(myParameters.keySet());
final Map<PartInfo, Object> values = matchToPartsAndConvert(myParameters, parameters,template);
Parameter firstParam = IteratorUtil.first(myParameters.keySet());
result.put(firstParam, renderQuery(values));
return result;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2011 the original author or authors.
* Copyright 2013 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.
@@ -20,8 +20,15 @@ import org.springframework.data.repository.query.Parameter;
import java.util.Map;
public class NodeEntityMatchingStartClause extends StartClause {
public NodeEntityMatchingStartClause(PartInfo partInfo) {
/**
* Represents a start clause where the actual graph id
* of the entity is used.
*
* @author Nicki Watt
*/
public class GraphIdStartClause extends StartClause {
public GraphIdStartClause(PartInfo partInfo) {
super(partInfo);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2011 the original author or authors.
* Copyright 2013 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.
@@ -15,16 +15,17 @@
*/
package org.springframework.data.neo4j.repository.query;
public class IndexRestrictingStartClause extends StartClause {
private final String className;
/**
* Abstract class which represents a start clause which makes
* use of an indexof some sort.
*
* @author Nicki Watt
*/
abstract class IndexBasedStartClause extends StartClause {
public IndexRestrictingStartClause(PartInfo partInfo, String className) {
public IndexBasedStartClause(PartInfo partInfo) {
super(partInfo);
this.className = className;
}
@Override
public String toString() {
return String.format(QueryTemplates.DEFAULT_INDEXBASED_START_CLAUSE, getPartInfo().getIdentifier(), className);
}
}

View File

@@ -22,6 +22,8 @@ import org.springframework.data.repository.query.parser.Part;
import org.springframework.util.Assert;
/**
* This class represents ....
* All the information about a particular part of a Neo4jPersistentProperty?
* @author mh
* @since 31.10.11
*/

View File

@@ -27,60 +27,33 @@ import java.util.*;
/**
* Representation of a Cypher {@literal start} clause.
*
*
* @author Oliver Gierke
*/
// TODO id-startclause, index-startclause (exact,point,fulltext)
class StartClause {
abstract class StartClause {
private final SortedMap<Integer,PartInfo> partInfos=new TreeMap<Integer, PartInfo> ();
/**
* Creates a new {@link StartClause} from the given {@link Neo4jPersistentProperty}, variable and the given
* parameter index.
* Creates a new {@link StartClause} from the given {@link Neo4jPersistentProperty},
* variable and the given parameter index.
*/
public StartClause(PartInfo partInfo) {
this.partInfos.put(partInfo.getParameterIndex(), partInfo);
}
@Override
public String toString() {
final PartInfo partInfo = getPartInfo();
final String identifier = partInfo.getIdentifier();
final String indexName = partInfo.getIndexName();
final int parameterIndex = partInfo.getParameterIndex();
// fulltext or multiple
if (shouldRenderQuery()) {
return String.format(QueryTemplates.START_CLAUSE_INDEX_QUERY, identifier, indexName, parameterIndex);
}
// exact and single
return String.format(QueryTemplates.START_CLAUSE_INDEX_LOOKUP, identifier, indexName, partInfo.getNeo4jPropertyName(), parameterIndex);
/**
* Returns true if this start clause comprises of multiple parts
* @return
*/
protected boolean hasMultipleParts() {
return partInfos.size() > 1;
}
private boolean shouldRenderQuery() {
PartInfo partInfo = getPartInfo();
return partInfo.isFullText() || EnumSet.of(Part.Type.LIKE,Part.Type.STARTING_WITH,Part.Type.CONTAINING,Part.Type.ENDING_WITH).contains(partInfo.getType())|| partInfos.size() > 1;
}
public Map<Parameter, Object> resolveParameters(Map<Parameter, Object> parameters, Neo4jTemplate template) {
Map<Parameter, PartInfo> myParameters = findMyParameters(parameters.keySet());
public abstract Map<Parameter, Object> resolveParameters(Map<Parameter, Object> parameters, Neo4jTemplate template);
Map<Parameter, Object> result = new LinkedHashMap<Parameter, Object>(parameters);
result.keySet().removeAll(myParameters.keySet());
final Map<PartInfo, Object> values = matchToPartsAndConvert(myParameters, parameters,template);
Parameter firstParam = IteratorUtil.first(myParameters.keySet());
if (shouldRenderQuery()) {
result.put(firstParam, renderQuery(values));
} else {
Object value=IteratorUtil.first(values.values());
result.put(firstParam, value);
}
return result;
}
private String renderQuery(Map<PartInfo, Object> values) {
protected String renderQuery(Map<PartInfo, Object> values) {
StringBuilder sb=new StringBuilder();
for (Map.Entry<PartInfo, Object> entry : values.entrySet()) {
if (sb.length()>0) sb.append(" AND ");
@@ -91,7 +64,7 @@ class StartClause {
return sb.toString();
}
private Map<PartInfo, Object> matchToPartsAndConvert(Map<Parameter, PartInfo> myParameters, Map<Parameter, Object> parameters, Neo4jTemplate template) {
protected Map<PartInfo, Object> matchToPartsAndConvert(Map<Parameter, PartInfo> myParameters, Map<Parameter, Object> parameters, Neo4jTemplate template) {
Map<PartInfo, Object> result = new LinkedHashMap<PartInfo, Object>();
for (Map.Entry<Parameter, PartInfo> entry : myParameters.entrySet()) {
Object value = parameters.get(entry.getKey());
@@ -103,15 +76,15 @@ class StartClause {
return result;
}
private Object convertIfNecessary(Neo4jTemplate template, Object value, Neo4jPersistentProperty property) {
if (property.isIndexedNumerically()) return new ValueContext(value).indexNumeric();
protected Object convertIfNecessary(Neo4jTemplate template, Object value, Neo4jPersistentProperty property) {
if (property.isIndexedNumerically()) return new ValueContext(value).indexNumeric();
if (property.isNeo4jPropertyType() && property.isNeo4jPropertyValue(value)) return value;
PropertyConverter converter = new PropertyConverter(template.getConversionService(), property);
return converter.serializePropertyValue(value);
}
private Map<Parameter,PartInfo> findMyParameters(Set<Parameter> parameters) {
protected Map<Parameter,PartInfo> findMyParameters(Set<Parameter> parameters) {
Map<Parameter,PartInfo> result=new LinkedHashMap<Parameter, PartInfo>();
for (Parameter parameter : parameters) {
PartInfo partInfo = partInfos.get(parameter.getIndex());
@@ -149,4 +122,10 @@ class StartClause {
}
return true;
}
}
protected Collection<PartInfo> getPartInfos() {
return partInfos.values();
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright 2013 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.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.repository.query.parser.Part;
import java.util.EnumSet;
import java.util.List;
/**
* This class is responsible for creating appropriate StartClause
* instances depending on the part(s) it is being created from.
*
* @author Nicki Watt
*/
public class StartClauseFactory {
/**
* At present, Multiple parts are always assumed to result in a
* a FullTextIndexBasedStartClause
* @param partInfos The various parts which the start clause
* needs to be created around/for.
* @return A appropriate StartClause
*/
public static StartClause create(List<PartInfo> partInfos) {
return (partInfos.size() == 1)
? create(partInfos.get(0))
: new FullTextIndexBasedStartClause(partInfos);
}
/**
* Given a particular partInfo, create an appropriate StartClause
* instance
*
* @param partInfo The part which the start clause needs to be
* created around/for.
* @return A appropriate StartClause
*/
public static StartClause create(PartInfo partInfo) {
if (partInfo.isIndexed()) {
return (partInfo.isFullText() || isTextualSearchLikePart(partInfo))
? new FullTextIndexBasedStartClause(partInfo)
: new ExactIndexBasedStartClause(partInfo);
}
Neo4jPersistentProperty leafProperty = partInfo.getLeafProperty();
if (leafProperty.isRelationship() || leafProperty.isIdProperty()) {
return new GraphIdStartClause(partInfo);
}
throw new IllegalArgumentException("Cannot determine an appropriate Start Clause for partInfo=" + partInfo );
}
private static boolean isTextualSearchLikePart(PartInfo partInfo) {
return EnumSet.of(Part.Type.LIKE,Part.Type.STARTING_WITH,Part.Type.CONTAINING,Part.Type.ENDING_WITH).contains(partInfo.getType());
}
}

View File

@@ -103,7 +103,7 @@ public abstract class AbstractDerivedFinderMethodTestBase {
protected Object[] trsSpecificExpectedParams;
@Test
public void testCreateIndexQuery() throws Exception {
public void testCypherQueryBuilderWithTwoIndexedParams() throws Exception {
CypherQueryBuilder builder = new CypherQueryBuilder(ctx, Thing.class, template);
builder.addRestriction(new Part("firstName",Thing.class));
builder.addRestriction(new Part("lastName",Thing.class));
@@ -111,6 +111,7 @@ public abstract class AbstractDerivedFinderMethodTestBase {
assertEquals(
getExpectedQuery("START `thing`=node:`Thing`({0}) RETURN `thing`"),
query.toQueryString());
}
@Test