Add n1ql.scope and n1ql.collection spel expressions for @Query.

This adds n1ql.scope and n1ql.collection spel expressions for
@Query so that n1ql.bucket doesn't need to be overloaded with
the collection name. This will make the bucket name available
in n1q.bucket.

This is a breaking change as queries that used n1ql.bucket to
get the collection name (especially in the case of pre-scope-
and-collection @Queries that (a) referenced n1ql.bucket instead
of referencing other spel expressions that include the
bucket/collection name; and (b)  still worked after the
repository was moved from a bucket onto a collection by
virtue of n1ql.bucket having the value of the collection in
such instances.

Closes #1445.
This commit is contained in:
Michael Reiche
2022-06-14 14:33:21 -07:00
parent 1db04d2cf5
commit c6fa4ee041
16 changed files with 314 additions and 170 deletions

View File

@@ -133,6 +133,8 @@ A few N1QL-specific values are provided through SpEL:
- `#n1ql.selectEntity` allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value).
- `#n1ql.filter` in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information.
- `#n1ql.bucket` will be replaced by the name of the bucket the entity is stored in, escaped in backticks.
- `#n1ql.scope` will be replaced by the name of the scope the entity is stored in, escaped in backticks.
- `#n1ql.collection` will be replaced by the name of the collection the entity is stored in, escaped in backticks.
- `#n1ql.fields` will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity.
- `#n1ql.delete` will be replaced by the `delete from` statement.
- `#n1ql.returning` will be replaced by returning clause needed for reconstructing entity.
@@ -164,7 +166,7 @@ This is *NOT* intended for projections to DTOs.
Another example: +
`#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND test = $1` +
is equivalent to +
`SELECT #{#n1ql.fields} FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} AND test = $1`
`SELECT #{#n1ql.fields} FROM #{#n1ql.collection} WHERE #{#n1ql.filter} AND test = $1`
.A practical application of SpEL with Spring Security
****

View File

@@ -169,7 +169,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
@Override
public Flux<T> all() {
PseudoArgs<QueryOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection());
String statement = assembleEntityQuery(false, distinctFields, pArgs.getScope(), pArgs.getCollection());
LOG.trace("findByQuery {} statement: {}", pArgs, statement);
Mono<ReactiveQueryResult> allResult = pArgs.getScope() == null
? template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
@@ -214,7 +214,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
@Override
public Mono<Long> count() {
PseudoArgs<QueryOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection());
String statement = assembleEntityQuery(true, distinctFields, pArgs.getScope(), pArgs.getCollection());
LOG.trace("findByQuery {} statement: {}", pArgs, statement);
Mono<ReactiveQueryResult> countResult = pArgs.getScope() == null
? template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
@@ -236,8 +236,8 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
return count().map(count -> count > 0); // not efficient, just need the first one
}
private String assembleEntityQuery(final boolean count, String[] distinctFields, String collection) {
return query.toN1qlSelectString(template, collection, this.domainType, this.returnType, count,
private String assembleEntityQuery(final boolean count, String[] distinctFields, String scope, String collection) {
return query.toN1qlSelectString(template, scope, collection, this.domainType, this.returnType, count,
query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields, fields);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -71,7 +71,7 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu
@Override
public Flux<RemoveResult> all() {
PseudoArgs<QueryOptions> pArgs = new PseudoArgs<>(template, scope, collection, options, domainType);
String statement = assembleDeleteQuery(pArgs.getCollection());
String statement = assembleDeleteQuery(pArgs.getScope(), pArgs.getCollection());
LOG.trace("removeByQuery {} statement: {}", pArgs, statement);
Mono<ReactiveQueryResult> allResult = pArgs.getScope() == null
? template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
@@ -119,8 +119,8 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu
options);
}
private String assembleDeleteQuery(String collection) {
return query.toN1qlRemoveString(template, collection, this.domainType);
private String assembleDeleteQuery(String scope, String collection) {
return query.toN1qlRemoveString(template, scope, collection, this.domainType);
}
@Override

View File

@@ -73,13 +73,14 @@ public class N1qlJoinResolver {
String from = "FROM " + keySpacePair.lhs.keyspace + " lks " + useLKS + joinType + " " + keySpacePair.rhs.keyspace
+ " rks";
StringBasedN1qlQueryParser.N1qlSpelValues n1qlL = Query.getN1qlSpelValues(template, keySpacePair.lhs.collection,
parameters.getEntityTypeInfo().getType(), parameters.getEntityTypeInfo().getType(), false, null, null);
StringBasedN1qlQueryParser.N1qlSpelValues n1qlL = Query.getN1qlSpelValues(template, null,
keySpacePair.lhs.collection, parameters.getEntityTypeInfo().getType(), parameters.getEntityTypeInfo().getType(),
false, null, null);
String onLks = "lks." + n1qlL.filter;
StringBasedN1qlQueryParser.N1qlSpelValues n1qlR = Query.getN1qlSpelValues(template, keySpacePair.rhs.collection,
parameters.getAssociatedEntityTypeInfo().getType(), parameters.getAssociatedEntityTypeInfo().getType(), false,
null, null);
StringBasedN1qlQueryParser.N1qlSpelValues n1qlR = Query.getN1qlSpelValues(template, null,
keySpacePair.rhs.collection, parameters.getAssociatedEntityTypeInfo().getType(),
parameters.getAssociatedEntityTypeInfo().getType(), false, null, null);
String onRks = "rks." + n1qlR.filter;
StringBuilder useRKSBuilder = new StringBuilder();

View File

@@ -209,6 +209,13 @@ public class N1QLExpression {
return x("COUNT(" + expression.toString() + ")");
}
/**
* Returned expression results in distinct of the expression
*/
public static N1QLExpression distinct(N1QLExpression expression) {
return x("distinct{" + expression.toString() + "}");
}
/**
* Helper method to wrap varargs with the given character.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -48,8 +48,8 @@ public class N1QLQuery extends Query {
}
@Override
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass,
Class returnClass, boolean isCount, String[] distinctFields, String[] fields) {
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String scopeName, String collectionName,
Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, String[] fields) {
return expression.toString();
}
}

View File

@@ -338,13 +338,17 @@ public class Query {
return sb.toString();
}
/**
*
*/
@Deprecated
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) {
return toN1qlSelectString(template, null, domainClass, null, isCount, null, null);
return toN1qlSelectString(template, null, null, domainClass, null, isCount, null, null);
}
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass,
Class returnClass, boolean isCount, String[] distinctFields, String[] fields) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass,
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String scopeName, String collectionName,
Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, String[] fields) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, scopeName, collectionName, domainClass,
returnClass, isCount, distinctFields, fields);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.selectEntity); // select ...
@@ -357,9 +361,10 @@ public class Query {
return statement.toString();
}
public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass, null,
false, null, null);
public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String scopeName, String collectionName,
Class domainClass) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, scopeName, collectionName, domainClass,
null, false, null, null);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.delete); // delete ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
@@ -369,8 +374,8 @@ public class Query {
}
public static StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template,
String collectionName, Class domainClass, Class returnClass, boolean isCount, String[] distinctFields,
String[] fields) {
String scopeName, String collectionName, Class domainClass, Class returnClass, boolean isCount,
String[] distinctFields, String[] fields) {
String typeKey = template.getConverter().getTypeKey();
final CouchbasePersistentEntity<?> persistentEntity = template.getConverter().getMappingContext()
.getRequiredPersistentEntity(domainClass);
@@ -382,9 +387,10 @@ public class Query {
typeValue = alias.toString();
}
StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(template.getBucketName(), collectionName,
template.getConverter(), domainClass, returnClass, typeKey, typeValue, isCount, distinctFields, fields);
return isCount ? sbnqp.getCountContext() : sbnqp.getStatementContext();
StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(template.getBucketName(), scopeName,
collectionName, template.getConverter(), domainClass, returnClass, typeKey, typeValue, isCount, distinctFields,
fields);
return sbnqp.getStatementContext();
}
/**

View File

@@ -18,9 +18,20 @@ package org.springframework.data.couchbase.core.query;
import java.util.Locale;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.support.TemplateUtils;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser;
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
import org.springframework.data.mapping.Alias;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
/**
@@ -38,32 +49,46 @@ import com.couchbase.client.java.json.JsonValue;
*/
public class StringQuery extends Query {
private final CouchbaseQueryMethod queryMethod;
private final String inlineN1qlQuery;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ParameterAccessor parameterAccessor;
private final SpelExpressionParser spelExpressionParser;
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);
public StringQuery(CouchbaseQueryMethod queryMethod, String n1qlString,
QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider, ParameterAccessor parameterAccessor,
SpelExpressionParser spelExpressionParser) {
this.queryMethod = queryMethod;
this.inlineN1qlQuery = n1qlString;
this.evaluationContextProvider = queryMethodEvaluationContextProvider;
this.parameterAccessor = parameterAccessor;
this.spelExpressionParser = spelExpressionParser;
}
@Override
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collection, Class domainClass,
Class resultClass, boolean isCount, String[] distinctFields, String[] fields) {
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String scope, String collection,
Class domainClass, Class resultClass, boolean isCount, String[] distinctFields, String[] fields) {
StringBasedN1qlQueryParser parser = getStringN1qlQueryParser(template, scope, collection, domainClass,
distinctFields, fields);
N1QLExpression parsedExpression = parser.getExpression(inlineN1qlQuery, queryMethod, parameterAccessor,
spelExpressionParser, evaluationContextProvider);
String queryString = parsedExpression.toString();
JsonValue parameters = parser.getPlaceholderValues(parameterAccessor);
if (parameters instanceof JsonArray) {
this.setPositionalParameters((JsonArray) parameters);
} else {
this.setNamedParameters((JsonObject) parameters);
}
final StringBuilder statement = new StringBuilder();
boolean makeCount = isCount && inlineN1qlQuery != null
&& !inlineN1qlQuery.toLowerCase(Locale.ROOT).contains("count(");
boolean makeCount = isCount && queryString != null && !queryString.toLowerCase(Locale.ROOT).contains("count(");
if (makeCount) {
statement.append("SELECT COUNT(*) AS " + TemplateUtils.SELECT_COUNT + " FROM (");
}
appendInlineN1qlStatement(statement); // apply the string statement
statement.append(queryString); // apply the string statement
// 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
@@ -86,6 +111,26 @@ public class StringQuery extends Query {
return statement.toString();
}
private StringBasedN1qlQueryParser getStringN1qlQueryParser(ReactiveCouchbaseTemplate template, String scopeName,
String collectionName, Class domainClass, String[] distinctFields, String[] fields) {
String typeKey = template.getConverter().getTypeKey();
final CouchbasePersistentEntity<?> persistentEntity = template.getConverter().getMappingContext()
.getRequiredPersistentEntity(domainClass);
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
String typeValue = info.getJavaType().getName();
TypeInformation<?> typeInfo = ClassTypeInformation.from(info.getJavaType());
Alias alias = template.getConverter().getTypeAlias(typeInfo);
if (alias != null && alias.isPresent()) {
typeValue = alias.toString();
}
// there are no options for distinct and fields for @Query
StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(inlineN1qlQuery, queryMethod,
template.getBucketName(), scopeName, collectionName, template.getConverter(), typeKey, typeValue,
parameterAccessor, new SpelExpressionParser(), evaluationContextProvider);
return sbnqp;
}
/**
* toN1qlRemoveString - use toN1qlSelectString
*
@@ -94,7 +139,8 @@ public class StringQuery extends Query {
* @param domainClass
*/
@Override
public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) {
return toN1qlSelectString(template, collectionName, domainClass, domainClass, false, null, null);
public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String scopeName, String collectionName,
Class domainClass) {
return toN1qlSelectString(template, scopeName, collectionName, domainClass, domainClass, false, null, null);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -65,15 +65,16 @@ public class N1qlRepositoryQueryExecutor {
Query query;
ExecutableFindByQuery q;
if (queryMethod.hasN1qlAnnotation()) {
query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), operations.getBucketName(),
SPEL_PARSER, evaluationContextProvider, namedQueries).createQuery();
query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), SPEL_PARSER,
evaluationContextProvider, namedQueries).createQuery();
} else {
final PartTree tree = new PartTree(queryMethod.getName(), domainClass);
query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter(), operations.getBucketName()).createQuery();
query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter(), operations.getBucketName())
.createQuery();
}
ExecutableFindByQuery<?> operation = (ExecutableFindByQuery<?>) operations
.findByQuery(domainClass).withConsistency(buildQueryScanConsistency());
ExecutableFindByQuery<?> operation = (ExecutableFindByQuery<?>) operations.findByQuery(domainClass)
.withConsistency(buildQueryScanConsistency());
if (queryMethod.isCountQuery()) {
return operation.matching(query).count();
} else if (queryMethod.isCollectionQuery()) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2022 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.
@@ -77,8 +77,7 @@ public class ReactiveStringBasedCouchbaseQuery extends AbstractReactiveCouchbase
protected Query createQuery(ParametersParameterAccessor accessor) {
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(),
getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider,
namedQueries);
getOperations().getConverter(), expressionParser, evaluationContextProvider, namedQueries);
Query query = creator.createQuery();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2022 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.
@@ -75,8 +75,7 @@ public class StringBasedCouchbaseQuery extends AbstractCouchbaseQuery {
protected Query createQuery(ParametersParameterAccessor accessor) {
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(),
getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider,
namedQueries);
getOperations().getConverter(), expressionParser, evaluationContextProvider, namedQueries);
Query query = creator.createQuery();
if (LOG.isTraceEnabled()) {

View File

@@ -44,7 +44,6 @@ import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -75,6 +74,18 @@ public class StringBasedN1qlQueryParser {
* <code>"SELECT * FROM #{{@value SPEL_BUCKET}} LIMIT 3"</code>.
*/
public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket";
/**
* Use this variable in a SpEL expression in a {@link Query @Query} annotation's inline statement. This will be
* replaced by the (escaped) scope name. Eg.
* <code>"SELECT * FROM #{{@value SPEL_SCOPE}}.#{{@value SPEL_COLLECTION}} LIMIT 3"</code>.
*/
public static final String SPEL_SCOPE = "#" + SPEL_PREFIX + ".scope";
/**
* Use this variable in a SpEL expression in a {@link Query @Query} annotation's inline statement. This will be
* replaced by the (escaped) collection name. Eg.
* <code>"SELECT * FROM #{{@value SPEL_SCOPE}}.#{{@value SPEL_COLLECTION}} LIMIT 3"</code>.
*/
public static final String SPEL_COLLECTION = "#" + SPEL_PREFIX + ".collection";
/**
* Use this variable in a SpEL expression in a {@link Query @Query} annotation's inline statement. This will be
* replaced by the fields allowing to construct the repository's entity (SELECT clause). Eg.
@@ -118,48 +129,84 @@ public class StringBasedN1qlQueryParser {
private final CouchbaseQueryMethod queryMethod;
private PlaceholderType placeHolderType;
private final N1qlSpelValues statementContext;
private final N1qlSpelValues countContext;
private final CouchbaseConverter couchbaseConverter;
private final Collection<String> parameterNames = new HashSet<String>();
private final Collection<String> parameterNames = new HashSet<>();
public final N1QLExpression parsedExpression;
public StringBasedN1qlQueryParser(String statement, CouchbaseQueryMethod queryMethod, String bucketName,
CouchbaseConverter couchbaseConverter, String typeField, String typeValue, ParameterAccessor accessor,
SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
/**
* This constructor is to allow for generating the n1ql spel expressions from @Queries.
*
* @param statement
* @param queryMethod
* @param bucketName
* @param scope
* @param collection
* @param couchbaseConverter
* @param typeField
* @param typeValue
* @param accessor
* @param spelExpressionParser
* @param evaluationContextProvider
*/
public StringBasedN1qlQueryParser(String statement, CouchbaseQueryMethod queryMethod, String bucketName, String scope,
String collection, CouchbaseConverter couchbaseConverter, String typeField, String typeValue,
ParameterAccessor accessor, SpelExpressionParser spelExpressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.statement = statement;
this.queryMethod = queryMethod;
this.couchbaseConverter = couchbaseConverter;
String collection = queryMethod.getCollection();
this.statementContext = createN1qlSpelValues(bucketName, collection,
queryMethod.getEntityInformation().getJavaType(), queryMethod.getReturnedObjectType(), typeField, typeValue,
false, null, null);
this.countContext = createN1qlSpelValues(bucketName, collection, queryMethod.getEntityInformation().getJavaType(),
queryMethod.getReturnedObjectType(), typeField, typeValue, true, null, null);
this.parsedExpression = getExpression(accessor, null, parser, evaluationContextProvider);
checkPlaceholders(this.parsedExpression.toString());
this.statementContext = createN1qlSpelValues(collection != null ? collection : bucketName, scope, collection,
queryMethod.getEntityInformation().getJavaType(), typeField, typeValue, queryMethod.isCountQuery(), null, null);
this.parsedExpression = getExpression(statement, queryMethod, accessor, spelExpressionParser,
evaluationContextProvider);
}
public StringBasedN1qlQueryParser(String bucketName, String collection, CouchbaseConverter couchbaseConverter,
Class domainClass, Class resultClass, String typeField, String typeValue, boolean isCount,
String[] distinctFields, String[] fields) {
/**
* This constructor is to allow for generating the n1ql spel expressions from NON-@Queries. For selects,
* n1ql.selectEntity and n1ql.filter. FOr deletes, n1ql.delete, n1ql.filter and n1ql.returning
*
* @param bucketName
* @param scope
* @param collection
* @param couchbaseConverter
* @param domainClass
* @param resultClass
* @param typeField
* @param typeValue
* @param isCount
* @param distinctFields
* @param fields
*/
public StringBasedN1qlQueryParser(String bucketName, String scope, String collection,
CouchbaseConverter couchbaseConverter, Class<?> domainClass, Class<?> resultClass, String typeField,
String typeValue, boolean isCount, String[] distinctFields, String[] fields) {
this.statement = null;
this.queryMethod = null;
this.couchbaseConverter = couchbaseConverter;
if (!isCount) {
this.statementContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField,
typeValue, false, distinctFields, fields);
this.countContext = null;
} else {
this.statementContext = null;
this.countContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue,
true, distinctFields, fields);
}
this.statementContext = createN1qlSpelValues(bucketName, scope, collection, domainClass, typeField, typeValue,
isCount, distinctFields, fields);
this.parsedExpression = null;
}
public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, Class domainClass, Class resultClass,
/**
* Create the n1ql spel values. The domainClass is needed, but not the returnClass. Mapping the domainClass to the
* returnClass is the responsibility of decoding.
*
* @param bucketName
* @param scope
* @param collection
* @param domainClass
* @param typeField
* @param typeValue
* @param isCount
* @param distinctFields
* @param fields
* @return
*/
public N1qlSpelValues createN1qlSpelValues(String bucketName, String scope, String collection, Class domainClass,
String typeField, String typeValue, boolean isCount, String[] distinctFields, String[] fields) {
String b = collection != null ? collection : bucketName;
String b = bucketName;
String keyspace = collection != null ? collection : bucketName;
Assert.isTrue(!(distinctFields != null && fields != null),
"only one of project(fields) and distinct(distinctFields) can be specified");
String entityFields = "";
@@ -167,24 +214,26 @@ public class StringBasedN1qlQueryParser {
if (distinctFields != null) {
String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, typeField, fields, distinctFields);
if (isCount) {
selectEntity = "SELECT COUNT( DISTINCT {" + distinctFieldsStr + "} ) " + CountFragment.COUNT_ALIAS + " FROM "
+ i(b);
selectEntity = N1QLExpression.select(N1QLExpression.count(N1QLExpression.distinct(x(distinctFieldsStr)))
.as(i(CountFragment.COUNT_ALIAS)).from(keyspace)).toString();
} else {
selectEntity = "SELECT DISTINCT " + distinctFieldsStr + " FROM " + i(b);
selectEntity = N1QLExpression.select(N1QLExpression.distinct(x(distinctFieldsStr))).from(keyspace).toString();
}
} else if (isCount) {
selectEntity = "SELECT " + "COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM " + i(b);
selectEntity = N1QLExpression.select(N1QLExpression.count(x("\"*\"")).as(i(CountFragment.COUNT_ALIAS)))
.from(keyspace).toString();
} else {
String projectedFields = getProjectedOrDistinctFields(b, domainClass, typeField, fields, distinctFields);
String projectedFields = getProjectedOrDistinctFields(keyspace, domainClass, typeField, fields, distinctFields);
entityFields = projectedFields;
selectEntity = "SELECT " + projectedFields + " FROM " + i(b);
selectEntity = N1QLExpression.select(x(projectedFields)).from(keyspace).toString();
}
String typeSelection = "`" + typeField + "` = \"" + typeValue + "\"";
String delete = N1QLExpression.delete().from(b).toString();
String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(b).toString();
String delete = N1QLExpression.delete().from(keyspace).toString();
String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(keyspace);
return new N1qlSpelValues(selectEntity, entityFields, i(b).toString(), typeSelection, delete, returning);
return new N1qlSpelValues(selectEntity, entityFields, i(b).toString(), i(scope).toString(),
i(collection).toString(), typeSelection, delete, returning);
}
private String getProjectedOrDistinctFields(String b, Class resultClass, String typeField, String[] fields,
@@ -193,8 +242,15 @@ public class StringBasedN1qlQueryParser {
return i(distinctFields).toString();
}
String projectedFields;
PersistentEntity persistentEntity = null;
if (resultClass != null && !Modifier.isAbstract(resultClass.getModifiers())) {
PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass);
try {
persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass);
} catch (Exception e) {
e.printStackTrace();
}
}
if (persistentEntity != null) {
StringBuilder sb = new StringBuilder();
getProjectedFieldsInternal(b, null, sb, persistentEntity, typeField, fields, distinctFields != null);
projectedFields = sb.toString();
@@ -300,14 +356,11 @@ public class StringBasedN1qlQueryParser {
// this static method can be used to test the parsing behavior for Couchbase specific spel variables
// in isolation from the rest of the spel parser initialization chain.
public String doParse(SpelExpressionParser parser, EvaluationContext evaluationContext, boolean isCountQuery) {
org.springframework.expression.Expression parsedExpression = parser.parseExpression(this.getStatement(),
public static String doParse(String statement, SpelExpressionParser parser, EvaluationContext evaluationContext,
N1qlSpelValues n1qlSpelValues) {
org.springframework.expression.Expression parsedExpression = parser.parseExpression(statement,
new TemplateParserContext());
if (isCountQuery) {
evaluationContext.setVariable(SPEL_PREFIX, this.getCountContext());
} else {
evaluationContext.setVariable(SPEL_PREFIX, this.getStatementContext());
}
evaluationContext.setVariable(SPEL_PREFIX, n1qlSpelValues);
return parsedExpression.getValue(evaluationContext, String.class);
}
@@ -425,7 +478,7 @@ public class StringBasedN1qlQueryParser {
return namedValues;
}
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
public JsonValue getPlaceholderValues(ParameterAccessor accessor) {
switch (this.placeHolderType) {
case NAMED:
return getNamedPlaceholderValues(accessor);
@@ -483,10 +536,6 @@ public class StringBasedN1qlQueryParser {
return this.statement.contains(SPEL_SELECT_FROM_CLAUSE);
}
public N1qlSpelValues getCountContext() {
return this.countContext;
}
public N1qlSpelValues getStatementContext() {
return this.statementContext;
}
@@ -527,6 +576,18 @@ public class StringBasedN1qlQueryParser {
*/
public final String bucket;
/**
* <code>#{{@value SPEL_SCOPE}.
* bucket</code> will be replaced by (escaped) scope name in which the entity is stored.
*/
public final String scope;
/**
* <code>#{{@value SPEL_COLLECTION}.
* bucket</code> will be replaced by (escaped) collection name in which the entity is stored.
*/
public final String collection;
/**
* <code>#{{@value SPEL_FILTER}}.
* filter</code> will be replaced by an expression allowing to select only entries matching the entity in a WHERE
@@ -547,25 +608,37 @@ public class StringBasedN1qlQueryParser {
*/
public final String returning;
public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter, String delete,
String returning) {
public N1qlSpelValues(String selectClause, String entityFields, String bucket, String scope, String collection,
String filter, String delete, String returning) {
this.selectEntity = selectClause;
this.fields = entityFields;
this.bucket = bucket;
this.scope = scope;
this.collection = collection;
this.filter = filter;
this.delete = delete;
this.returning = returning;
}
}
// copied from StringN1qlBasedQuery
private N1QLExpression getExpression(ParameterAccessor accessor, ReturnedType returnedType,
/**
* Creates the N1QLExpression and parameterNames
*
* @param statement
* @param queryMethod
* @param accessor
* @param parser
* @param evaluationContextProvider
* @return
*/
public N1QLExpression getExpression(String statement, CouchbaseQueryMethod queryMethod, ParameterAccessor accessor,
SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
boolean isCountQuery = queryMethod.isCountQuery();
Object[] runtimeParameters = getParameters(accessor);
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(),
runtimeParameters);
N1QLExpression parsedStatement = x(this.doParse(parser, evaluationContext, isCountQuery));
N1QLExpression parsedStatement = x(doParse(statement, parser, evaluationContext, this.getStatementContext()));
checkPlaceholders(parsedStatement.toString());
return parsedStatement;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors
* Copyright 2021-2022 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.
@@ -23,13 +23,11 @@ import java.util.Optional;
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.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.core.NamedQueries;
@@ -39,31 +37,25 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
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.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
/**
* @author Michael Reiche
* @author Mauro Monti
*/
public class StringN1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria> {
// everything we need in the StringQuery such that we can doParse() later when we have the scope and collection
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 CouchbaseQueryMethod queryMethod;
private final CouchbaseConverter couchbaseConverter;
private final N1QLExpression parsedExpression;
private final String queryString;
private final SpelExpressionParser spelExpressionParser;
public StringN1qlQueryCreator(final ParameterAccessor accessor, CouchbaseQueryMethod queryMethod,
CouchbaseConverter couchbaseConverter, String bucketName, SpelExpressionParser spelExpressionParser,
CouchbaseConverter couchbaseConverter, SpelExpressionParser spelExpressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider, NamedQueries namedQueries) {
// AbstractQueryCreator needs a PartTree, so we give it a dummy one.
@@ -76,27 +68,19 @@ public class StringN1qlQueryCreator extends AbstractQueryCreator<Query, QueryCri
this.context = couchbaseConverter.getMappingContext();
this.queryMethod = queryMethod;
this.couchbaseConverter = couchbaseConverter;
this.spelExpressionParser = spelExpressionParser;
this.evaluationContextProvider = evaluationContextProvider;
final String namedQueryName = queryMethod.getNamedQueryName();
String queryString;
String qString;
if (queryMethod.hasInlineN1qlQuery()) {
queryString = queryMethod.getInlineN1qlQuery();
qString = queryMethod.getInlineN1qlQuery();
} else if (namedQueries.hasQuery(namedQueryName)) {
queryString = namedQueries.getQuery(namedQueryName);
qString = namedQueries.getQuery(namedQueryName);
} else {
throw new IllegalArgumentException("query has no inline Query or named Query not found");
}
Class javaType = getType();
String typeValue = javaType.getName();
TypeInformation<?> typeInfo = ClassTypeInformation.from(javaType);
Alias alias = couchbaseConverter.getTypeAlias(typeInfo);
if (alias != null && alias.isPresent()) {
typeValue = alias.toString();
}
this.queryParser = new StringBasedN1qlQueryParser(queryString, queryMethod, bucketName, couchbaseConverter,
getTypeField(), typeValue, accessor, spelExpressionParser, evaluationContextProvider);
this.parser = spelExpressionParser;
this.parsedExpression = this.queryParser.parsedExpression;
// Save the query string to be parsed later after we have the scope and collection to be used in the query
this.queryString = qString;
}
protected QueryMethod getQueryMethod() {
@@ -148,13 +132,9 @@ public class StringN1qlQueryCreator extends AbstractQueryCreator<Query, QueryCri
@Override
protected Query complete(QueryCriteria criteria, Sort sort) {
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);
}
// everything we need in the StringQuery such that we can doParse() later when we have the scope and collection
Query q = new StringQuery(queryMethod, queryString, evaluationContextProvider, accessor, spelExpressionParser)
.with(sort);
return q;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,9 +20,11 @@ import java.util.List;
import java.util.stream.Stream;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.couchbase.repository.ScanConsistency;
import org.springframework.data.couchbase.repository.Scope;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@@ -59,6 +61,11 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
List<User> findByVersionEqualsAndFirstnameEquals(Long version, String firstname);
@Query("#{#n1ql.selectEntity}|#{#n1ql.filter}|#{#n1ql.bucket}|#{#n1ql.scope}|#{#n1ql.collection}")
@Scope("thisScope")
@Collection("thisCollection")
List<User> spelTests();
// simulate a slow operation
@Cacheable("mySpringCache")
default List<User> getByFirstname(String firstname) {

View File

@@ -66,9 +66,9 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @author Michael Nitschinger
* @author Michael Reiche
*/
@SpringJUnitConfig(StringN1qlQueryCreatorTests.Config.class)
@SpringJUnitConfig(StringN1qlQueryCreatorIntegrationTests.Config.class)
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
class StringN1qlQueryCreatorIntegrationTests extends ClusterAwareIntegrationTests {
MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> context;
CouchbaseConverter converter;
@@ -98,8 +98,8 @@ class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"),
queryMethod, converter, config().bucketname(), new SpelExpressionParser(),
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries);
Query query = creator.createQuery();
@@ -131,8 +131,8 @@ class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"),
queryMethod, converter, config().bucketname(), new SpelExpressionParser(),
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries);
Query query = creator.createQuery();

View File

@@ -84,13 +84,15 @@ class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests {
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"),
queryMethod, converter, "travel-sample", new SpelExpressionParser(),
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
Query query = creator.createQuery();
assertEquals(
"SELECT `_class`, META(`travel-sample`).`cas` AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`travel-sample`).`id` AS __id, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and firstname = $1 and lastname = $2",
query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false));
"SELECT `_class`, META(`" + bucketName()
+ "`).`cas` AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`"
+ bucketName() + "`).`id` AS __id, `firstname`, `lastname`, `subtype` FROM `" + bucketName()
+ "` where `_class` = \"abstractuser\" and firstname = $1 and lastname = $2",
query.toN1qlSelectString(couchbaseTemplate.reactive(), null, null, User.class, User.class, false, null, null));
}
@Test
@@ -103,13 +105,15 @@ class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests {
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"),
queryMethod, converter, "travel-sample", new SpelExpressionParser(),
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
Query query = creator.createQuery();
assertEquals(
"SELECT `_class`, META(`travel-sample`).`cas` AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`travel-sample`).`id` AS __id, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and (firstname = $first or lastname = $last)",
query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false));
"SELECT `_class`, META(`" + bucketName()
+ "`).`cas` AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`"
+ bucketName() + "`).`id` AS __id, `firstname`, `lastname`, `subtype` FROM `" + bucketName()
+ "` where `_class` = \"abstractuser\" and (firstname = $first or lastname = $last)",
query.toN1qlSelectString(couchbaseTemplate.reactive(), null, null, User.class, User.class, false, null, null));
}
@Test
@@ -123,8 +127,8 @@ class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests {
try {
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"),
queryMethod, converter, "travel-sample", new SpelExpressionParser(),
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries);
} catch (IllegalArgumentException e) {
return;
}
@@ -141,14 +145,33 @@ class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests {
try {
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"),
queryMethod, converter, "travel-sample", new SpelExpressionParser(),
QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT,
namedQueries);
} catch (IllegalArgumentException e) {
return;
}
fail("should have failed with IllegalArgumentException: query has no inline Query or named Query not found");
}
@Test
void spelTests() throws Exception {
String input = "spelTests";
Method method = UserRepository.class.getMethod(input);
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method,
new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(),
converter.getMappingContext());
StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method)), queryMethod,
converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries);
Query query = creator.createQuery();
String s = query.toN1qlSelectString(couchbaseTemplate.reactive(), "myScope", "myCollection", User.class, null,
false, null, null);
System.out.println("query: " + s);
}
private ParameterAccessor getAccessor(Parameters<?, ?> params, Object... values) {
return new ParametersParameterAccessor(params, values);
}