DATACOUCH-137 - queryDerivation for N1QL annotated methods.

Abstracted the N1QL-based variants in AbstractN1qlBasedQuery. This implementation doesn't recognize QueryParams or QueryPlan anymore in the method parameters so that the method signatures are not store-dependent.

Added query derivation from PartTree to N1QL query, using N1qlQueryCreator.

Unit tested the mapping between a Part.Type and the corresponding N1QL Expression.
This commit is contained in:
Simon Baslé
2015-07-07 19:41:27 +02:00
parent 9deaf6b480
commit c29727595c
12 changed files with 947 additions and 109 deletions

View File

@@ -18,6 +18,8 @@ package org.springframework.data.couchbase.config;
import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.springframework.data.couchbase.repository.config.CouchbaseRepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryBeanDefinitionParser;
/**
* {@link NamespaceHandler} for Couchbase configuration.
@@ -33,7 +35,8 @@ public class CouchbaseNamespaceHandler extends NamespaceHandlerSupport {
* Register bean definition parsers in the namespace handler.
*/
public final void init() {
//TODO repositories (CouchbaseRepositoryConfigurationExtension and RepositoryBeanDefinitionParser)
CouchbaseRepositoryConfigurationExtension extension = new CouchbaseRepositoryConfigurationExtension();
registerBeanDefinitionParser("repositories", new RepositoryBeanDefinitionParser(extension));
registerBeanDefinitionParser("env", new CouchbaseEnvironmentParser());
registerBeanDefinitionParser("cluster", new CouchbaseClusterParser());
registerBeanDefinitionParser("bucket", new CouchbaseBucketParser());

View File

@@ -16,7 +16,9 @@
package org.springframework.data.couchbase.core.mapping;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentPropertyPath;
/**
* Represents a property part of an entity that needs to be persisted.
@@ -32,4 +34,16 @@ public interface CouchbasePersistentProperty extends PersistentProperty<Couchbas
*/
String getFieldName();
/**
* A converter that can be used to extract the {@link #getFieldName() fieldName}, eg. when one wants
* a path from {@link PersistentPropertyPath#toDotPath(Converter)} made of field names.
*/
Converter<? super CouchbasePersistentProperty,String> FIELD_NAME = new Converter<CouchbasePersistentProperty, String>() {
@Override
public String convert(CouchbasePersistentProperty source) {
return source.getFieldName();
}
};
}

View File

@@ -16,13 +16,10 @@
package org.springframework.data.couchbase.repository.query;
import java.util.Iterator;
import java.util.List;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.query.Query;
import com.couchbase.client.java.query.QueryParams;
import com.couchbase.client.java.query.QueryPlan;
import com.couchbase.client.java.query.Statement;
import org.springframework.data.couchbase.core.CouchbaseOperations;
@@ -38,7 +35,7 @@ import org.springframework.data.util.StreamUtils;
*/
public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
private final CouchbaseQueryMethod queryMethod;
protected final CouchbaseQueryMethod queryMethod;
private final CouchbaseOperations couchbaseOperations;
protected AbstractN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
@@ -46,45 +43,28 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
this.couchbaseOperations = couchbaseOperations;
}
protected abstract Statement getStatement();
protected abstract Statement getStatement(ParameterAccessor accessor);
protected abstract JsonArray getPlaceholderValues(ParameterAccessor accessor);
@Override
public Object execute(Object[] parameters) {
Statement statement = getStatement();
ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
Statement statement = getStatement(accessor);
JsonArray queryPlaceholderValues = getPlaceholderValues(accessor);
ParameterAccessor parameterAccessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
Query query = buildQuery(statement, parameterAccessor.iterator());
Query query = buildQuery(statement, queryPlaceholderValues);
return executeDependingOnType(query, queryMethod, queryMethod.isPageQuery(), queryMethod.isModifyingQuery(),
queryMethod.isSliceQuery());
}
protected static Query buildQuery(Statement statement, Iterator<Object> paramIterator) {
JsonArray queryValues = JsonArray.create();
QueryParams queryParams = null;
QueryPlan preparedPayload = null;
while (paramIterator.hasNext()) {
Object next = paramIterator.next();
if (next instanceof QueryParams) {
queryParams = (QueryParams) next;
}
else if (next instanceof QueryPlan) {
preparedPayload = (QueryPlan) next;
}
else {
queryValues.add(next);
}
}
protected static Query buildQuery(Statement statement, JsonArray queryPlaceholderValues) {
Query query;
if (preparedPayload != null) {
query = Query.prepared(preparedPayload, queryValues.isEmpty() ? null : queryValues, queryParams);
}
else if (!queryValues.isEmpty()) {
query = Query.parametrized(statement, queryValues, queryParams);
if (!queryPlaceholderValues.isEmpty()) {
query = Query.parametrized(statement, queryPlaceholderValues);
}
else {
query = Query.simple(statement, queryParams);
query = Query.simple(statement);
}
return query;
@@ -123,4 +103,8 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
public CouchbaseQueryMethod getQueryMethod() {
return this.queryMethod;
}
protected CouchbaseOperations getCouchbaseOperations() {
return this.couchbaseOperations;
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.data.couchbase.repository.query;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
@@ -26,8 +28,6 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
/**
* Represents a query method with couchbase extensions, allowing to discover
* if View-based query or N1QL-based query must be used.

View File

@@ -0,0 +1,301 @@
/*
* Copyright 2012-2015 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.couchbase.repository.query;
import static com.couchbase.client.java.query.dsl.Expression.s;
import static com.couchbase.client.java.query.dsl.Expression.x;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.functions.StringFunctions;
import com.couchbase.client.java.query.dsl.path.LimitPath;
import com.couchbase.client.java.query.dsl.path.OrderByPath;
import com.couchbase.client.java.query.dsl.path.WherePath;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**
* This {@link AbstractQueryCreator Query Creator} is responsible for parsing a {@link PartTree} (representing
* a method name) into the WHERE clause of a N1QL query.
* <p>
* In the following, "field" represents the path in JSON deduced from the part of the method name. "a" and "b" represent
* the values of next consumed method parameters. "array" represent a {@link JsonArray} constructed from the next method
* parameter value (if a collection or array, contained values are used to fill the array, otherwise it's a single item
* array).
* <br/>
* Here are the {@link Part.Type} supported (<code>field</code>:
* <ul>
* <li><b>BETWEEN:</b> field BETWEEN a AND b</li>
* <li><b>IS_NOT_NULL:</b> field IS NOT NULL</li>
* <li><b>IS_NULL:</b> field IS NULL</li>
* <li><b>NEGATING_SIMPLE_PROPERTY:</b> - field != a</li>
* <li><b>SIMPLE_PROPERTY:</b> - field = a</li>
* <li><b>LESS_THAN:</b> field &lt; a</li>
* <li><b>LESS_THAN_EQUAL:</b> field &lt;= a</li>
* <li><b>GREATER_THAN_EQUAL:</b> field &gt;= a</li>
* <li><b>GREATER_THAN:</b> field &gt; a</li>
* <li><b>BEFORE:</b> field &lt; a</li>
* <li><b>AFTER:</b> field &gt; a</li>
* <li><b>NOT_LIKE:</b> field NOT LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)</li>
* <li><b>LIKE:</b> field LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)</li>
* <li><b>STARTING_WITH:</b> field LIKE "a%" - a should be a String prefix</li>
* <li><b>ENDING_WITH:</b> field LIKE "%a" - a should be a String suffix</li>
* <li><b>NOT_CONTAINING:</b> field NOT LIKE "%a%" - a should be a String</li>
* <li><b>CONTAINING:</b> field LIKE "%a%" - a should be a String</li>
* <li><b>NOT_IN:</b> field NOT IN array - note that the next parameter value (or its children if a collection/array)
* should be compatible for storage in a {@link JsonArray})</li>
* <li><b>IN:</b> field IN array - note that the next parameter value (or its children if a collection/array) should
* be compatible for storage in a {@link JsonArray})</li>
* <li><b>TRUE:</b> field = TRUE</li>
* <li><b>FALSE:</b> field = FALSE</li>
* <li><b>REGEX:</b> REGEXP_LIKE(field, "a") - note that the ignoreCase is ignored here, a is a regular expression
* in String form</li>
* <li><b>EXISTS:</b> field IS NOT MISSING - used to verify that the JSON contains this attribute</li>
* </ul>
* <br/>
* The following are not supported and will throw an {@link IllegalArgumentException} if encountered:
* <ul>
* <li><b>NEAR, WITHIN:</b> geospatial is not supported in N1QL as of now</li>
* </ul>
* </p>
*
* @author Simon Baslé
*/
public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression> {
private final ParameterAccessor accessor;
private final WherePath selectFrom;
private final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> context;
public N1qlQueryCreator(PartTree tree, ParameterAccessor parameters, WherePath selectFrom,
CouchbaseConverter converter) {
super(tree, parameters);
this.accessor = parameters;
this.selectFrom = selectFrom;
this.context = converter.getMappingContext();
}
@Override
protected Expression create(Part part, Iterator<Object> iterator) {
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
return prepareExpression(part, path, iterator);
}
@Override
protected Expression and(Part part, Expression base, Iterator<Object> iterator) {
if (base == null) {
return create(part, iterator);
}
return base.and(create(part, iterator));
}
@Override
protected Expression or(Expression base, Expression criteria) {
return base.or(criteria);
}
@Override
protected LimitPath complete(Expression criteria, Sort sort) {
OrderByPath selectFromWhere = selectFrom.where(criteria);
if (sort != null) {
List<com.couchbase.client.java.query.dsl.Sort> cbSortList = new ArrayList<com.couchbase.client.java.query.dsl.Sort>();
for (Sort.Order order : sort) {
if (order.isAscending()) {
cbSortList.add(com.couchbase.client.java.query.dsl.Sort.asc(order.getProperty()));
} else {
cbSortList.add(com.couchbase.client.java.query.dsl.Sort.desc(order.getProperty()));
}
}
com.couchbase.client.java.query.dsl.Sort[] cbSorts =
cbSortList.toArray(new com.couchbase.client.java.query.dsl.Sort[cbSortList.size()]);
return selectFromWhere.orderBy(cbSorts);
}
return selectFrom.where(criteria);
}
protected static Expression prepareExpression(Part part, PersistentPropertyPath<CouchbasePersistentProperty> path,
Iterator<Object> parameterValues) {
//FIXME use conversions for the parameters and types
//get the whole doted path with fieldNames instead of potentially wrong propNames
String fieldNamePath = path.toDotPath(CouchbasePersistentProperty.FIELD_NAME);
//deal with ignore case
boolean ignoreCase = false;
Class<?> leafType = path.getLeafProperty().getType();
boolean isString = leafType == String.class;
if (part.shouldIgnoreCase() == Part.IgnoreCaseType.WHEN_POSSIBLE) {
ignoreCase = isString;
} else if (part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS) {
if (!isString) {
throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", fieldNamePath, leafType));
}
ignoreCase = true;
}
return createExpression(part.getType(), fieldNamePath, ignoreCase, parameterValues);
}
protected static Expression createExpression(Part.Type partType, String fieldNamePath, boolean ignoreCase, Iterator<Object> parameterValues) {
//create the left hand side of the expression, taking ignoreCase into account
Expression left = ignoreCase ? StringFunctions.lower(x(fieldNamePath)) : x(fieldNamePath);
switch (partType) {
case BETWEEN:
return left.between(leftAndRight(parameterValues, ignoreCase));
case IS_NOT_NULL:
return left.isNotNull();
case IS_NULL:
return left.isNull();
case NEGATING_SIMPLE_PROPERTY:
return left.ne(right(parameterValues, ignoreCase));
case SIMPLE_PROPERTY:
return left.eq(right(parameterValues, ignoreCase));
case BEFORE:
case LESS_THAN:
return left.lt(right(parameterValues, ignoreCase));
case LESS_THAN_EQUAL:
return left.lte(right(parameterValues, ignoreCase));
case GREATER_THAN_EQUAL:
return left.gte(right(parameterValues, ignoreCase));
case AFTER:
case GREATER_THAN:
return left.gt(right(parameterValues, ignoreCase));
case NOT_LIKE:
return left.notLike(right(parameterValues, ignoreCase));
case LIKE:
return left.like(right(parameterValues, ignoreCase));
case STARTING_WITH:
return left.like(like(parameterValues, ignoreCase, false, true));
case ENDING_WITH:
return left.like(like(parameterValues, ignoreCase, true, false));
case NOT_CONTAINING:
return left.notLike(like(parameterValues, ignoreCase, true, true));
case CONTAINING:
return left.like(like(parameterValues, ignoreCase, true, true));
case NOT_IN:
return left.notIn(rightArray(parameterValues));
case IN:
return left.in(rightArray(parameterValues));
case TRUE:
return left.eq(true);
case FALSE:
return left.eq(false);
case REGEX:
return regexp(fieldNamePath, parameterValues);
case EXISTS:
return left.isNotMissing();
case WITHIN:
case NEAR:
default:
throw new IllegalArgumentException("Unsupported keyword");
}
}
protected static Expression regexp(String left, Iterator<Object> parameterValues) {
//TODO migrate to using the Functions util class when 2.0-dp2 / 2.0 GA
Object next = parameterValues.next();
String pattern;
if (next == null) {
pattern = "";
} else {
pattern = String.valueOf(next);
}
return x("REGEXP_LIKE(" + left + ", \"" + pattern + "\")");
}
protected static Expression leftAndRight(Iterator<Object> parameterValues, boolean ignoreCase) {
return right(parameterValues, ignoreCase).and(right(parameterValues, ignoreCase));
}
protected static Expression like(Iterator<Object> parameterValues, boolean ignoreCase,
boolean anyPrefix, boolean anySuffix) {
Object next = parameterValues.next();
if (next == null) {
return Expression.NULL();
}
Expression converted;
if (next instanceof String) {
String pattern = (String) next;
if (anyPrefix) {
pattern = "%" + pattern;
}
if (anySuffix) {
pattern = pattern + "%";
}
converted = s(pattern);
} else {
converted = x(String.valueOf(next));
}
if (ignoreCase) {
return StringFunctions.lower(converted);
}
return converted;
}
protected static Expression right(Iterator<Object> parameterValues, boolean ignoreCase) {
Object next = parameterValues.next();
if (next == null) {
return Expression.NULL();
}
Expression converted;
if (next instanceof String) {
converted = s((String) next);
} else {
converted = x(String.valueOf(next));
}
if (ignoreCase) {
return StringFunctions.lower(converted);
}
return converted;
}
protected static JsonArray rightArray(Iterator<Object> parameterValues) {
Object next = parameterValues.next();
Object[] values;
if (next instanceof Collection) {
values = ((Collection<?>) next).toArray();
} else if (next.getClass().isArray()) {
values = (Object[]) next;
} else {
values = new Object[] {next};
}
return JsonArray.from(values);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2015 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.couchbase.repository.query;
import static com.couchbase.client.java.query.Select.select;
import static com.couchbase.client.java.query.dsl.Expression.i;
import static com.couchbase.client.java.query.dsl.Expression.path;
import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count;
import static com.couchbase.client.java.query.dsl.functions.MetaFunctions.meta;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.path.FromPath;
import com.couchbase.client.java.query.dsl.path.LimitPath;
import com.couchbase.client.java.query.dsl.path.WherePath;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.PartTree;
public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
private final PartTree partTree;
public PartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
super(queryMethod, couchbaseOperations);
this.partTree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType());
}
@Override
protected JsonArray getPlaceholderValues(ParameterAccessor accessor) {
return JsonArray.empty();
}
@Override
protected Statement getStatement(ParameterAccessor accessor) {
Expression bucket = i(getCouchbaseOperations().getCouchbaseBucket().name());
Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID);
Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS);
FromPath select;
if (partTree.isCountProjection()) {
select = select(count("*"));
} else {
select = select(metaId, metaCas, path(bucket, "*"));
}
WherePath selectFrom = select.from(bucket);
N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom, getCouchbaseOperations().getConverter());
LimitPath selectFromWhereOrderBy = queryCreator.createQuery();
if (partTree.isLimiting()) {
return selectFromWhereOrderBy.limit(partTree.getMaxResults());
} else {
return selectFromWhereOrderBy;
}
}
}

View File

@@ -16,10 +16,12 @@
package org.springframework.data.couchbase.repository.query;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.query.Query;
import com.couchbase.client.java.query.Statement;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
/**
@@ -63,7 +65,17 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
}
@Override
public Statement getStatement() {
protected JsonArray getPlaceholderValues(ParameterAccessor accessor) {
JsonArray values = JsonArray.create();
for (Object value : accessor) {
values.add(value);
}
return values;
}
@Override
public Statement getStatement(ParameterAccessor accessor) {
return this.statement;
}
}

View File

@@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.PartTreeN1qlBasedQuery;
import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
import org.springframework.data.couchbase.repository.query.ViewBasedCouchbaseQuery;
import org.springframework.data.mapping.context.MappingContext;
@@ -32,6 +33,7 @@ import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;
@@ -121,7 +123,7 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
}
@Override
protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key) {
protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key, EvaluationContextProvider contextProvider) {
return new CouchbaseQueryLookupStrategy();
}
@@ -141,8 +143,7 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations);
} else {
//FIXME return new PartBasedN1qlQuery(queryMethod, couchbaseOperations, namedQueries);
return new StringN1qlBasedQuery("SELECT 1", queryMethod, couchbaseOperations);
return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
}
}
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);