DATACOUCH - 534 Add support for Query annotation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user