DATACOUCH-413 Use parameterized queries for Query derivation

Changes
-------
- N1ql query creators implement an interface which provides the place
holder values.
- The query creator uses a position index counter to inject place holders in
the statement created and caches the place holder values in an array.
- Repository query constructor passes the generated query statement
and place holder values to query through the SDK.
- Cleanup N1ql integration tests for count validation and also add
couple of more tests

Results
-------
The testing for changes are already covered under the modified existing tests
in N1qlQueryCreator and Repository.
This commit is contained in:
Subhashni Balakrishnan
2018-11-13 19:11:29 -08:00
parent 9840acd2c6
commit c4491dc8b4
13 changed files with 492 additions and 218 deletions

View File

@@ -101,6 +101,7 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
//prepare a count query
Statement countStatement = getCount(accessor, parameters);
//the place holder values are the same for the count query as well
N1qlQuery countQuery = buildQuery(countStatement, queryPlaceholderValues,
getCouchbaseOperations().getDefaultConsistency().n1qlConsistency());
return processor.processResult(executeDependingOnType(query, countQuery, queryMethod, accessor.getPageable(), typeToRead));

View File

@@ -17,6 +17,10 @@
package org.springframework.data.couchbase.repository.query;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.path.*;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
@@ -35,11 +39,13 @@ import org.springframework.data.repository.query.parser.PartTree;
*
* @author Subhashni Balakrishnan
*/
public class N1qlMutateQueryCreator extends AbstractQueryCreator<MutateLimitPath, Expression> {
public class N1qlMutateQueryCreator extends AbstractQueryCreator<MutateLimitPath, Expression> implements PartTreeN1qlQueryCreator {
private final MutateWherePath mutateFrom;
private final CouchbaseConverter converter;
private final CouchbaseQueryMethod queryMethod;
private final ParameterAccessor accessor;
private final JsonArray placeHolderValues;
private final AtomicInteger position;
public N1qlMutateQueryCreator(PartTree tree, ParameterAccessor parameters, MutateWherePath mutateFrom,
CouchbaseConverter converter, CouchbaseQueryMethod queryMethod) {
@@ -48,11 +54,13 @@ public class N1qlMutateQueryCreator extends AbstractQueryCreator<MutateLimitPath
this.converter = converter;
this.queryMethod = queryMethod;
this.accessor = parameters;
this.placeHolderValues = JsonArray.create();
this.position = new AtomicInteger(1);
}
@Override
protected Expression create(Part part, Iterator<Object> iterator) {
return N1qlQueryCreatorUtils.prepareExpression(this.converter, part, iterator);
return N1qlQueryCreatorUtils.prepareExpression(this.converter, part, iterator, this.position, this.placeHolderValues);
}
@Override
@@ -74,4 +82,9 @@ public class N1qlMutateQueryCreator extends AbstractQueryCreator<MutateLimitPath
Expression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter, this.queryMethod.getEntityInformation());
return mutateFrom.where(whereCriteria);
}
@Override
public JsonValue getPlaceHolderValues() {
return this.placeHolderValues;
}
}

View File

@@ -17,8 +17,10 @@
package org.springframework.data.couchbase.repository.query;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.path.LimitPath;
import com.couchbase.client.java.query.dsl.path.OrderByPath;
@@ -83,12 +85,13 @@ import org.springframework.data.repository.query.parser.PartTree;
* @author Subhashni Balakrishnan
* @author Mark Paluch
*/
public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression> {
public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression> implements PartTreeN1qlQueryCreator {
private final WherePath selectFrom;
private final CouchbaseConverter converter;
private final CouchbaseQueryMethod queryMethod;
private final ParameterAccessor accessor;
private final JsonArray placeHolderValues;
private final AtomicInteger position;
public N1qlQueryCreator(PartTree tree, ParameterAccessor parameters, WherePath selectFrom,
CouchbaseConverter converter, CouchbaseQueryMethod queryMethod) {
@@ -97,11 +100,13 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
this.converter = converter;
this.queryMethod = queryMethod;
this.accessor = parameters;
this.placeHolderValues = JsonArray.create();
this.position = new AtomicInteger(1);
}
@Override
protected Expression create(Part part, Iterator<Object> iterator) {
return N1qlQueryCreatorUtils.prepareExpression(converter, part, iterator);
return N1qlQueryCreatorUtils.prepareExpression(converter, part, iterator, this.position, this.placeHolderValues);
}
@Override
@@ -137,4 +142,8 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
return selectFromWhere;
}
@Override
public JsonValue getPlaceHolderValues() {
return this.placeHolderValues;
}
}

View File

@@ -22,6 +22,7 @@ import static com.couchbase.client.java.query.dsl.Expression.i;
import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count;
import static org.springframework.data.couchbase.repository.query.support.N1qlUtils.createReturningExpressionForDelete;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.dsl.Expression;
@@ -49,6 +50,7 @@ import org.springframework.util.Assert;
public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
private final PartTree partTree;
private JsonValue placeHolderValues;
public PartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
super(queryMethod, couchbaseOperations);
@@ -57,7 +59,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
@Override
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
return JsonArray.empty();
return this.placeHolderValues;
}
@Override
@@ -65,10 +67,13 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
Expression bucket = i(getCouchbaseOperations().getCouchbaseBucket().name());
WherePath countFrom = select(count("*").as(CountFragment.COUNT_ALIAS)).from(bucket);
N1qlQueryCreator queryCreator = new N1qlCountQueryCreator(partTree, accessor, countFrom,
N1qlCountQueryCreator queryCountCreator = new N1qlCountQueryCreator(partTree, accessor, countFrom,
getCouchbaseOperations().getConverter(), getQueryMethod());
return queryCreator.createQuery();
Statement statement = queryCountCreator.createQuery();
this.placeHolderValues = queryCountCreator.getPlaceHolderValues();
return statement;
}
@Override
protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) {
String bucketName = getCouchbaseOperations().getCouchbaseBucket().name();
@@ -76,8 +81,10 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
if (partTree.isDelete()) {
DeleteUsePath deleteUsePath = deleteFrom(bucket);
N1qlMutateQueryCreator queryCreator = new N1qlMutateQueryCreator(partTree, accessor, deleteUsePath, getCouchbaseOperations().getConverter(), getQueryMethod());
MutateLimitPath mutateFromWhereOrderBy = queryCreator.createQuery();
N1qlMutateQueryCreator mutateQueryCreator = new N1qlMutateQueryCreator(partTree, accessor, deleteUsePath, getCouchbaseOperations().getConverter(), getQueryMethod());
MutateLimitPath mutateFromWhereOrderBy = mutateQueryCreator.createQuery();
this.placeHolderValues = mutateQueryCreator.getPlaceHolderValues();
if (partTree.isLimiting()) {
return mutateFromWhereOrderBy.limit(partTree.getMaxResults());
} else {
@@ -94,6 +101,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom,
getCouchbaseOperations().getConverter(), getQueryMethod());
LimitPath selectFromWhereOrderBy = queryCreator.createQuery();
this.placeHolderValues = queryCreator.getPlaceHolderValues();
if (queryMethod.isPageQuery()) {
Pageable pageable = accessor.getPageable();

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2018 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 com.couchbase.client.java.document.json.JsonValue;
/**
* A Part Tree Query creator for Couchbase
*
* @author Subhashni Balakrishnan
*/
public interface PartTreeN1qlQueryCreator {
/** Get the named placeholder values */
JsonValue getPlaceHolderValues();
}

View File

@@ -19,6 +19,7 @@ import static com.couchbase.client.java.query.Select.select;
import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.dsl.Expression;
@@ -41,6 +42,7 @@ import org.springframework.data.repository.query.parser.PartTree;
public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery {
private final PartTree partTree;
private JsonValue placeHolderValues;
public ReactivePartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, RxJavaCouchbaseOperations operations) {
super(queryMethod, operations);
@@ -49,7 +51,7 @@ public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQue
@Override
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
return JsonArray.empty();
return this.placeHolderValues;
}
@Override
@@ -68,6 +70,7 @@ public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQue
N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom,
getCouchbaseOperations().getConverter(), getQueryMethod());
LimitPath selectFromWhereOrderBy = queryCreator.createQuery();
this.placeHolderValues = queryCreator.getPlaceHolderValues();
if (partTree.isLimiting()) {
return selectFromWhereOrderBy.limit(partTree.getMaxResults());
} else {

View File

@@ -18,9 +18,13 @@ package org.springframework.data.couchbase.repository.query.support;
import static com.couchbase.client.java.query.dsl.Expression.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import com.couchbase.client.java.document.json.JsonObject;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.repository.query.ConvertingIterator;
@@ -38,7 +42,7 @@ import com.couchbase.client.java.query.dsl.functions.StringFunctions;
* @author Subhashni Balakrishnan
*/
public class N1qlQueryCreatorUtils {
public static Expression prepareExpression(CouchbaseConverter converter, Part part, Iterator<Object> iterator) {
public static Expression prepareExpression(CouchbaseConverter converter, Part part, Iterator<Object> iterator, AtomicInteger position, JsonArray placeHolderValues) {
PersistentPropertyPath<CouchbasePersistentProperty> path = N1qlUtils.getPathWithAlternativeFieldNames(
converter, part.getProperty());
ConvertingIterator parameterValues = new ConvertingIterator(iterator, converter);
@@ -59,68 +63,116 @@ public class N1qlQueryCreatorUtils {
ignoreCase = true;
}
return createExpression(part.getType(), fieldNamePath, ignoreCase, parameterValues);
return createExpression(part.getType(), fieldNamePath, ignoreCase, parameterValues, position, placeHolderValues);
}
public static Expression createExpression(Part.Type partType, String fieldNamePath, boolean ignoreCase, Iterator<Object> parameterValues) {
public static Expression createExpression(Part.Type partType, String fieldNamePath, boolean ignoreCase,
Iterator<Object> parameterValues, AtomicInteger position, JsonArray placeHolderValues) {
//create the left hand side of the expression, taking ignoreCase into account
Expression left = ignoreCase ? StringFunctions.lower(x(fieldNamePath)) : x(fieldNamePath);
Expression left = ignoreCase ? StringFunctions.lower(x(fieldNamePath)) : x(fieldNamePath);
Expression exp;
switch (partType) {
case BETWEEN:
return left.between(leftAndRight(parameterValues, ignoreCase));
exp = left.between(x(getPlaceHolder(position, ignoreCase)).and(x(getPlaceHolder(position, ignoreCase))));
placeHolderValues.add(getValue(parameterValues));
placeHolderValues.add(getValue(parameterValues));
break;
case IS_NOT_NULL:
return left.isNotNull();
exp = left.isNotNull();
break;
case IS_NULL:
return left.isNull();
exp = left.isNull();
break;
case NEGATING_SIMPLE_PROPERTY:
return left.ne(right(parameterValues, ignoreCase));
exp = left.ne(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case SIMPLE_PROPERTY:
return left.eq(right(parameterValues, ignoreCase));
exp = left.eq(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case BEFORE:
case LESS_THAN:
return left.lt(right(parameterValues, ignoreCase));
exp = left.lt(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case LESS_THAN_EQUAL:
return left.lte(right(parameterValues, ignoreCase));
exp = left.lte(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case GREATER_THAN_EQUAL:
return left.gte(right(parameterValues, ignoreCase));
exp = left.gte(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case AFTER:
case GREATER_THAN:
return left.gt(right(parameterValues, ignoreCase));
exp = left.gt(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case NOT_LIKE:
return left.notLike(right(parameterValues, ignoreCase));
exp = left.notLike(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case LIKE:
return left.like(right(parameterValues, ignoreCase));
exp = left.like(getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case STARTING_WITH:
return left.like(like(parameterValues, ignoreCase, false, true));
exp = left.like(getPlaceHolder(position, ignoreCase) + " || '%'");
placeHolderValues.add(getValue(parameterValues));
break;
case ENDING_WITH:
return left.like(like(parameterValues, ignoreCase, true, false));
exp = left.like("'%' || " + getPlaceHolder(position, ignoreCase));
placeHolderValues.add(getValue(parameterValues));
break;
case NOT_CONTAINING:
return left.notLike(like(parameterValues, ignoreCase, true, true));
exp = left.notLike("'%' || " + getPlaceHolder(position, ignoreCase) + " || '%'");
placeHolderValues.add(getValue(parameterValues));
break;
case CONTAINING:
return left.like(like(parameterValues, ignoreCase, true, true));
exp = left.like("'%' || " + getPlaceHolder(position, ignoreCase) + " || '%'");
placeHolderValues.add(getValue(parameterValues));
break;
case NOT_IN:
return left.notIn(rightArray(parameterValues));
exp = left.notIn(getPlaceHolder(position, false));
placeHolderValues.add(getArray(parameterValues));
break;
case IN:
return left.in(rightArray(parameterValues));
exp = left.in(getPlaceHolder(position, false));
placeHolderValues.add(getArray(parameterValues));
break;
case TRUE:
return left.eq(true);
exp = left.eq(true);
break;
case FALSE:
return left.eq(false);
exp = left.eq(false);
break;
case REGEX:
return regexp(fieldNamePath, parameterValues);
exp = x("REGEXP_LIKE(" + left.toString() + ", " + getPlaceHolder(position, false) + ")");
placeHolderValues.add(getValueAsString(parameterValues));
break;
case EXISTS:
return left.isNotMissing();
exp = left.isNotMissing();
break;
case WITHIN:
case NEAR:
default:
throw new IllegalArgumentException("Unsupported keyword in N1QL query derivation");
}
return exp;
}
protected static String getPlaceHolder(AtomicInteger position, boolean ignoreCase) {
String placeHolder = "$" + position.getAndIncrement();
if (ignoreCase) {
placeHolder = StringFunctions.lower(x(placeHolder)).toString();
}
return placeHolder;
}
protected static Expression regexp(String left, Iterator<Object> parameterValues) {
protected static String getValueAsString(Iterator<Object> parameterValues) {
Object next = parameterValues.next();
String pattern;
@@ -129,11 +181,7 @@ public class N1qlQueryCreatorUtils {
} else {
pattern = String.valueOf(next);
}
return PatternMatchingFunctions.regexpLike(left, pattern);
}
protected static Expression leftAndRight(Iterator<Object> parameterValues, boolean ignoreCase) {
return right(parameterValues, ignoreCase).and(right(parameterValues, ignoreCase));
return pattern;
}
protected static Expression like(Iterator<Object> parameterValues, boolean ignoreCase,
@@ -164,28 +212,15 @@ public class N1qlQueryCreatorUtils {
return converted;
}
protected static Expression right(Iterator<Object> parameterValues, boolean ignoreCase) {
protected static Object getValue(Iterator<Object> parameterValues) {
Object next = parameterValues.next();
if (next == null) {
return Expression.NULL();
if (next instanceof Enum) {
next = String.valueOf(next);
}
Expression converted;
if (next instanceof String) {
converted = s((String) next);
} else if (next instanceof Enum) {
converted = s(String.valueOf(next));
} else {
converted = x(String.valueOf(next));
}
if (ignoreCase) {
return StringFunctions.lower(converted);
}
return converted;
return next;
}
protected static JsonArray rightArray(Iterator<Object> parameterValues) {
protected static JsonArray getArray(Iterator<Object> parameterValues) {
Object next = parameterValues.next();
Object[] values;
@@ -198,4 +233,4 @@ public class N1qlQueryCreatorUtils {
}
return JsonArray.from(values);
}
}
}