diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 94f1d2859..5e3471e3f 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -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 **** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 1e8197c53..a5774e974 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -169,7 +169,7 @@ public Mono first() { @Override public Flux all() { PseudoArgs 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 allResult = pArgs.getScope() == null ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, @@ -214,7 +214,7 @@ public QueryOptions buildOptions(QueryOptions options) { @Override public Mono count() { PseudoArgs 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 countResult = pArgs.getScope() == null ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, @@ -236,8 +236,8 @@ public Mono exists() { 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); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index d6ea991d6..a9f1db8e9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -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 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery @Override public Flux all() { PseudoArgs 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 allResult = pArgs.getScope() == null ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, @@ -119,8 +119,8 @@ public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency 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 diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java b/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java index 9ccd40fee..80df2eb57 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java @@ -73,13 +73,14 @@ public static String buildQuery(ReactiveCouchbaseTemplate template, Strin 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(); diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java b/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java index e70bd8f00..53835a40a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java @@ -209,6 +209,13 @@ public static N1QLExpression count(N1QLExpression expression) { 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. * diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java index ba354f93e..c5fefd6ec 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java @@ -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 JsonObject n1ql() { } @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(); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index e6fbc6bcd..808bf5392 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -338,13 +338,17 @@ public String export(int[]... paramIndexPtrHolder) { // used only by tests 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 String toN1qlSelectString(ReactiveCouchbaseTemplate template, String coll 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 String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String coll } 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 static StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(Reacti 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(); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java index 52ed05d13..372f6192d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java @@ -18,9 +18,20 @@ 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 @@ */ 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 String toN1qlSelectString(ReactiveCouchbaseTemplate template, String coll 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 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 String toN1qlSelectString(ReactiveCouchbaseTemplate template, String coll * @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); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java index 599f2d3df..36cf3c462 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java @@ -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 Object execute(final Object[] parameters) { 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()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java index 1f29b72d0..71d1074f5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java @@ -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 ReactiveStringBasedCouchbaseQuery(ReactiveCouchbaseQueryMethod method, 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(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java index 14799bd04..5fc2ca095 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java @@ -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 StringBasedCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperation 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()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index 168e53b41..d5f0765c9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -44,7 +44,6 @@ 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 { * "SELECT * FROM #{{@value SPEL_BUCKET}} LIMIT 3". */ 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. + * "SELECT * FROM #{{@value SPEL_SCOPE}}.#{{@value SPEL_COLLECTION}} LIMIT 3". + */ + 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. + * "SELECT * FROM #{{@value SPEL_SCOPE}}.#{{@value SPEL_COLLECTION}} LIMIT 3". + */ + 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 parameterNames = new HashSet(); + private final Collection 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 N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, 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 @@ private String getProjectedOrDistinctFields(String b, Class resultClass, String 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 @@ private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentPr // 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 @@ private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) { return namedValues; } - protected JsonValue getPlaceholderValues(ParameterAccessor accessor) { + public JsonValue getPlaceholderValues(ParameterAccessor accessor) { switch (this.placeHolderType) { case NAMED: return getNamedPlaceholderValues(accessor); @@ -483,10 +536,6 @@ protected boolean useGeneratedCountQuery() { 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 static final class N1qlSpelValues { */ public final String bucket; + /** + * #{{@value SPEL_SCOPE}. + * bucket will be replaced by (escaped) scope name in which the entity is stored. + */ + public final String scope; + + /** + * #{{@value SPEL_COLLECTION}. + * bucket will be replaced by (escaped) collection name in which the entity is stored. + */ + public final String collection; + /** * #{{@value SPEL_FILTER}}. * filter will be replaced by an expression allowing to select only entries matching the entity in a WHERE @@ -547,25 +608,37 @@ public static final class N1qlSpelValues { */ 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; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java index 4b9635377..659c226bb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java @@ -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 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.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 { + // 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 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 StringN1qlQueryCreator(final ParameterAccessor accessor, CouchbaseQueryMe 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 @@ protected QueryCriteria or(QueryCriteria base, QueryCriteria criteria) { @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; } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 31b5eab3c..9eacb99f9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -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.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 { List findByVersionEqualsAndFirstnameEquals(Long version, String firstname); + @Query("#{#n1ql.selectEntity}|#{#n1ql.filter}|#{#n1ql.bucket}|#{#n1ql.scope}|#{#n1ql.collection}") + @Scope("thisScope") + @Collection("thisCollection") + List spelTests(); + // simulate a slow operation @Cacheable("mySpringCache") default List getByFirstname(String firstname) { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java similarity index 94% rename from src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java rename to src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java index b14b712d8..3c35a8d92 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java @@ -66,9 +66,9 @@ * @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, CouchbasePersistentProperty> context; CouchbaseConverter converter; @@ -98,8 +98,8 @@ void findUsingStringNq1l() throws Exception { 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 @@ void findUsingStringNq1l_3x_projection_id_cas() throws Exception { 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(); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java index 3198e48d2..298779024 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java @@ -84,13 +84,15 @@ void createsQueryCorrectly() throws Exception { 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 @@ void createsQueryCorrectly2() throws Exception { 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 @@ void wrongNumberArgs() throws Exception { 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 @@ void doesNotHaveAnnotation() throws Exception { 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); }