DATACOUCH - 534 Add support for Query annotation

This commit is contained in:
mikereiche
2020-05-10 01:50:13 -07:00
parent 3f3bd9d1a8
commit 749c0a84e8
26 changed files with 1315 additions and 254 deletions

View File

@@ -50,8 +50,8 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu
this.template = template;
this.domainType = domainType;
this.query = query;
this.reactiveSupport = new ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport<>(template.reactive(),
domainType, query, scanConsistency);
this.reactiveSupport = new ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport<T>(
template.reactive(), domainType, query, scanConsistency);
this.scanConsistency = scanConsistency;
}

View File

@@ -15,16 +15,19 @@
*/
package org.springframework.data.couchbase.core;
import org.springframework.data.couchbase.core.support.TemplateUtils;
import org.springframework.data.couchbase.core.query.Query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
import com.couchbase.client.java.query.ReactiveQueryResult;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryOperation {
private static final Query ALL_QUERY = new Query();
@@ -47,8 +50,8 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
private final Query query;
private final QueryScanConsistency scanConsistency;
ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class<T> domainType, final Query query,
final QueryScanConsistency scanConsistency) {
ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class<T> domainType,
final Query query, final QueryScanConsistency scanConsistency) {
this.template = template;
this.domainType = domainType;
this.query = query;
@@ -79,20 +82,20 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
public Flux<T> all() {
return Flux.defer(() -> {
String statement = assembleEntityQuery(false);
return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions())
.onErrorMap(throwable -> {
if (throwable instanceof RuntimeException) {
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
} else {
return throwable;
}
}).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> {
String id = row.getString("__id");
long cas = row.getLong("__cas");
row.removeKey("__id");
row.removeKey("__cas");
return template.support().decodeEntity(id, row.toString(), cas, domainType);
});
return template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
query.buildQueryOptions(scanConsistency)).onErrorMap(throwable -> {
if (throwable instanceof RuntimeException) {
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
} else {
return throwable;
}
}).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> {
String id = row.getString(TemplateUtils.SELECT_ID);
long cas = row.getLong(TemplateUtils.SELECT_CAS);
row.removeKey(TemplateUtils.SELECT_ID);
row.removeKey(TemplateUtils.SELECT_CAS);
return template.support().decodeEntity(id, row.toString(), cas, domainType);
});
});
}
@@ -100,14 +103,15 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
public Mono<Long> count() {
return Mono.defer(() -> {
String statement = assembleEntityQuery(true);
return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions())
.onErrorMap(throwable -> {
if (throwable instanceof RuntimeException) {
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
} else {
return throwable;
}
}).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong("__count")).next();
return template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
query.buildQueryOptions(scanConsistency)).onErrorMap(throwable -> {
if (throwable instanceof RuntimeException) {
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
} else {
return throwable;
}
}).flatMapMany(ReactiveQueryResult::rowsAsObject).map(
row -> row.getLong(TemplateUtils.SELECT_COUNT)).next();
});
}
@@ -117,35 +121,9 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
}
private String assembleEntityQuery(final boolean count) {
final String bucket = "`" + template.getBucketName() + "`";
final StringBuilder statement = new StringBuilder("SELECT ");
if (count) {
statement.append("count(*) as __count");
} else {
statement.append("meta().id as __id, meta().cas as __cas, ").append(bucket).append(".*");
}
statement.append(" FROM ").append(bucket);
String typeKey = template.getConverter().getTypeKey();
String typeValue = template.support().getJavaNameForEntity(domainType);
query.addCriteria(QueryCriteria.where(typeKey).is(typeValue));
query.appendWhere(statement);
query.appendSort(statement);
query.appendSkipAndLimit(statement);
return statement.toString();
return query.toN1qlString(template, this.domainType, count);
}
private QueryOptions buildQueryOptions() {
final QueryOptions options = QueryOptions.queryOptions();
if (scanConsistency != null) {
options.scanConsistency(scanConsistency);
}
return options;
}
}
}

View File

@@ -17,19 +17,38 @@ package org.springframework.data.couchbase.core.query;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser;
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class Query {
private final List<QueryCriteria> criteria = new ArrayList<>();
private JsonValue parameters = JsonValue.ja();
private long skip;
private int limit;
private Sort sort = Sort.unsorted();
public Query() {}
static private final Pattern WHERE_PATTERN = Pattern.compile("\\sWHERE\\s");
public Query() {
}
public Query(final QueryCriteria criteriaDefinition) {
addCriteria(criteriaDefinition);
@@ -40,6 +59,34 @@ public class Query {
return this;
}
/**
* set the postional parameters on the query object
* There can only be named parameters or positional parameters - not both.
*
* @param parameters - the positional parameters
* @return - the query
*/
public Query setPositionalParameters(JsonArray parameters) {
this.parameters = parameters;
return this;
}
/**
* set the named parameters on the query object
* There can only be named parameters or positional parameters - not both.
*
* @param parameters - the named parameters
* @return - the query
*/
public Query setNamedParameters(JsonObject parameters) {
this.parameters = parameters;
return this;
}
JsonValue getParameters() {
return parameters;
}
/**
* Set number of documents to skip before returning results.
*
@@ -110,33 +157,133 @@ public class Query {
sb.append(" ORDER BY ");
sort.stream().forEach(order -> {
if (order.isIgnoreCase()) {
throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! "
+ "Couchbase N1QL does not support sorting ignoring case currently!", order.getProperty()));
throw new IllegalArgumentException(String.format(
"Given sort contained an Order for %s with ignore case! "
+ "Couchbase N1QL does not support sorting ignoring case currently!",
order.getProperty()));
}
sb.append(order.getProperty()).append(" ").append(order.isAscending() ? "ASC," : "DESC,");
});
sb.deleteCharAt(sb.length() - 1);
}
public void appendWhere(final StringBuilder sb) {
sb.append(" WHERE ");
boolean first = true;
for (QueryCriteria c : criteria) {
if (first) {
first = false;
} else {
sb.append(" AND ");
public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) {
if (!criteria.isEmpty()) {
appendWhereOrAnd(sb);
boolean first = true;
for (QueryCriteria c : criteria) {
if (first) {
first = false;
} else {
sb.append(" AND ");
}
sb.append(c.export(paramIndexPtr));
}
sb.append(c.export());
}
}
public void appendCriteria(StringBuilder sb, QueryCriteria criteria) {
appendWhereOrAnd(sb);
sb.append(criteria.export());
}
public void appendWhereString(StringBuilder sb, String whereString) {
appendWhereOrAnd(sb);
sb.append(whereString);
}
public void appendString(StringBuilder sb, String whereString) {
sb.append(whereString);
}
private void appendWhereOrAnd(StringBuilder sb) {
String querySoFar = sb.toString().toUpperCase();
Matcher whereMatcher = WHERE_PATTERN.matcher(querySoFar);
boolean alreadyWhere = false;
while (!alreadyWhere && whereMatcher.find()) {
if (notQuoted(whereMatcher.start(), whereMatcher.end(), querySoFar)) {
alreadyWhere = true;
}
}
if (alreadyWhere) {
sb.append(" AND ");
} else {
sb.append(" WHERE ");
}
}
/**
* ensure that the WHERE we found was not quoted
*
* @param start
* @param end
* @param querySoFar
* @return true -> not quoted, false -> quoted
*/
private static boolean notQuoted(int start, int end, String querySoFar) {
Matcher quoteMatcher = StringBasedN1qlQueryParser.QUOTE_DETECTION_PATTERN.matcher(querySoFar);
List<int[]> quotes = new ArrayList<int[]>();
while (quoteMatcher.find()) {
quotes.add(new int[] { quoteMatcher.start(), quoteMatcher.end() });
}
for (int[] quote : quotes) {
if (quote[0] <= start && quote[1] >= end) {
return false; // it is quoted
}
}
return true; // is not quoted
}
public String export() {
StringBuilder sb = new StringBuilder();
appendWhere(sb);
appendWhere(sb, null);
appendSort(sb);
appendSkipAndLimit(sb);
return sb.toString();
}
public String toN1qlString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, domainClass, isCount);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.selectEntity); // select ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
appendWhere(statement, null); // criteria on this Query
appendSort(statement);
appendSkipAndLimit(statement);
return statement.toString();
}
StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, Class domainClass,
boolean isCount) {
String typeKey = template.getConverter().getTypeKey();
final CouchbasePersistentEntity<?> persistentEntity = template.getConverter().getMappingContext().getRequiredPersistentEntity(
domainClass);
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
String typeValue = info.getJavaType().getName();
return StringBasedN1qlQueryParser.createN1qlSpelValues(template.getBucketName(), typeKey, typeValue, isCount);
}
/**
* build QueryOptions forom parameters and scanConsistency
*
* @param scanConsistency
* @return QueryOptions
*/
public QueryOptions buildQueryOptions(QueryScanConsistency scanConsistency) {
final QueryOptions options = QueryOptions.queryOptions();
if (getParameters() != null) {
if (getParameters() instanceof JsonArray) {
options.parameters((JsonArray) getParameters());
} else {
options.parameters((JsonObject) getParameters());
}
}
if (scanConsistency != null) {
options.scanConsistency(scanConsistency);
}
return options;
}
}

View File

@@ -22,6 +22,10 @@ import java.util.List;
import org.springframework.lang.Nullable;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class QueryCriteria implements QueryCriteriaDefinition {
private final String key;
@@ -126,29 +130,23 @@ public class QueryCriteria implements QueryCriteriaDefinition {
}
public QueryCriteria startingWith(@Nullable Object o) {
if (o instanceof QueryCriteria) {
QueryCriteria term = where("\"%\"").plus(o);
like(term);
} else {
like(o.toString() + "%");
}
operator = "STARTING_WITH";
value = new Object[] { o };
format = "%1$s like (%3$s||\"%%\")";
return this;
}
public QueryCriteria plus(@Nullable Object o) {
operator = "PLUS";
value = new Object[] { o };
format = "(%1$s + %3$s)";
format = "(%1$s || %3$s)";
return this;
}
public QueryCriteria endingWith(@Nullable Object o) {
if (o instanceof QueryCriteria) {
QueryCriteria term = where("%").plus(o);
like(term);
} else {
like("%" + o.toString());
}
operator = "ENDING_WITH";
value = new Object[] { o };
format = "%1$s like (\"%%\"||%3$s)";
return this;
}
@@ -181,10 +179,10 @@ public class QueryCriteria implements QueryCriteriaDefinition {
}
public QueryCriteria notLike(@Nullable Object o) {
value = new QueryCriteria[] { wrap(like(o)) };
operator = "NOT";
format = format = "not( %3$s )";
return (QueryCriteria) this;
operator = "NOTLIKE";
value = new Object[] { o };
format = "not(%1$s like %3$s)";
return this;
}
public QueryCriteria isNull() {
@@ -277,8 +275,17 @@ public class QueryCriteria implements QueryCriteriaDefinition {
return (QueryCriteria) this;
}
/**
* This exports the query criteria into a string to be appended to the beginning of an N1QL statement
*
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters
* There may already be positional parameters in the beginning of the statement,
* so it may not always start at 1. If it has the value -1, the query is using
* named parameters. If the pointer is null, the query is not using parameters.
* @return string containing part of N1QL query
*/
@Override
public String export() {
public String export(int[] paramIndexPtr) {
StringBuilder output = new StringBuilder();
boolean first = true;
for (QueryCriteria c : this.criteriaChain) {
@@ -286,18 +293,29 @@ public class QueryCriteria implements QueryCriteriaDefinition {
if (c.chainOperator == null) {
throw new IllegalStateException("A chain operator must be present when chaining!");
}
// the consistent place to output this would be in the c.toQueryString(output) about five lines down
// the consistent place to output this would be in the c.exportSingle(output) about five lines down
output.append(" ").append(c.chainOperator.representation).append(" ");
} else {
first = false;
}
c.exportSingle(output);
c.exportSingle(output, paramIndexPtr);
}
return output.toString();
}
private StringBuilder exportSingle(StringBuilder sb) {
/**
* Export the query criteria to a string without using positional or named parameters.
*
* @return string containing part of N1QL query
*/
@Override
public String export() {
return export(null);
}
private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) {
String fieldName = maybeQuote(key);
int valueLen = value == null ? 0 : value.length;
Object[] v = new Object[valueLen + 2];
@@ -305,9 +323,9 @@ public class QueryCriteria implements QueryCriteriaDefinition {
v[1] = operator;
for (int i = 0; i < valueLen; i++) {
if (value[i] instanceof QueryCriteria) {
v[i + 2] = "(" + ((QueryCriteria) value[i]).export() + ")";
v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr) + ")";
} else {
v[i + 2] = maybeWrapValue(value[i]);
v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr);
}
}
@@ -322,7 +340,15 @@ public class QueryCriteria implements QueryCriteriaDefinition {
return sb;
}
private String maybeWrapValue(Object value) {
private String maybeWrapValue(String key, Object value, int[] paramIndexPtr) {
if (paramIndexPtr != null) {
if (paramIndexPtr[0] >= 0) {
return "$" + (++paramIndexPtr[0]); // these are generated in order
} else {
return "$" + key;
}
}
if (value instanceof String) {
return "\"" + value + "\"";
} else if (value == null) {

View File

@@ -18,8 +18,25 @@ package org.springframework.data.couchbase.core.query;
/**
* @author Oliver Gierke
* @author Christoph Strobl
* @author Michael Reiche
*/
public interface QueryCriteriaDefinition {
/**
* This exports the query criteria into a string to be appended to the beginning of an N1QL statement
*
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters
* There may already be positional parameters in the beginning of the statement,
* so it may not always start at 1. If it has the value -1, the query is using
* named parameters. If the pointer is null, the query is not using parameters.
* @return string containing part of N1QL query
*/
String export(int[] paramIndexPtr);
/**
* Export the query criteria to a string without using positional or named parameters.
*
* @return string containing part of N1QL query
*/
String export();
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2017-2020 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
*
* https://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.core.query;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser;
/**
* @author Michael Reiche
*
* Query created from the string in @Query annotation in the repository interface.
*
* @Query("#{#n1ql.selectEntity} where firstname = $1 and lastname = $2")
* List<User> getByFirstnameAndLastname(String firstname, String lastname);
*
* It must include the SELECT ... FROM ... preferably via the #n1ql expression
* In addition to any predicates in the string, a predicate for the domainType (class)
* will be added.
*/
public class StringQuery extends Query {
private String inlineN1qlQuery;
public StringQuery(String n1qlString) {
inlineN1qlQuery = n1qlString;
}
/**
* inlineN1qlQuery (Query Annotation)
* append the string query to the provided StringBuilder.
* To be used along with the other append*() methods to construct the N1QL statement
* @param sb - StringBuilder
*/
private void appendInlineN1qlStatement(final StringBuilder sb) {
sb.append(inlineN1qlQuery);
}
@Override
public String toN1qlString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, domainClass, isCount);
final StringBuilder statement = new StringBuilder();
appendInlineN1qlStatement(statement); // apply the string statement
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
// To use generated parameters for literals
// we need to figure out if we must use positional or named parameters
// If we are using positional parameters, we need to start where
// the inlineN1ql left off.
int[] paramIndexPtr = null;
JsonValue params = this.getParameters();
if (params instanceof JsonArray) { // positional parameters
paramIndexPtr = new int[] { ((JsonArray) params).size() };
} else { // named parameters or no parameters, no index required
paramIndexPtr = new int[] { -1 };
}
appendWhere(statement, paramIndexPtr); // criteria on this Query - should be empty for StringQuery
appendSort(statement);
appendSkipAndLimit(statement);
return statement.toString();
}
}

View File

@@ -25,11 +25,13 @@ import org.springframework.data.couchbase.core.OperationInterruptedException;
/**
* @author Subhashni Balakrishnan
* @author Michael Reiche
* @since 3.0
*/
public class TemplateUtils {
public static final String SELECT_ID = "_ID";
public static final String SELECT_CAS = "_CAS";
public static final String SELECT_ID = "__id";
public static final String SELECT_CAS = "__cas";
public static final String SELECT_COUNT = "__count";
private static PersistenceExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator();
public static Throwable translateError(Throwable e) {

View File

@@ -16,22 +16,30 @@
package org.springframework.data.couchbase.repository.query;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class CouchbaseRepositoryQuery implements RepositoryQuery {
private final CouchbaseOperations operations;
private final QueryMethod queryMethod;
private final CouchbaseQueryMethod queryMethod;
private final NamedQueries namedQueries;
public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final QueryMethod queryMethod) {
public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod,
final NamedQueries namedQueries) {
this.operations = operations;
this.queryMethod = queryMethod;
this.namedQueries = namedQueries;
}
@Override
public Object execute(final Object[] parameters) {
return new N1qlRepositoryQueryExecutor(operations, queryMethod).execute(parameters);
return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters);
}
@Override

View File

@@ -17,34 +17,50 @@ package org.springframework.data.couchbase.repository.query;
import static org.springframework.data.couchbase.core.query.QueryCriteria.*;
import java.lang.reflect.Array;
import java.util.Iterator;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria> {
private final ParameterAccessor accessor;
private final MappingContext<?, CouchbasePersistentProperty> context;
private final QueryMethod queryMethod;
private final CouchbaseConverter converter;
public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor,
final MappingContext<?, CouchbasePersistentProperty> context) {
public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, QueryMethod queryMethod,
CouchbaseConverter converter) {
super(tree, accessor);
this.accessor = accessor;
this.context = context;
this.context = converter.getMappingContext();
this.queryMethod = queryMethod;
this.converter = converter;
}
@Override
protected QueryCriteria create(final Part part, final Iterator<Object> iterator) {
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(
part.getProperty());
CouchbasePersistentProperty property = path.getLeafProperty();
return from(part, property, where(path.toDotPath()), iterator);
}
@@ -55,7 +71,8 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
return create(part, iterator);
}
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(
part.getProperty());
CouchbasePersistentProperty property = path.getLeafProperty();
return from(part, property, base.and(path.toDotPath()), iterator);
@@ -68,71 +85,92 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
@Override
protected Query complete(QueryCriteria criteria, Sort sort) {
return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort);
JsonArray params = (JsonArray) getPositionalPlaceholderValues(accessor);
return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort).setPositionalParameters(
params);
}
private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria,
final Iterator<Object> parameters) {
private QueryCriteria from(final Part part, final CouchbasePersistentProperty property,
final QueryCriteria criteria, final Iterator<Object> parameters) {
final Part.Type type = part.getType();
/*
NEAR(new String[]{"IsNear", "Near"}),
WITHIN(new String[]{"IsWithin", "Within"}),
*/
switch (type) {
case GREATER_THAN:
case AFTER:
return criteria.gt(parameters.next());
case GREATER_THAN_EQUAL:
return criteria.gte(parameters.next());
case LESS_THAN:
case BEFORE:
return criteria.lt(parameters.next());
case LESS_THAN_EQUAL:
return criteria.lte(parameters.next());
case SIMPLE_PROPERTY:
return criteria.eq(parameters.next());
case NEGATING_SIMPLE_PROPERTY:
return criteria.ne(parameters.next());
case CONTAINING:
return criteria.containing(parameters.next());
case NOT_CONTAINING:
return criteria.notContaining(parameters.next());
case STARTING_WITH:
return criteria.startingWith(parameters.next());
case ENDING_WITH:
return criteria.endingWith(parameters.next());
case LIKE:
return criteria.like(parameters.next());
case NOT_LIKE:
return criteria.notLike(parameters.next());
case WITHIN:
return criteria.within(parameters.next());
case IS_NULL:
return criteria.isNull(/*parameters.next()*/);
case IS_NOT_NULL:
return criteria.isNotNull(/*parameters.next()*/);
case IS_EMPTY:
return criteria.isNotValued(/*parameters.next()*/);
case IS_NOT_EMPTY:
return criteria.isValued(/*parameters.next()*/);
case EXISTS:
return criteria.isNotMissing(/*parameters.next()*/);
case REGEX:
return criteria.regex(parameters.next());
case BETWEEN:
return criteria.between(parameters.next(), parameters.next());
case IN:
return criteria.in((Object[]) parameters.next());
case NOT_IN:
return criteria.notIn((Object[]) parameters.next());
case TRUE:
return criteria.TRUE();
case FALSE:
return criteria.FALSE();
default:
throw new IllegalArgumentException("Unsupported keyword!");
case GREATER_THAN:
case AFTER:
return criteria.gt(parameters.next());
case GREATER_THAN_EQUAL:
return criteria.gte(parameters.next());
case LESS_THAN:
case BEFORE:
return criteria.lt(parameters.next());
case LESS_THAN_EQUAL:
return criteria.lte(parameters.next());
case SIMPLE_PROPERTY:
return criteria.eq(parameters.next());
case NEGATING_SIMPLE_PROPERTY:
return criteria.ne(parameters.next());
case CONTAINING:
return criteria.containing(parameters.next());
case NOT_CONTAINING:
return criteria.notContaining(parameters.next());
case STARTING_WITH:
return criteria.startingWith(parameters.next());
case ENDING_WITH:
return criteria.endingWith(parameters.next());
case LIKE:
return criteria.like(parameters.next());
case NOT_LIKE:
return criteria.notLike(parameters.next());
case WITHIN:
return criteria.within(parameters.next());
case IS_NULL:
return criteria.isNull(/*parameters.next()*/);
case IS_NOT_NULL:
return criteria.isNotNull(/*parameters.next()*/);
case IS_EMPTY:
return criteria.isNotValued(/*parameters.next()*/);
case IS_NOT_EMPTY:
return criteria.isValued(/*parameters.next()*/);
case EXISTS:
return criteria.isNotMissing(/*parameters.next()*/);
case REGEX:
return criteria.regex(parameters.next());
case BETWEEN:
return criteria.between(parameters.next(), parameters.next());
case IN:
return criteria.in((Object[]) parameters.next());
case NOT_IN:
return criteria.notIn((Object[]) parameters.next());
case TRUE:
return criteria.TRUE();
case FALSE:
return criteria.FALSE();
default:
throw new IllegalArgumentException("Unsupported keyword!");
}
}
// from StringN1qlQueryParser
private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) {
JsonArray posValues = JsonArray.create();
if (queryMethod == null)
return posValues;
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
try {
posValues.add(converter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex())));
} catch (InvalidArgumentException iae) {
Object o = accessor.getBindableValue(parameter.getIndex());
if (o instanceof Object[]) {
Object[] array = (Object[]) o;
for (Object e : array) {
posValues.add(converter.convertForWriteIfNeeded(e));
}
}
}
}
return posValues;
}
}

View File

@@ -15,33 +15,64 @@
*/
package org.springframework.data.couchbase.repository.query;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.repository.core.NamedQueries;
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.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.parser.PartTree;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class N1qlRepositoryQueryExecutor {
private final CouchbaseOperations operations;
private final QueryMethod queryMethod;
private final CouchbaseQueryMethod queryMethod;
private final NamedQueries namedQueries;
public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final QueryMethod queryMethod) {
public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod,
final NamedQueries namedQueries) {
this.operations = operations;
this.queryMethod = queryMethod;
this.namedQueries = namedQueries;
}
/**
* see also {@link ReactiveN1qlRepositoryQueryExecutor#execute(Object[] parameters) execute }
*
* @param parameters
* @return
*/
public Object execute(final Object[] parameters) {
final Class<?> domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType();
final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
final PartTree tree = new PartTree(queryMethod.getName(), domainClass);
Query query = new N1qlQueryCreator(tree, accessor, operations.getConverter().getMappingContext()).createQuery();
// this is identical to ReactiveN1qlRespositoryQueryExecutor,
// except for the type of 'q', and the call to oneValue() vs one()
Query query;
ExecutableFindByQueryOperation.ExecutableFindByQuery q;
if (queryMethod.hasN1qlAnnotation()) {
query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(),
operations.getBucketName(), QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries).createQuery();
} else {
final PartTree tree = new PartTree(queryMethod.getName(), domainClass);
query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery();
}
q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations.findByQuery(domainClass).matching(query);
if (queryMethod.isCollectionQuery()) {
return q.all();
} else {
return q.oneValue();
}
List<?> all = operations.findByQuery(domainClass).matching(query).all();
return all;
}
}

View File

@@ -16,22 +16,30 @@
package org.springframework.data.couchbase.repository.query;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class ReactiveCouchbaseRepositoryQuery implements RepositoryQuery {
private final ReactiveCouchbaseOperations operations;
private final QueryMethod queryMethod;
private final CouchbaseQueryMethod queryMethod;
private final NamedQueries namedQueries;
public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations, final QueryMethod queryMethod) {
public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations,
final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) {
this.operations = operations;
this.queryMethod = queryMethod;
this.namedQueries = namedQueries;
}
@Override
public Object execute(final Object[] parameters) {
return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod).execute(parameters);
return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters);
}
@Override

View File

@@ -15,35 +15,67 @@
*/
package org.springframework.data.couchbase.repository.query;
import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation;
import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.query.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import reactor.core.publisher.Flux;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.couchbase.core.query.Query;
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.parser.PartTree;
import java.util.List;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class ReactiveN1qlRepositoryQueryExecutor {
private final ReactiveCouchbaseOperations operations;
private final QueryMethod queryMethod;
private final CouchbaseQueryMethod queryMethod;
private final NamedQueries namedQueries;
public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations operations,
final QueryMethod queryMethod) {
final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) {
this.operations = operations;
this.queryMethod = queryMethod;
this.namedQueries = namedQueries;
}
/**
* see also {@link N1qlRepositoryQueryExecutor#execute(Object[] parameters) execute }
*
* @param parameters
* @return
*/
public Object execute(final Object[] parameters) {
final Class<?> domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType();
final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
final String namedQueryName = queryMethod.getNamedQueryName();
final PartTree tree = new PartTree(queryMethod.getName(), domainClass);
Query query = new N1qlQueryCreator(tree, accessor, operations.getConverter().getMappingContext()).createQuery();
// this is identical to ExecutableN1qlRespositoryQueryExecutor,
// except for the type of 'q', and the call to one() vs oneValue()
Flux<?> all = operations.findByQuery(domainClass).matching(query).all();
return all;
Query query;
ReactiveFindByQueryOperation.ReactiveFindByQuery q;
if (queryMethod.hasN1qlAnnotation()) {
query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(),
operations.getBucketName(), QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries).createQuery();
} else {
final PartTree tree = new PartTree(queryMethod.getName(), domainClass);
query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery();
}
q = (ReactiveFindByQueryOperation.ReactiveFindByQuery) operations.findByQuery(domainClass).matching(query);
if (queryMethod.isCollectionQuery()) {
return q.all();
} else {
return q.one();
}
}
}

View File

@@ -61,8 +61,8 @@ public class ReactiveStringN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery
return getCouchbaseOperations().getConverter().getTypeKey();
}
protected Class<?> getTypeValue() {
return getQueryMethod().getEntityInformation().getJavaType();
protected String getTypeValue() {
return getQueryMethod().getEntityInformation().getJavaType().getName();
}
@Override

View File

@@ -19,10 +19,13 @@ import static org.springframework.data.couchbase.core.query.N1QLExpression.*;
import static org.springframework.data.couchbase.core.support.TemplateUtils.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.couchbase.client.core.error.InvalidArgumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
@@ -42,6 +45,7 @@ import com.couchbase.client.java.json.JsonValue;
/**
* @author Subhashni Balakrishnan
* @author Michael Reiche
*/
public class StringBasedN1qlQueryParser {
public static final String SPEL_PREFIX = "n1ql";
@@ -84,11 +88,17 @@ public class StringBasedN1qlQueryParser {
* the statement.
*/
public static final String SPEL_RETURNING = "#" + SPEL_PREFIX + ".returning";
/** regexp that detect $named placeholder (starts with a letter, then alphanum chars) */
/**
* regexp that detect $named placeholder (starts with a letter, then alphanum chars)
*/
public static final Pattern NAMED_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Alpha}\\p{Alnum}*)\\b");
/** regexp that detect positional placeholder ($ followed by digits only) */
/**
* regexp that detect positional placeholder ($ followed by digits only)
*/
public static final Pattern POSITIONAL_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Digit}+)\\b");
/** regexp that detects " and ' quote boundaries, ignoring escaped quotes */
/**
* regexp that detects " and ' quote boundaries, ignoring escaped quotes
*/
public static final Pattern QUOTE_DETECTION_PATTERN = Pattern.compile("[\"'](?:[^\"'\\\\]*(?:\\\\.)?)*[\"']");
private static final Logger LOGGER = LoggerFactory.getLogger(StringBasedN1qlQueryParser.class);
private final String statement;
@@ -97,9 +107,10 @@ public class StringBasedN1qlQueryParser {
private final N1qlSpelValues statementContext;
private final N1qlSpelValues countContext;
private final CouchbaseConverter couchbaseConverter;
private final Collection<String> parameterNames = new HashSet<String>();
public StringBasedN1qlQueryParser(String statement, QueryMethod queryMethod, String bucketName,
CouchbaseConverter couchbaseConverter, String typeField, Class<?> typeValue) {
CouchbaseConverter couchbaseConverter, String typeField, String typeValue) {
this.statement = statement;
this.queryMethod = queryMethod;
this.placeHolderType = checkPlaceholders(statement);
@@ -108,7 +119,7 @@ public class StringBasedN1qlQueryParser {
this.couchbaseConverter = couchbaseConverter;
}
public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, Class<?> typeValue,
public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, String typeValue,
boolean isCount) {
String b = "`" + bucketName + "`";
String entity = "META(" + b + ").id AS " + SELECT_ID + ", META(" + b + ").cas AS " + SELECT_CAS;
@@ -119,7 +130,7 @@ public class StringBasedN1qlQueryParser {
} else {
selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b;
}
String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\"";
String typeSelection = "`" + typeField + "` = \"" + typeValue + "\"";
String delete = N1QLExpression.delete().from(i(bucketName)).toString();
String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(bucketName).toString();
@@ -141,6 +152,7 @@ public class StringBasedN1qlQueryParser {
}
private PlaceholderType checkPlaceholders(String statement) {
Matcher quoteMatcher = QUOTE_DETECTION_PATTERN.matcher(statement);
Matcher positionMatcher = POSITIONAL_PLACEHOLDER_PATTERN.matcher(statement);
Matcher namedMatcher = NAMED_PLACEHOLDER_PATTERN.matcher(statement);
@@ -159,6 +171,7 @@ public class StringBasedN1qlQueryParser {
if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes)) {
LOGGER.trace("{}: Found positional placeholder {}", this.queryMethod.getName(), placeholder);
posCount++;
parameterNames.add(placeholder.substring(1)); // save without the leading $
}
}
@@ -168,13 +181,17 @@ public class StringBasedN1qlQueryParser {
if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) {
LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder);
namedCount++;
parameterNames.add(placeholder.substring(1));//save without the leading $
}
}
if (posCount > 0 && namedCount > 0) {
if (posCount > 0 && namedCount > 0) { // actual values from parameterNames might be more useful
throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount
+ ") placeholders is not supported, please choose one over the other in " + this.queryMethod.getName());
} else if (posCount > 0) {
+ ") placeholders is not supported, please choose one over the other in "
+ queryMethod.getClass().getName() + "." + this.queryMethod.getName() + "()");
}
if (posCount > 0) {
return PlaceholderType.POSITIONAL;
} else if (namedCount > 0) {
return PlaceholderType.NAMED;
@@ -186,50 +203,127 @@ public class StringBasedN1qlQueryParser {
private boolean checkNotQuoted(String item, int start, int end, List<int[]> quotes) {
for (int[] quote : quotes) {
if (quote[0] <= start && quote[1] >= end) {
LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), item);
LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(),
item);
return false;
}
}
return true;
}
/**
* Get Postional argument placeholders to use for parameters. $1, $2 etc.
*
* @param accessor
* @return - JsonValue holding parameters.
*/
private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) {
JsonArray posValues = JsonArray.create();
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
posValues.add(this.couchbaseConverter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex())));
Object rawValue = accessor.getBindableValue(parameter.getIndex());
Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue);
putPositionalValue(accessor, parameter, posValues, value);
}
return posValues;
}
/**
* Get Named argument placeholders to use for parameters. $lastname, $city etc.
*
* @param accessor
* @return
*/
private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) {
JsonObject namedValues = JsonObject.create();
HashSet<String> pNames = new HashSet<>(parameterNames);
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
String placeholder = parameter.getPlaceholder();
Object value = this.couchbaseConverter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex()));
Object rawValue = accessor.getBindableValue(parameter.getIndex());
Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue);
if (placeholder != null && placeholder.charAt(0) == ':') {
placeholder = placeholder.replaceFirst(":", "");
namedValues.put(placeholder, value);
putNamedValue(accessor, parameter, namedValues, placeholder, value);
if (pNames.contains(placeholder)) {
pNames.remove(placeholder);
} else {
throw new RuntimeException(
"parameter named " + placeholder + " does not match any named parameter " + parameterNames
+ " in " + statement);
}
} else {
parameter.getName().ifPresent(name -> namedValues.put(name, value));
if (parameter.getName().isPresent()) {
putNamedValue(accessor, parameter, namedValues, parameter.getName().get(), value);
} else {
throw new RuntimeException(
"cannot determine argument for named parameter. " + "Argument " + parameter.getIndex()
+ " to " + queryMethod.getClass().getName() + "." + queryMethod.getName()
+ "() needs @Param(\"name\") that matches a named parameter in " + statement);
}
}
}
if (!pNames.isEmpty()) {
throw new RuntimeException("no parameter found for " + pNames);
}
return namedValues;
}
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
switch (this.placeHolderType) {
case NAMED:
return getNamedPlaceholderValues(accessor);
case POSITIONAL:
return getPositionalPlaceholderValues(accessor);
case NONE:
default:
return JsonArray.create();
case NAMED:
return getNamedPlaceholderValues(accessor);
case POSITIONAL:
return getPositionalPlaceholderValues(accessor);
case NONE:
default:
return JsonArray.create();
}
}
private void putPositionalValue(ParameterAccessor accessor, Parameter parameter, JsonArray posValues,
Object value) {
try {
posValues.add(value);
} catch (InvalidArgumentException iae) {
if (value instanceof Object[]) { // Maybe just need to treat it as an array ?
addAsArray(posValues, value);
} else {
throw iae;
}
}
}
private void addAsArray(JsonArray posValues, Object o) {
Object[] array = (Object[]) o;
JsonArray ja = JsonValue.ja();
for (Object e : array) {
ja.add(String.valueOf(couchbaseConverter.convertForWriteIfNeeded(e)));
}
posValues.add(ja);
}
private void putNamedValue(ParameterAccessor accessor, Parameter parameter, JsonObject namedValues,
String placeholder, Object value) {
try {
namedValues.put(placeholder, value);
} catch (InvalidArgumentException iae) {
if (value instanceof Object[]) { // Maybe just need to treat it as an array?
addAsArray(namedValues, placeholder, value);
} else {
throw iae;
}
}
}
private void addAsArray(JsonObject namedValues, String placeholder, Object o) {
Object[] array = (Object[]) o;
JsonArray ja = JsonValue.ja();
for (Object e : array) {
ja.add(String.valueOf(couchbaseConverter.convertForWriteIfNeeded(e)));
}
namedValues.put(placeholder, ja);
}
protected boolean useGeneratedCountQuery() {
return this.statement.contains(SPEL_SELECT_FROM_CLAUSE);
}
@@ -246,7 +340,9 @@ public class StringBasedN1qlQueryParser {
return this.statement;
}
/** enumeration of all the combinations of placeholder types that could be found in a N1QL statement */
/**
* enumeration of all the combinations of placeholder types that could be found in a N1QL statement
*/
private enum PlaceholderType {
NAMED, POSITIONAL, NONE
}
@@ -306,4 +402,5 @@ public class StringBasedN1qlQueryParser {
this.returning = returning;
}
}
}

View File

@@ -60,7 +60,7 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, couchbaseOperations);
this.queryParser = new StringBasedN1qlQueryParser(statement, queryMethod, getCouchbaseOperations().getBucketName(),
getCouchbaseOperations().getConverter(), getTypeField(), getTypeValue());
getCouchbaseOperations().getConverter(), getTypeField(), getTypeValue().getName());
this.parser = spelParser;
this.evaluationContextProvider = evaluationContextProvider;
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2020 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
*
* https://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 org.springframework.data.couchbase.core.query.N1QLExpression.x;
import static org.springframework.data.couchbase.core.query.QueryCriteria.*;
import java.util.ArrayList;
import java.util.Iterator;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.N1QLExpression;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.core.query.StringQuery;
import org.springframework.data.couchbase.repository.query.support.N1qlUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.query.*;
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.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
* @author Michael Reiche
*/
public class StringN1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria> {
private final ParameterAccessor accessor;
private final MappingContext<?, CouchbasePersistentProperty> context;
private final SpelExpressionParser parser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final StringBasedN1qlQueryParser queryParser;
private final QueryMethod queryMethod;
private final CouchbaseConverter couchbaseConverter;
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
public StringN1qlQueryCreator(final ParameterAccessor accessor, CouchbaseQueryMethod queryMethod,
CouchbaseConverter couchbaseConverter, String bucketName,
QueryMethodEvaluationContextProvider evaluationContextProvider, NamedQueries namedQueries) {
// AbstractQueryCreator needs a PartTree, so we give it a dummy one.
// The resulting dummy criteria will not be included in the Query
// by {@link #complete((QueryCriteria criteria, Sort sort)) complete}
super(new PartTree("dummy", (new Object() {
String dummy;
}).getClass()), accessor);
this.accessor = accessor;
this.context = couchbaseConverter.getMappingContext();
this.queryMethod = queryMethod;
this.couchbaseConverter = couchbaseConverter;
this.evaluationContextProvider = evaluationContextProvider;
final String namedQueryName = queryMethod.getNamedQueryName();
String queryString;
if (queryMethod.hasInlineN1qlQuery()) {
queryString = queryMethod.getInlineN1qlQuery();
} else if (namedQueries.hasQuery(namedQueryName)) {
queryString = namedQueries.getQuery(namedQueryName);
} else {
throw new IllegalArgumentException("query has no inline Query or named Query not found");
}
this.queryParser = new StringBasedN1qlQueryParser(queryString, queryMethod, bucketName, couchbaseConverter,
getTypeField(), getTypeValue());
this.parser = SPEL_PARSER;
}
protected QueryMethod getQueryMethod() {
return queryMethod;
}
protected String getTypeField() {
return couchbaseConverter.getTypeKey();
}
protected String getTypeValue() {
return getQueryMethod().getEntityInformation().getJavaType().getName();
}
@Override
protected QueryCriteria create(final Part part, final Iterator<Object> iterator) {
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(
part.getProperty());
CouchbasePersistentProperty property = path.getLeafProperty();
return from(part, property, where(path.toDotPath()), iterator);
}
@Override
protected QueryCriteria and(final Part part, final QueryCriteria base, final Iterator<Object> iterator) {
if (base == null) {
return create(part, iterator);
}
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(
part.getProperty());
CouchbasePersistentProperty property = path.getLeafProperty();
return from(part, property, base.and(path.toDotPath()), iterator);
}
@Override
protected QueryCriteria or(QueryCriteria base, QueryCriteria criteria) {
return base.or(criteria);
}
@Override
protected Query complete(QueryCriteria criteria, Sort sort) {
N1QLExpression parsedExpression = getExpression(accessor, getParameters(accessor), null /* returnedType */);
Query q = new StringQuery(parsedExpression.toString()).with(sort);
JsonValue params = queryParser.getPlaceholderValues(accessor);
if (params instanceof JsonArray) {
q.setPositionalParameters((JsonArray) params);
} else {
q.setNamedParameters((JsonObject) params);
}
return q;
}
private QueryCriteria from(final Part part, final CouchbasePersistentProperty property,
final QueryCriteria criteria, final Iterator<Object> parameters) {
final Part.Type type = part.getType();
switch (type) {
case SIMPLE_PROPERTY:
return criteria; //.eq(parameters.next()); // this will be the dummy from PartTree
default:
throw new IllegalArgumentException("Unsupported keyword!");
}
}
// copied from StringN1qlBasedQuery
private N1QLExpression getExpression(ParameterAccessor accessor, Object[] runtimeParameters,
ReturnedType returnedType) {
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(
getQueryMethod().getParameters(), runtimeParameters);
N1QLExpression parsedStatement = x(this.queryParser.doParse(parser, evaluationContext, false));
Sort sort = accessor.getSort();
if (sort.isSorted()) {
N1QLExpression[] cbSorts = N1qlUtils.createSort(sort);
parsedStatement = parsedStatement.orderBy(cbSorts);
}
if (queryMethod.isPageQuery()) {
Pageable pageable = accessor.getPageable();
Assert.notNull(pageable, "Pageable must not be null!");
parsedStatement = parsedStatement.limit(pageable.getPageSize()).offset(
Math.toIntExact(pageable.getOffset()));
} else if (queryMethod.isSliceQuery()) {
Pageable pageable = accessor.getPageable();
Assert.notNull(pageable, "Pageable must not be null!");
parsedStatement = parsedStatement.limit(pageable.getPageSize() + 1).offset(
Math.toIntExact(pageable.getOffset()));
}
return parsedStatement;
}
// getExpression() could do this itself, but pass as an arg to be consistent with StringN1qlBasedQuery
private static Object[] getParameters(ParameterAccessor accessor) {
ArrayList<Object> params = new ArrayList<>();
for (Object o : accessor) {
params.add(o);
}
return params.toArray();
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.CouchbaseRepositoryQuery;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
@@ -33,7 +34,6 @@ 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.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -46,6 +46,7 @@ import org.springframework.util.Assert;
* @author Simon Baslé
* @author Oliver Gierke
* @author Mark Paluch
* @author Michael Reiche
*/
public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
@@ -88,14 +89,14 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
* Returns entity information based on the domain class.
*
* @param domainClass the class for the entity.
* @param <T> the value type
* @param <ID> the id type.
* @param <T> the value type
* @param <ID> the id type.
* @return entity information for that domain class.
*/
@Override
public <T, ID> CouchbaseEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
CouchbasePersistentEntity<T> entity = (CouchbasePersistentEntity<T>) mappingContext
.getRequiredPersistentEntity(domainClass);
CouchbasePersistentEntity<T> entity = (CouchbasePersistentEntity<T>) mappingContext.getRequiredPersistentEntity(
domainClass);
return new MappingCouchbaseEntityInformation<>(entity);
}
@@ -151,10 +152,11 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
@Override
public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata metadata,
final ProjectionFactory factory, final NamedQueries namedQueries) {
final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping
.resolve(metadata.getRepositoryInterface(), metadata.getDomainType());
final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(
metadata.getRepositoryInterface(), metadata.getDomainType());
return new CouchbaseRepositoryQuery(couchbaseOperations, new QueryMethod(method, metadata, factory));
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext);
return new CouchbaseRepositoryQuery(couchbaseOperations, queryMethod, namedQueries);
/*CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext);
String namedQueryName = queryMethod.getNamedQueryName();

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.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseRepositoryQuery;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
@@ -32,7 +33,6 @@ import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
/**
* @author Subhashni Balakrishnan
* @author Mark Paluch
* @author Michael Reiche
* @since 3.0
*/
public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactorySupport {
@@ -83,14 +84,14 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor
* Returns entity information based on the domain class.
*
* @param domainClass the class for the entity.
* @param <T> the value type
* @param <ID> the id type.
* @param <T> the value type
* @param <ID> the id type.
* @return entity information for that domain class.
*/
@Override
public <T, ID> CouchbaseEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
CouchbasePersistentEntity<T> entity = (CouchbasePersistentEntity<T>) mappingContext
.getRequiredPersistentEntity(domainClass);
CouchbasePersistentEntity<T> entity = (CouchbasePersistentEntity<T>) mappingContext.getRequiredPersistentEntity(
domainClass);
return new MappingCouchbaseEntityInformation<>(entity);
}
@@ -105,8 +106,8 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor
*/
@Override
protected final Object getTargetRepository(final RepositoryInformation metadata) {
ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping
.resolve(metadata.getRepositoryInterface(), metadata.getDomainType());
ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(
metadata.getRepositoryInterface(), metadata.getDomainType());
CouchbaseEntityInformation<?, Serializable> entityInformation = getEntityInformation(metadata.getDomainType());
SimpleReactiveCouchbaseRepository repository = getTargetRepositoryViaReflection(metadata, entityInformation,
couchbaseOperations);
@@ -148,10 +149,10 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping
.resolve(metadata.getRepositoryInterface(), metadata.getDomainType());
return new ReactiveCouchbaseRepositoryQuery(couchbaseOperations, new QueryMethod(method, metadata, factory));
final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(
metadata.getRepositoryInterface(), metadata.getDomainType());
return new ReactiveCouchbaseRepositoryQuery(couchbaseOperations,
new CouchbaseQueryMethod(method, metadata, factory, mappingContext), namedQueries);
}
}

View File

@@ -67,8 +67,8 @@ class QueryCriteriaTests {
@Test
void testNestedNotIn() {
QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria"))
.and(where("state").notIn(new String[] { "Alabama", "Florida" }));
QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")).and(
where("state").notIn(new String[] { "Alabama", "Florida" }));
assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\") and "
+ "(not( (`state` in ( [ \"Alabama\", \"Florida\" ] )) ))", c.export());
}
@@ -106,19 +106,28 @@ class QueryCriteriaTests {
@Test
void testStartingWith() {
QueryCriteria c = where("name").startingWith("Cou");
assertEquals("`name` like \"Cou%\"", c.export());
assertEquals("`name` like (\"Cou\"||\"%\")", c.export());
}
/* cannot do this properly yet because in arg to when() in
* startingWith() cannot be a QueryCriteria
@Test
void testStartingWithExpr() {
QueryCriteria c = where("name").startingWith(where("name").plus(""));
assertEquals("`name` like ((\"%\" + ((`name` + \"\"))))", c.export());
QueryCriteria c = where("name").startingWith(where("name").plus("xxx"));
assertEquals("`name` like (((`name` || "xxx") || ""%""))", c.export());
}
*/
@Test
void testEndingWith() {
QueryCriteria c = where("name").endingWith("ouch");
assertEquals("`name` like \"%ouch\"", c.export());
assertEquals("`name` like (\"%\"||\"ouch\")", c.export());
}
@Test
void testEndingWithExpr() {
QueryCriteria c = where("name").endingWith(where("name").plus("xxx"));
assertEquals("`name` like (\"%\"||((`name` || \"xxx\")))", c.export());
}
@Test
@@ -148,7 +157,7 @@ class QueryCriteriaTests {
@Test
void testNotLike() {
QueryCriteria c = where("name").notLike("%Elvis%");
assertEquals("not( (`name` like \"%Elvis%\") )", c.export());
assertEquals("not(`name` like \"%Elvis%\")", c.export());
}
@Test

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import org.springframework.context.annotation.Bean;
@@ -23,6 +24,11 @@ import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider;
import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
/**
* @author Michael Nitschinger
* @author Michael Reiche
* @since 3.0
*/
@Configuration
@EnableCouchbaseRepositories
@EnableCouchbaseAuditing // this activates auditing
@@ -32,23 +38,69 @@ public class Config extends AbstractCouchbaseConfiguration {
String password = "password";
String connectionString = "127.0.0.1";
// if running a clusterAwareIntegrationTests, use those properties
static Class clusterAware = null;
static {
try {
clusterAware = Class.forName("org.springframework.data.couchbase.util.ClusterAwareIntegrationTests");
} catch (ClassNotFoundException cnfe) {
}
}
@Override
public String getConnectionString() {
if (clusterAware != null) {
try {
if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) {
return (String) clusterAware.getMethod("connectionString").invoke(null, (Object[]) null);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return connectionString;
}
@Override
public String getUserName() {
if (clusterAware != null) {
try {
if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) {
return (String) clusterAware.getMethod("username").invoke(null, (Object[]) null);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return username;
}
@Override
public String getPassword() {
if (clusterAware != null) {
try {
if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) {
return (String) clusterAware.getMethod("password").invoke(null, (Object[]) null);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return password;
}
@Override
public String getBucketName() {
if (clusterAware != null) {
try {
if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) {
return (String) clusterAware.getMethod("bucketName").invoke(null, (Object[]) null);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return bucketname;
}

View File

@@ -30,17 +30,23 @@ public class Person extends AbstractEntity {
Optional<String> firstname;
Optional<String> lastname;
@CreatedBy private String creator;
@CreatedBy
private String creator;
@LastModifiedBy private String lastModifiedBy;
@LastModifiedBy
private String lastModifiedBy;
@LastModifiedDate private long lastModification;
@LastModifiedDate
private long lastModification;
@CreatedDate private long creationDate; // =System.currentTimeMillis();
@CreatedDate
private long creationDate; // =System.currentTimeMillis();
@Version private long version;
@Version
private long version;
public Person() {}
public Person() {
}
public Person(String firstname, String lastname) {
this();
@@ -54,11 +60,13 @@ public class Person extends AbstractEntity {
}
static String optional(String name, Optional<String> obj) {
if (obj != null)
if (obj.isPresent())
if (obj != null) {
if (obj.isPresent()) {
return (" " + name + ": '" + obj.get() + "'\n");
else
} else {
return " " + name + ": null\n";
}
}
return "";
}
@@ -86,6 +94,10 @@ public class Person extends AbstractEntity {
this.lastname = lastname;
}
public long getVersion() {
return version;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Person : {\n");
@@ -93,15 +105,31 @@ public class Person extends AbstractEntity {
sb.append(optional(", firstname", firstname));
sb.append(optional(", lastname", lastname));
sb.append(", version : " + version);
if (creator != null)
if (creator != null) {
sb.append(", creator : " + creator);
if (creationDate != 0)
}
if (creationDate != 0) {
sb.append(", creationDate : " + creationDate);
if (lastModifiedBy != null)
}
if (lastModifiedBy != null) {
sb.append(", lastModifiedBy : " + lastModifiedBy);
if (lastModification != 0)
}
if (lastModification != 0) {
sb.append(", lastModification : " + lastModification);
}
sb.append("}");
return sb.toString();
}
static String optional(String name, String obj) {
if (obj != null) {
if (obj != null /*.isPresent() */) {
return (" " + name + ": '" + obj/*.get()*/ + "'\n");
} else {
return " " + name + ": null\n";
}
}
return "";
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2012-2020 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
*
* https://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.domain;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* @author Michael Reiche
*/
public interface PersonRepository extends CrudRepository<Person, String> {
/*
* These methods are exercised in HomeController of the test spring-boot DemoApplication
*/
public List<Person> findByLastname(String lastname);
@Query("#{#n1ql.selectEntity} where firstname = 'Reba' and lastname = $last")
public List<Person> any(@Param("last") String any);
@Query("#{#n1ql.selectEntity} where firstname = 'Reba' and lastname = 'McIntyre'")
public List<Person> none();
@Query("#{#n1ql.selectEntity} where firstname = 'Reba' and lastname = $1")
public List<Person> one(@Param("last") String any);
@Query("#{#n1ql.selectEntity} where lastname = $2 and firstname = $1")
public List<Person> two(@Param("one") String one, @Param("two") String two);
@Query("#{#n1ql.selectEntity} where lastname in ( $1 )")
public List<Person> lastnameIn(@Param("lastnames") String[] lastnames);
public Person findByFirstname(String firstname);
public Person findFromReplicasByFirstname(String firstname);
public List<Person> findFromReplicasById(String id);
public List<Person> findByFirstnameLike(String firstname);
public List<Person> findByFirstnameIsNull();
public List<Person> findByFirstnameIsNotNull();
public List<Person> findByFirstnameNotLike(String firstname);
public List<Person> findByFirstnameStartingWith(String firstname);
public List<Person> findByFirstnameEndingWith(String firstname);
public List<Person> findByFirstnameContaining(String firstname);
public List<Person> findByFirstnameNotContaining(String firstname);
public List<Person> findByFirstnameBetween(String firstname1, String firstname2);
public List<Person> findByFirstnameIn(String... firstnames);
public List<Person> findByFirstnameNotIn(String... firstnames);
public List<Person> findByFirstnameTrue(Object... o);
public List<Person> findByFirstnameFalse(Object... o);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
<S extends Person> S save(S var1);
<S extends Person> Iterable<S> saveAll(Iterable<S> var1);
Optional<Person> findById(UUID var1);
boolean existsById(UUID var1);
Iterable<Person> findAll();
long count();
void deleteById(UUID var1);
void delete(Person var1);
void deleteAll(Iterable<? extends Person> var1);
void deleteAll();
}

View File

@@ -18,7 +18,9 @@ package org.springframework.data.couchbase.domain;
import java.util.List;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
/**
@@ -34,4 +36,9 @@ public interface UserRepository extends PagingAndSortingRepository<User, String>
List<User> findByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where firstname = $1 and lastname = $2")
List<User> getByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where (firstname = $first or lastname = $last)")
List<User> getByFirstnameOrLastname(@Param("first")String firstname, @Param("last")String lastname);
}

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import static org.junit.jupiter.api.Assertions.*;
@@ -32,12 +31,13 @@ import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserRepository;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.DefaultParameters;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.*;
import org.springframework.data.repository.query.parser.PartTree;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
class N1qlQueryCreatorTests {
MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> context;
@@ -55,7 +55,8 @@ class N1qlQueryCreatorTests {
PartTree tree = new PartTree(input, User.class);
Method method = UserRepository.class.getMethod(input, String.class);
N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), context);
N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), null,
converter);
Query query = creator.createQuery();
assertEquals(query.export(), " WHERE " + where("firstname").is("Oliver").export());
@@ -66,8 +67,8 @@ class N1qlQueryCreatorTests {
String input = "findByFirstnameAndLastname";
PartTree tree = new PartTree(input, User.class);
Method method = UserRepository.class.getMethod(input, String.class, String.class);
N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "John", "Doe"), context);
N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "John", "Doe"), null,
converter);
Query query = creator.createQuery();
assertEquals(query.export(), " WHERE " + where("firstname").is("John").and("lastname").is("Doe").export());

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2017-2019 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
*
* https://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 org.junit.jupiter.api.Assertions.*;
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE;
import java.lang.reflect.Method;
import java.util.Properties;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserRepository;
import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.data.repository.query.*;
import javax.el.MethodNotFoundException;
/**
* @author Michael Nitschinger
* @author Michael Reiche
*/
class StringN1qlQueryCreatorTests {
MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> context;
CouchbaseConverter converter;
CouchbaseTemplate couchbaseTemplate;
static NamedQueries namedQueries = new PropertiesBasedNamedQueries(new Properties());
@BeforeEach
public void beforeEach() {
context = new CouchbaseMappingContext();
converter = new MappingCouchbaseConverter(context);
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE);
}
@Test
void createsQueryCorrectly() throws Exception {
String input = "getByFirstnameAndLastname";
Method method = UserRepository.class.getMethod(input, String.class, String.class);
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method,
new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(),
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(
getAccessor(getParameters(method), "Oliver", "Twist"), queryMethod, converter, "travel-sample",
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
Query query = creator.createQuery();
assertEquals(
"SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `travel-sample`.* FROM `travel-sample` where firstname = $1 and lastname = $2 AND `_class` = \"org.springframework.data.couchbase.domain.User\"",
query.toN1qlString(couchbaseTemplate.reactive(), User.class, false));
}
@Test
void createsQueryCorrectly2() throws Exception {
String input = "getByFirstnameOrLastname";
Method method = UserRepository.class.getMethod(input, String.class, String.class);
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method,
new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(),
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(
getAccessor(getParameters(method), "Oliver", "Twist"), queryMethod, converter, "travel-sample",
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
Query query = creator.createQuery();
assertEquals(
"SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `travel-sample`.* FROM `travel-sample` where (firstname = $first or lastname = $last) AND `_class` = \"org.springframework.data.couchbase.domain.User\"",
query.toN1qlString(couchbaseTemplate.reactive(), User.class, false));
}
@Test
void wrongNumberArgs() throws Exception {
String input = "getByFirstnameOrLastname";
Method method = UserRepository.class.getMethod(input, String.class, String.class);
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method,
new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(),
converter.getMappingContext());
try {
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"),
queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries);
} catch (IllegalArgumentException e) {
return;
}
fail("should have failed with IllegalArgumentException: Invalid number of parameters given!");
}
@Test
void doesNotHaveAnnotation() throws Exception {
String input = "findByFirstname";
Method method = UserRepository.class.getMethod(input, String.class);
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method,
new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(),
converter.getMappingContext());
try {
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"),
queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries);
} catch (IllegalArgumentException e) {
return;
}
fail("should have failed with IllegalArgumentException: query has no inline Query or named Query not found");
}
@Test
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
void findUsingStringNq1l() throws Exception {
User user = new User(UUID.randomUUID().toString(), "Oliver", "Twist");
User modified = couchbaseTemplate.upsertById(User.class).one(user);
String input = "getByFirstnameOrLastname";
Method method = UserRepository.class.getMethod(input, String.class, String.class);
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method,
new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(),
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(
getAccessor(getParameters(method), "Oliver", "Twist"), queryMethod, converter, "travel-sample",
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
Query query = creator.createQuery();
ExecutableFindByQueryOperation.ExecutableFindByQuery q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) couchbaseTemplate.findByQuery(
User.class).matching(query);
User u = (User) q.oneValue();
assertEquals(user, u);
couchbaseTemplate.removeById().one(user.getId());
}
private ParameterAccessor getAccessor(Parameters<?, ?> params, Object... values) {
return new ParametersParameterAccessor(params, values);
}
private Parameters<?, ?> getParameters(Method method) {
return new DefaultParameters(method);
}
}

View File

@@ -52,6 +52,8 @@ public abstract class ClusterAwareIntegrationTests {
return PasswordAuthenticator.create(config().adminUsername(), config().adminPassword());
}
public static String username() { return config().adminUsername(); }
public static String password() { return config().adminPassword(); }
public static String bucketName() {
return config().bucketname();
}
@@ -62,13 +64,33 @@ public abstract class ClusterAwareIntegrationTests {
* @return the connection string to connect.
*/
public static String connectionString() {
/*
return seedNodes().stream().map(s -> {
if (s.kvPort().isPresent()) {
return s.address() + ":" + s.kvPort().get();
return s.address() + ":" + s.kvPort().get() + "=" + Services.KV;
} else if (s.clusterManagerPort().isPresent()) {
return s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER;
} else {
return s.address();
return s.address() ;
}
}).collect(Collectors.joining(","));
*/
StringBuffer sb = new StringBuffer();
for(SeedNode s:seedNodes()) {
if (s.kvPort().isPresent()) {
if(sb.length() > 0 ) sb.append(",");
sb.append (s.address() + ":" + s.kvPort().get() + "=" + Services.KV);
}
if (s.clusterManagerPort().isPresent()) {
if (sb.length() > 0)
sb.append(",");
sb.append(s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER);
}
if(sb.length() == 0 ){
sb.append(s.address());
}
}
return sb.toString();
}
public static Set<SeedNode> seedNodes() {