diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index 8e6e603a..f147dda6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -221,7 +221,7 @@ public interface ExecutableFindByQueryOperation { } /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. * * @param the entity type to use for the results. */ @@ -254,12 +254,30 @@ public interface ExecutableFindByQueryOperation { FindByQueryWithConsistency as(Class returnType); } + /** + * Fluent method to specify fields to project. + * + * @param the entity type to use for the results. + */ + interface FindByQueryWithProjecting extends FindByQueryWithProjection { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param fields to project + * @return new instance of {@link ReactiveFindByQueryOperation.FindByQueryWithConsistency}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + FindByQueryWithProjection project(String[] fields); + } + /** * Fluent method to specify DISTINCT fields * * @param the entity type to use for the results. */ - interface FindByQueryWithDistinct extends FindByQueryWithProjection, WithDistinct { + interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithDistinct { /** * Finds the distinct values for a specified {@literal field} across a single collection diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index 4d5b2e93..cffdd571 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.stream.Stream; import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; -import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.couchbase.core.query.Query; import org.springframework.util.Assert; @@ -45,7 +44,7 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu @Override public ExecutableFindByQuery findByQuery(final Class domainType) { return new ExecutableFindByQuerySupport(template, domainType, domainType, ALL_QUERY, null, null, null, null, - null); + null, null); } static class ExecutableFindByQuerySupport implements ExecutableFindByQuery { @@ -60,22 +59,24 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu private final String collection; private final QueryOptions options; private final String[] distinctFields; + private final String[] fields; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, - final QueryOptions options, final String[] distinctFields) { + final QueryOptions options, final String[] distinctFields, final String[] fields) { this.template = template; this.domainType = domainType; this.returnType = returnType; this.query = query; this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, returnType, query, - scanConsistency, scope, collection, options, distinctFields, + scanConsistency, scope, collection, options, distinctFields, fields, new NonReactiveSupportWrapper(template.support())); this.scanConsistency = scanConsistency; this.scope = scope; this.collection = collection; this.options = options; this.distinctFields = distinctFields; + this.fields = fields; } @Override @@ -102,38 +103,47 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu scanCons = scanConsistency; } return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields); + options, distinctFields, fields); } @Override @Deprecated public FindByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, distinctFields, fields); } @Override public FindByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, distinctFields, fields); } @Override public FindByQueryWithConsistency as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, distinctFields, fields); + } + + @Override + public FindByQueryWithProjection project(String[] fields) { + Assert.notNull(fields, "Fields must not be null"); + Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields); } @Override public FindByQueryWithProjection distinct(final String[] distinctFields) { - Assert.notNull(distinctFields, "distinctFields must not be null!"); + Assert.notNull(distinctFields, "distinctFields must not be null"); + Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified"); // Coming from an annotation, this cannot be null. // But a non-null but empty distinctFields means distinct on all fields // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields); + collection, options, dFields, fields); } @Override @@ -144,8 +154,8 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu @Override public long count() { Long l = reactiveSupport.count().block(); - if ( l == null ){ - throw new CouchbaseQueryExecutionException("count query did not return a count : "+query.export()); + if (l == null) { + throw new CouchbaseQueryExecutionException("count query did not return a count : " + query.export()); } return l; } @@ -159,19 +169,19 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu public TerminatingFindByQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, distinctFields, fields); } @Override public FindByQueryInCollection inScope(final String scope) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, distinctFields, fields); } @Override public FindByQueryWithConsistency inCollection(final String collection) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, distinctFields, fields); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index c7a613b3..9a839ed7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -201,12 +201,30 @@ public interface ReactiveFindByQueryOperation { FindByQueryWithConsistency as(Class returnType); } + /** + * Fluent method to specify fields to project. + * + * @param the entity type to use for the results. + */ + interface FindByQueryWithProjecting extends FindByQueryWithProjection { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param fields to project + * @return new instance of {@link FindByQueryWithConsistency}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + FindByQueryWithProjection project(String[] fields); + } + /** * Fluent method to specify DISTINCT fields * * @param the entity type to use for the results. */ - interface FindByQueryWithDistinct extends FindByQueryWithProjection, WithDistinct { + interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithDistinct { /** * Finds the distinct values for a specified {@literal field} across a single {@link } or view. 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 ff181e7d..e87f3eae 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -50,7 +50,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO @Override public ReactiveFindByQuery findByQuery(final Class domainType) { return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, null, null, null, null, - template.support()); + null, template.support()); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @@ -63,12 +63,13 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO private final String collection; private final String scope; private final String[] distinctFields; + private final String[] fields; private final QueryOptions options; private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, + final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); @@ -81,6 +82,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO this.collection = collection; this.options = options; this.distinctFields = distinctFields; + this.fields = fields; this.support = support; } @@ -94,57 +96,65 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO scanCons = scanConsistency; } return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, support); + options, distinctFields, fields, support); } @Override public TerminatingFindByQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, distinctFields, fields, support); } @Override public FindByQueryInCollection inScope(final String scope) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, distinctFields, fields, support); } @Override public FindByQueryWithConsistency inCollection(final String collection) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, distinctFields, fields, support); } @Override @Deprecated public FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, distinctFields, fields, support); } @Override public FindByQueryWithConsistency withConsistency(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, distinctFields, fields, support); } - @Override public FindByQueryWithConsistency as(Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, distinctFields, fields, support); + } + + @Override + public FindByQueryWithProjection project(String[] fields) { + Assert.notNull(fields, "Fields must not be null"); + Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields, support); } @Override public FindByQueryWithDistinct distinct(final String[] distinctFields) { - Assert.notNull(distinctFields, "distinctFields must not be null!"); + Assert.notNull(distinctFields, "distinctFields must not be null"); + Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified"); // Coming from an annotation, this cannot be null. // But a non-null but empty distinctFields means distinct on all fields // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, support); + collection, options, dFields, fields, support); } @Override @@ -228,7 +238,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO private String assembleEntityQuery(final boolean count, String[] distinctFields, String collection) { return query.toN1qlSelectString(template, collection, this.domainType, this.returnType, count, - query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields); + query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields, fields); } } } 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 a1388fab..7942aaf3 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 @@ -320,13 +320,13 @@ public class Query { } public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) { - return toN1qlSelectString(template, null, domainClass, null, isCount, null); + return toN1qlSelectString(template, null, domainClass, null, isCount, null, null); } public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass, - Class returnClass, boolean isCount, String[] distinctFields) { + Class returnClass, boolean isCount, String[] distinctFields, String[] fields) { StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass, - returnClass, isCount, distinctFields); + returnClass, isCount, distinctFields, fields); final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.selectEntity); // select ... appendWhereString(statement, n1ql.filter); // typeKey = typeValue @@ -340,7 +340,7 @@ public class Query { public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) { StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass, null, - false, null); + false, null, null); final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.delete); // delete ... appendWhereString(statement, n1ql.filter); // typeKey = typeValue @@ -350,7 +350,7 @@ public class Query { } StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, String collectionName, - Class domainClass, Class returnClass, boolean isCount, String[] distinctFields) { + Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, String[] fields) { String typeKey = template.getConverter().getTypeKey(); final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext() .getRequiredPersistentEntity(domainClass); @@ -363,7 +363,7 @@ public class Query { } StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(template.getBucketName(), collectionName, - template.getConverter(), domainClass, returnClass, typeKey, typeValue, distinctFields); + template.getConverter(), domainClass, returnClass, typeKey, typeValue, isCount, distinctFields, fields); return isCount ? sbnqp.getCountContext() : 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 58f2f7dd..47248e62 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 @@ -15,13 +15,13 @@ */ package org.springframework.data.couchbase.core.query; +import java.util.Locale; + import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.support.TemplateUtils; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonValue; -import org.springframework.data.couchbase.core.support.TemplateUtils; - -import java.util.Locale; /** * Query created from the string in @Query annotation in the repository interface. @@ -56,7 +56,7 @@ public class StringQuery extends Query { @Override public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collection, Class domainClass, - Class resultClass, boolean isCount, String[] distinctFields) { + Class resultClass, boolean isCount, String[] distinctFields, String[] fields) { final StringBuilder statement = new StringBuilder(); boolean makeCount = isCount && inlineN1qlQuery != null && !inlineN1qlQuery.toLowerCase(Locale.ROOT).contains("count("); @@ -95,6 +95,6 @@ public class StringQuery extends Query { */ @Override public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) { - return toN1qlSelectString(template, collectionName, domainClass, domainClass, false, null); + return toN1qlSelectString(template, collectionName, domainClass, domainClass, false, null, null); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java b/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java new file mode 100644 index 00000000..72f95b29 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.support; + +/** + * A common interface for all of Insert, Replace, Upsert that take Projection + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithProjecting { + Object project(String[] fields); + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java index 9dc3fe51..115d1872 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java @@ -87,7 +87,7 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< CouchbaseQueryExecution execution = getExecution(accessor, new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find); - return execution.execute(query, processor.getReturnedType().getDomainType(), null); + return execution.execute(query, processor.getReturnedType().getDomainType(), typeToRead, null); } /** @@ -115,20 +115,21 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< if (isDeleteQuery()) { return new DeleteExecution(getOperations(), getQueryMethod()); } else if (isTailable(getQueryMethod())) { - return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of all() + return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of + // all() } else if (getQueryMethod().isCollectionQuery()) { - return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); + return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all(); } else if (getQueryMethod().isStreamQuery()) { - return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).stream(); + return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).stream(); } else if (isCountQuery()) { - return (q, t, c) -> operation.matching(q).count(); + return (q, t, r, c) -> operation.as(r).matching(q).count(); } else if (isExistsQuery()) { - return (q, t, c) -> operation.matching(q).exists(); + return (q, t, r, c) -> operation.as(r).matching(q).exists(); } else if (getQueryMethod().isPageQuery()) { return new PagedExecution(operation, accessor.getPageable()); } else { - return (q, t, c) -> { - TerminatingFindByQuery find = operation.matching(q); + return (q, t, r, c) -> { + TerminatingFindByQuery find = operation.as(r).matching(q); if (isCountQuery()) { return find.count(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java index 8b04d292..878e44a8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java @@ -87,7 +87,7 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu ReactiveCouchbaseQueryExecution execution = getExecution(accessor, new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find); - return execution.execute(query, processor.getReturnedType().getDomainType(), null); + return execution.execute(query, processor.getReturnedType().getDomainType(), typeToRead, null); } /** @@ -98,7 +98,7 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu * @return */ private ReactiveCouchbaseQueryExecution getExecution(ParameterAccessor accessor, - Converter resultProcessing, ReactiveFindByQueryOperation.FindByQueryWithQuery operation) { + Converter resultProcessing, ReactiveFindByQuery operation) { return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing); } @@ -110,23 +110,24 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu * @return */ private ReactiveCouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, - ReactiveFindByQueryOperation.FindByQueryWithQuery operation) { + ReactiveFindByQuery operation) { if (isDeleteQuery()) { return new DeleteExecution(getOperations(), getQueryMethod()); } else if (isTailable(getQueryMethod())) { - return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of all() + return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of + // all() } else if (getQueryMethod().isCollectionQuery()) { - return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); + return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all(); // } else if (getQueryMethod().isStreamQuery()) { // return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all().toStream(); } else if (isCountQuery()) { - return (q, t, c) -> operation.matching(q).count(); + return (q, t, r, c) -> operation.as(r).matching(q).count(); } else if (isExistsQuery()) { - return (q, t, c) -> operation.matching(q).exists(); + return (q, t, r, c) -> operation.as(r).matching(q).exists(); } else { - return (q, t, c) -> { - ReactiveFindByQueryOperation.TerminatingFindByQuery find = operation.matching(q); + return (q, t, r, c) -> { + ReactiveFindByQueryOperation.TerminatingFindByQuery find = operation.as(r).matching(q); return isLimiting() ? find.first() : find.one(); }; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java index 599f38ee..9a98018f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java @@ -39,7 +39,7 @@ import org.springframework.util.Assert; @FunctionalInterface interface CouchbaseQueryExecution { - Object execute(Query query, Class type, String collection); + Object execute(Query query, Class type, Class returnType, String collection); /** * {@link CouchbaseQueryExecution} removing documents matching the query. @@ -60,7 +60,7 @@ interface CouchbaseQueryExecution { * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery.Execution#execute(org.springframework.data.couchbase.core.query.Query, java.lang.Class, java.lang.String) */ @Override - public Object execute(Query query, Class type, String collection) { + public Object execute(Query query, Class type, Class returnType, String collection) { return operations.removeByQuery(type).matching(query).all(); } @@ -83,8 +83,8 @@ interface CouchbaseQueryExecution { } @Override - public Object execute(Query query, Class type, String collection) { - return converter.convert(delegate.execute(query, type, collection)); + public Object execute(Query query, Class type, Class returnType, String collection) { + return converter.convert(delegate.execute(query, type, returnType, collection)); } } @@ -109,11 +109,11 @@ interface CouchbaseQueryExecution { */ @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Object execute(Query query, Class type, String collection) { + public Object execute(Query query, Class type, Class returnType, String collection) { int pageSize = pageable.getPageSize(); // Apply Pageable but tweak limit to peek into next page Query modifiedQuery = query.skip(pageable.getOffset()).limit(pageSize + 1); - List result = find.matching(modifiedQuery).all(); + List result = find.as(returnType).matching(modifiedQuery).all(); boolean hasNext = result.size() > pageSize; return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); } @@ -139,9 +139,9 @@ interface CouchbaseQueryExecution { * @see org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution#execute(org.springframework.data.couchbase.core.query.Query) */ @Override - public Object execute(Query query, Class type, String collection) { + public Object execute(Query query, Class type, Class returnType, String collection) { int overallLimit = 0; // query.getLimit(); - TerminatingFindByQuery matching = operation.matching(query); + TerminatingFindByQuery matching = operation.as(returnType).matching(query); // Adjust limit if page would exceed the overall limit if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) { query.limit((int) (overallLimit - pageable.getOffset())); 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 020c1c8b..599f2d3d 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 @@ -80,7 +80,7 @@ public class N1qlRepositoryQueryExecutor { return operation.matching(query).all(); } else if (queryMethod.isPageQuery()) { Pageable p = accessor.getPageable(); - return new CouchbaseQueryExecution.PagedExecution(operation, p).execute(query, null, null); + return new CouchbaseQueryExecution.PagedExecution(operation, p).execute(query, null, null, null); } else { return operation.matching(query).oneValue(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java index 953c25db..7194c695 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java @@ -28,9 +28,10 @@ import org.springframework.util.Assert; * @author Michael Reiche * @since 4.1 */ +@FunctionalInterface interface ReactiveCouchbaseQueryExecution { - Object execute(Query query, Class type, String collection); + Object execute(Query query, Class type, Class returnType, String collection); /** * {@link ReactiveCouchbaseQueryExecution} removing documents matching the query. @@ -51,7 +52,7 @@ interface ReactiveCouchbaseQueryExecution { * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery.Execution#execute(org.springframework.data.couchbase.core.query.Query, java.lang.Class, java.lang.String) */ @Override - public Object execute(Query query, Class type, String collection) { + public Object execute(Query query, Class type, Class returnType, String collection) { return operations.removeByQuery(type)/*.inCollection(collection)*/.matching(query).all(); } @@ -74,8 +75,8 @@ interface ReactiveCouchbaseQueryExecution { } @Override - public Object execute(Query query, Class type, String collection) { - return converter.convert(delegate.execute(query, type, collection)); + public Object execute(Query query, Class type, Class returnType, String collection) { + return converter.convert(delegate.execute(query, type, returnType, collection)); } } 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 f779ea41..0013bf47 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 @@ -21,12 +21,15 @@ import static org.springframework.data.couchbase.core.support.TemplateUtils.SELE import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.couchbase.client.core.error.CouchbaseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -127,28 +130,38 @@ public class StringBasedN1qlQueryParser { this.queryMethod = queryMethod; this.couchbaseConverter = couchbaseConverter; String collection = queryMethod.getCollection(); - this.statementContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, false, null); - this.countContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, true, null); + this.statementContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, false, null, + null); + this.countContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, true, null, + null); this.parsedExpression = getExpression(accessor, getParameters(accessor), null, parser, evaluationContextProvider); checkPlaceholders(this.parsedExpression.toString()); } public StringBasedN1qlQueryParser(String bucketName, String collection, CouchbaseConverter couchbaseConverter, - Class domainClass, Class resultClass, String typeField, String typeValue, String[] distinctFields) { + Class domainClass, Class resultClass, String typeField, String typeValue, boolean isCount, + String[] distinctFields, String[] fields) { this.statement = null; this.queryMethod = null; this.couchbaseConverter = couchbaseConverter; - this.statementContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue, - false, distinctFields); - this.countContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue, - true, distinctFields); + 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.parsedExpression = null; } public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, Class domainClass, Class resultClass, - String typeField, String typeValue, boolean isCount, String[] distinctFields) { + String typeField, String typeValue, boolean isCount, String[] distinctFields, String[] fields) { String b = collection != null ? collection : bucketName; - String projectedFields = getProjectedFields(b, resultClass); + Assert.isTrue(!(distinctFields != null && fields != null), + "only one of project(fields) and distinct(distinctFields) can be specified"); + String projectedFields = getProjectedFields(b, resultClass, fields); String entity = "META(" + i(b) + ").id AS " + SELECT_ID + ", META(" + i(b) + ").cas AS " + SELECT_CAS; String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS; String selectEntity; @@ -163,7 +176,7 @@ public class StringBasedN1qlQueryParser { } else if (isCount) { selectEntity = "SELECT " + count + " FROM " + i(b); } else { - selectEntity = "SELECT " + entity + ", " + projectedFields + " FROM " + i(b); + selectEntity = "SELECT " + entity + (!projectedFields.isEmpty() ? ", " : " ") + projectedFields + " FROM " + i(b); } String typeSelection = "`" + typeField + "` = \"" + typeValue + "\""; @@ -177,50 +190,77 @@ public class StringBasedN1qlQueryParser { return i(distinctFields).toString(); } - private String getProjectedFields(String b, Class resultClass) { + private String getProjectedFields(String b, Class resultClass, String[] fields) { String projectedFields = i(b) + ".*"; if (resultClass != null) { PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); StringBuilder sb = new StringBuilder(); - getProjectedFieldsInternal(b, null, sb, persistentEntity.getTypeInformation()/*, ""*/); + getProjectedFieldsInternal(b, null, sb, persistentEntity.getTypeInformation(), fields/*, ""*/); projectedFields = sb.toString(); } return projectedFields; } private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentProperty parent, StringBuilder sb, - TypeInformation resultClass/*, String path*/) { + TypeInformation resultClass, String[] fields/*, String path*/) { - PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); - // CouchbasePersistentProperty property = path.getLeafProperty(); - persistentEntity.doWithProperties(new PropertyHandler() { - @Override - public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { - if (prop.isIdProperty() && parent == null) { - return; + if (resultClass != null) { + Set fieldList = fields != null ? new HashSet<>(Arrays.asList(fields)) : null; + PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); + // CouchbasePersistentProperty property = path.getLeafProperty(); + + persistentEntity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.isIdProperty() && parent == null) { + return; + } + if (prop.isVersionProperty()) { + return; + } + String projectField = null; + + if (fieldList == null || fieldList.contains(prop.getFieldName())) { + PersistentPropertyPath path = couchbaseConverter.getMappingContext() + .getPersistentPropertyPath(prop.getName(), resultClass.getType()); + projectField = N1qlQueryCreator.addMetaIfRequired(bucketName, path, prop).toString(); + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(projectField); // from N1qlQueryCreator + } + + if (fieldList != null) { + fieldList.remove(prop.getFieldName()); + } + // The current limitation is that only top-level properties can be projected + // This traversing of nested data structures would need to replicate the processing done by + // MappingCouchbaseConverter. Either the read or write + // And the n1ql to project lower-level properties is complex + + // if (!conversions.isSimpleType(prop.getType())) { + // getProjectedFieldsInternal(prop, sb, prop.getTypeInformation(), path+prop.getName()+"."); + // } else { + + // } } - if (prop.isVersionProperty()) { - return; - } - PersistentPropertyPath path = couchbaseConverter.getMappingContext() - .getPersistentPropertyPath(prop.getName(), resultClass.getType()); - - // The current limitation is that only top-level properties can be projected - // This traversing of nested data structures would need to replicate the processing done by - // MappingCouchbaseConverter. Either the read or write - // And the n1ql to project lower-level properties is complex - - // if (!conversions.isSimpleType(prop.getType())) { - // getProjectedFieldsInternal(prop, sb, prop.getTypeInformation(), path+prop.getName()+"."); - // } else { + }); + // throw an exception if there is an request for a field not in the entity. + // needs further discussion as removing a field from an entity could cause this and not necessarily be an error + if (fieldList != null && !fieldList.isEmpty()) { + throw new CouchbaseException( + "projected fields (" + fieldList + ") not found in entity: " + persistentEntity.getName()); + } + } else { + for (String field : fields) { if (sb.length() > 0) { sb.append(", "); } - sb.append(N1qlQueryCreator.addMetaIfRequired(bucketName, path, prop)); // from N1qlQueryCreator - // } + sb.append(x(field)); } - }); + + } } // this static method can be used to test the parsing behavior for Couchbase specific spel variables diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 8e08c089..5e0c4b60 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -27,10 +27,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; +import com.couchbase.client.core.error.CouchbaseException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.dao.DataIntegrityViolationException; @@ -38,18 +41,23 @@ import org.springframework.dao.DuplicateKeyException; import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById; import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById; import org.springframework.data.couchbase.core.support.OneAndAllEntity; +import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.PersonValue; +import org.springframework.data.couchbase.domain.Submission; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserAnnotated; import org.springframework.data.couchbase.domain.UserAnnotated2; import org.springframework.data.couchbase.domain.UserAnnotated3; +import org.springframework.data.couchbase.domain.UserSubmission; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; ; @@ -86,8 +94,8 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { modifying.setVersion(user.getVersion()); modified = couchbaseTemplate.replaceById(User.class).one(modifying); assertEquals(modifying, modified); - if(user == modified){ - throw new RuntimeException ( " user == modified "); + if (user == modified) { + throw new RuntimeException(" user == modified "); } assertNotEquals(user, modified); assertEquals(NaiveAuditorAware.AUDITOR, modified.getCreatedBy()); @@ -107,6 +115,52 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { couchbaseTemplate.removeById().one(user.getId()); } + @Test + void findProjected() { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + couchbaseTemplate.insertById(User.class).one(user); + User found = couchbaseTemplate.findById(User.class).project(new String[] { "firstname" }).one(user.getId()); + System.err.println(found); + couchbaseTemplate.removeById(User.class).one(user.getId()); + } + + @Test + void findProjecting() { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + couchbaseTemplate.insertById(User.class).one(user); + List found = couchbaseTemplate.findByQuery(User.class).project(new String[] { "firstname" }) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)).all(); + assertEquals(1, found.size()); + assertNotEquals(user, found.get(0), "should have found this document"); + assertEquals(user.getFirstname(), found.get(0).getFirstname(), "firstname should match"); + assertNull(found.get(0).getLastname(), "lastname should be null"); + couchbaseTemplate.removeById(User.class).one(user.getId()); + } + + @Test + void findProjectingPath() { + UserSubmission user = new UserSubmission(); + user.setId(UUID.randomUUID().toString()); + user.setUsername("dave"); + user.setRoles(Arrays.asList("role1", "role2")); + Address address = new Address(); + address.setStreet("1234 Olcott Street"); + address.setCity("Santa Clara"); + user.setAddress(address); + user.setSubmissions( + Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123))); + couchbaseTemplate.upsertById(UserSubmission.class).one(user); + assertThrows(CouchbaseException.class, () -> couchbaseTemplate.findByQuery(UserSubmission.class).project(new String[] { "address.street" }) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all()); + + List found = couchbaseTemplate.findByQuery(UserSubmission.class).project(new String[] { "address" }) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + assertEquals(found.size(), 1); + assertEquals(found.get(0).getAddress(), address); + assertNull(found.get(0).getUsername(), "username should have been null"); + couchbaseTemplate.removeById(User.class).one(user.getId()); + } + @Test void withDurability() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java b/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java new file mode 100644 index 00000000..fbec7196 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.domain; + +import java.util.Objects; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.couchbase.core.mapping.Document; + +/** + * AirportMini entity + * + * @author Michael Reiche + */ +@Document +public class AirportMini extends ComparableEntity { + + @Id private String id; + private String iata; + private Address address; + + @PersistenceConstructor + public AirportMini(final String id, final String iata) { + this.id = id; + this.iata = iata; + } + + public String getId() { + return id; + } + + public String getIata() { + return iata; + } + + public void setIata(String iata) { + this.iata = iata; + } + + @Override + public int hashCode() { + return Objects.hash(id, iata); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index ff3afe40..7bdae87d 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -68,6 +68,9 @@ public interface AirportRepository extends CouchbaseRepository, @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List findAllByIata(String iata); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + List getByIata(String iata); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) @ComposedMetaAnnotation(collection = "_default", timeoutMs = 1000) Airport findByIata(String iata); diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index 74805da9..e8b160d3 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -56,6 +56,7 @@ import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.AirportMini; import org.springframework.data.couchbase.domain.AirportRepository; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Person; @@ -239,6 +240,20 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr } } + @Test + void findBySimplePropertyReturnType() { + Airport vie = null; + try { + vie = new Airport("airports::vie", "vie", "low6"); + vie = airportRepository.save(vie); + List airports = airportRepository.getByIata("vie"); + assertEquals(1, airports.size()); + System.out.println(airports.get(0)); + } finally { + airportRepository.delete(vie); + } + } + @Test void findByTypeAlias() { Airport vie = null;