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 46859bd89..58e6a317d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -21,6 +21,7 @@ import org.springframework.data.couchbase.core.query.Query; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQueryOperation { @@ -50,8 +51,8 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery this.template = template; this.domainType = domainType; this.query = query; - this.reactiveSupport = new ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport( - template.reactive(), domainType, query, scanConsistency); + this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), + domainType, query, scanConsistency); this.scanConsistency = scanConsistency; } @@ -72,7 +73,13 @@ public List all() { @Override public TerminatingFindByQuery matching(final Query query) { - return new ExecutableFindByQuerySupport<>(template, domainType, query, scanConsistency); + QueryScanConsistency scanCons; + if (query.getConsistency() != null) { + scanCons = query.getConsistency(); + } else { + scanCons = scanConsistency; + } + return new ExecutableFindByQuerySupport<>(template, domainType, query, scanCons); } @Override 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 50e333a97..363d63bb0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -104,6 +104,129 @@ interface FindByQueryConsistentWith extends FindByQueryWithQuery { } - interface ReactiveFindByQuery extends FindByQueryConsistentWith {} + /** + * Collection override (optional). + */ + interface FindWithCollection extends FindByQueryWithQuery { + + /** + * Explicitly set the name of the collection to perform the query on.
+ * Skip this step to use the default collection derived from the domain type. + * + * @param collection must not be {@literal null} nor {@literal empty}. + * @return new instance of {@link FindWithProjection}. + * @throws IllegalArgumentException if collection is {@literal null}. + */ + FindWithProjection inCollection(String collection); + } + + /** + * Result type override (optional). + */ + interface FindWithProjection extends FindByQueryWithQuery, FindDistinct { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link FindWithProjection}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + FindByQueryWithQuery as(Class resultType); + } + + /** + * Distinct Find support. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface FindDistinct { + + /** + * Finds the distinct values for a specified {@literal field} across a single {@link } or view. + * + * @param field name of the field. Must not be {@literal null}. + * @return new instance of {@link TerminatingDistinct}. + * @throws IllegalArgumentException if field is {@literal null}. + */ + TerminatingDistinct distinct(String field); + } + + /** + * Result type override. Optional. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface DistinctWithProjection { + + /** + * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion. + *
+ *
{@link Object} (the default)
+ *
Result is mapped according to the {@link } converting eg. {@link } into plain {@link String}, {@link } to + * {@link Long}, etc. always picking the most concrete type with respect to the domain types property.
+ * Any {@link } is run through the {@link org.springframework.data.convert.EntityReader} to obtain the domain type. + *
+ * Using {@link Object} also works for non strictly typed fields. Eg. a mixture different types like fields using + * {@link String} in one {@link } while {@link Long} in another.
+ *
Any Simple type like {@link String}, {@link Long}, ...
+ *
The result is mapped directly by the Couchbase Java driver and the {@link } in place. This works only for + * results where all documents considered for the operation use the very same type for the field.
+ *
Any Domain type
+ *
Domain types can only be mapped if the if the result of the actual {@code distinct()} operation returns + * {@link }.
+ *
{@link }
+ *
Using {@link } allows retrieval of the raw driver specific format, which returns eg. {@link }.
+ *
+ * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link TerminatingDistinct}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + TerminatingDistinct as(Class resultType); + } + + /** + * Result restrictions. Optional. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface DistinctWithQuery extends DistinctWithProjection { + + /** + * Set the filter {@link Query criteria} to be used. + * + * @param query must not be {@literal null}. + * @return new instance of {@link TerminatingDistinct}. + * @throws IllegalArgumentException if criteria is {@literal null}. + * @since 3.0 + */ + TerminatingDistinct matching(Query query); + } + + /** + * Terminating distinct find operations. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface TerminatingDistinct extends DistinctWithQuery { + + /** + * Get all matching distinct field values. + * + * @return empty {@link Flux} if not match found. Never {@literal null}. + */ + Flux all(); + } + + interface ReactiveFindByQuery extends FindByQueryConsistentWith, FindWithCollection, FindDistinct {} } 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 09f8d81ab..79f946f16 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -60,7 +60,13 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @Override public TerminatingFindByQuery matching(Query query) { - return new ReactiveFindByQuerySupport<>(template, domainType, query, scanConsistency); + QueryScanConsistency scanCons; + if (query.getConsistency() != null) { + scanCons = query.getConsistency(); + } else { + scanCons = scanConsistency; + } + return new ReactiveFindByQuerySupport<>(template, domainType, query, scanCons); } @Override @@ -94,6 +100,7 @@ public Flux all() { long cas = row.getLong(TemplateUtils.SELECT_CAS); row.removeKey(TemplateUtils.SELECT_ID); row.removeKey(TemplateUtils.SELECT_CAS); + System.out.println("Row -> " + row.toString()); return template.support().decodeEntity(id, row.toString(), cas, domainType); }); }); @@ -110,8 +117,10 @@ public Mono count() { } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(TemplateUtils.SELECT_COUNT)) - .next(); + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { + System.out.println("count: " + row.getLong(TemplateUtils.SELECT_COUNT)); + return row.getLong(TemplateUtils.SELECT_COUNT); + }).next(); }); } @@ -124,6 +133,15 @@ private String assembleEntityQuery(final boolean count) { return query.toN1qlString(template, this.domainType, count); } + @Override + public FindWithProjection inCollection(String collection) { + throw new RuntimeException(("not implemented")); + } + + @Override + public TerminatingDistinct distinct(String field) { + throw new RuntimeException(("not implemented")); + } } } 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 5b154ec1f..3e0a0d2df 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 @@ -47,6 +47,7 @@ public class Query { private long skip; private int limit; private Sort sort = Sort.unsorted(); + private QueryScanConsistency queryScanConsistency; static private final Pattern WHERE_PATTERN = Pattern.compile("\\sWHERE\\s"); @@ -104,13 +105,22 @@ public Query skip(long skip) { * Limit the number of returned documents to {@code limit}. * * @param limit - * @return + * @return this */ public Query limit(int limit) { this.limit = limit; return this; } + /** + * limit + * + * @return limit + */ + public int getLimit() { + return limit; + } + /** * Sets the given pagination information on the {@link Query} instance. Will transparently set {@code skip} and * {@code limit} as well as applying the {@link Sort} instance defined with the {@link Pageable}. @@ -127,6 +137,27 @@ public Query with(final Pageable pageable) { return with(pageable.getSort()); } + /** + * queryScanconsistency + * + * @return queryScanConsistency + */ + public QueryScanConsistency getConsistency() { + return queryScanConsistency; + } + + /** + * Sets the given pagination information on the {@link Query} instance. Will transparently set {@code skip} and + * {@code limit} as well as applying the {@link Sort} instance defined with the {@link Pageable}. + * + * @param queryScanConsistency + * @return + */ + public Query with(final QueryScanConsistency queryScanConsistency) { + this.queryScanConsistency = queryScanConsistency; + return this; + } + /** * Adds a {@link Sort} to the {@link Query} instance. * 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 new file mode 100644 index 000000000..cf56e18de --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java @@ -0,0 +1,251 @@ +package org.springframework.data.couchbase.repository.query; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.*; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public abstract class AbstractCouchbaseQuery implements RepositoryQuery { + + private final CouchbaseQueryMethod method; + private final CouchbaseOperations operations; + private final EntityInstantiators instantiators; + // private final FindWithProjection findOperationWithProjection; + private final ExecutableFindByQueryOperation.ExecutableFindByQuery findOperationWithProjection; + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /** + * Creates a new {@link AbstractCouchbaseQuery} from the given {@link ReactiveCouchbaseQueryMethod} and + * {@link org.springframework.data.couchbase.core.CouchbaseOperations}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public AbstractCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); + Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + + this.method = method; + this.operations = operations; + this.instantiators = new EntityInstantiators(); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + EntityMetadata metadata = method.getEntityInformation(); + Class type = metadata.getJavaType(); + + this.findOperationWithProjection = operations.findByQuery(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + */ + public CouchbaseQueryMethod getQueryMethod() { + return method; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + */ + public Object execute(Object[] parameters) { + + return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) + : execute(new ReactiveCouchbaseParameterAccessor(method, parameters)); + } + + @SuppressWarnings("unchecked") + private Object executeDeferred(Object[] parameters) { + + ReactiveCouchbaseParameterAccessor parameterAccessor = new ReactiveCouchbaseParameterAccessor(method, parameters); + + if (getQueryMethod().isCollectionQuery()) { + return Flux.defer(() -> (Publisher) execute(parameterAccessor)); + } + + return Mono.defer(() -> (Mono) execute(parameterAccessor)); + } + + private Object execute(ParametersParameterAccessor parameterAccessor) { + + // ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(), + // parameterAccessor); + + TypeInformation returnType = ClassTypeInformation + .from(method.getResultProcessor().getReturnedType().getReturnedType()); + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor); + Class typeToRead = processor.getReturnedType().getTypeToRead(); + + if (typeToRead == null && returnType.getComponentType() != null) { + typeToRead = returnType.getComponentType().getType(); + } + + return doExecute(method, processor, parameterAccessor, typeToRead); + } + + /** + * Execute the {@link RepositoryQuery} of the given method with the parameters provided by the + * {@link ParametersParameterAccessor accessor} + * + * @param method the {@link ReactiveCouchbaseQueryMethod} invoked. Never {@literal null}. + * @param processor {@link ResultProcessor} for post procession. Never {@literal null}. + * @param accessor for providing invocation arguments. Never {@literal null}. + * @param typeToRead the desired component target type. Can be {@literal null}. + */ + protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processor, + ParametersParameterAccessor accessor, @Nullable Class typeToRead) { + + Query query = createQuery(accessor); + + query = applyAnnotatedConsistencyIfPresent(query); + // query = applyAnnotatedCollationIfPresent(query, accessor); + + ExecutableFindByQueryOperation.ExecutableFindByQuery find = typeToRead == null // + ? findOperationWithProjection // + : findOperationWithProjection; // TODO .as(typeToRead); + + String collection = "_default._default";// method.getEntityInformation().getCollectionName(); + + CouchbaseQueryExecution execution = getExecution(accessor, + new CouchbaseQueryExecution.ResultProcessingConverter(processor, getOperations(), instantiators), find); + return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + } + + /** + * Returns the execution instance to use. + * + * @param accessor must not be {@literal null}. + * @param resultProcessing must not be {@literal null}. + * @return + */ + private CouchbaseQueryExecution getExecution(ParameterAccessor accessor, Converter resultProcessing, + ExecutableFindByQueryOperation.ExecutableFindByQuery operation) { + return new CouchbaseQueryExecution.ResultProcessingExecution(getExecutionToWrap(accessor, operation), + resultProcessing); + } + + private CouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, + ExecutableFindByQueryOperation.ExecutableFindByQuery operation) { + + if (isDeleteQuery()) { + return new CouchbaseQueryExecution.DeleteExecution(getOperations(), method); + // } else if (isTailable(method)) { + // return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).tail(); + } else if (method.isCollectionQuery()) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); + } else if (isCountQuery()) { + return (q, t, c) -> operation.matching(q).count(); + } else if (isExistsQuery()) { + return (q, t, c) -> operation.matching(q).exists(); + } else if (method.isPageQuery()) { + return new CouchbaseQueryExecution.PagedExecution(operation, accessor.getPageable()); + } else { + return (q, t, c) -> { + + ExecutableFindByQueryOperation.TerminatingFindByQuery find = operation.matching(q); + + if (isCountQuery()) { + return find.count(); + } + + return isLimiting() ? find.first() : find.one(); + }; + } + } + + private boolean isTailable(ReactiveCouchbaseQueryMethod method) { + return false; // method.getTailableAnnotation() != null; + } + + /** + * Add a scan consistency from {@link org.springframework.data.couchbase.repository.ScanConsistency} to the given + * {@link Query} if present. + * + * @param query the {@link Query} to potentially apply the sort to. + * @return the query with potential scan consistency applied. + * @since 4.1 + */ + Query applyAnnotatedConsistencyIfPresent(Query query) { + + if (!method.hasScanConsistencyAnnotation()) { + return query; + } + return query.with(method.getScanConsistencyAnnotation().query()); + } + + /** + * Creates a {@link Query} instance using the given {@link ParametersParameterAccessor}. Will delegate to + * {@link #createQuery(ParametersParameterAccessor)} by default but allows customization of the count query to be + * triggered. + * + * @param accessor must not be {@literal null}. + * @return + */ + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return /*applyQueryMetaAttributesWhenPresent*/(createQuery(accessor)); + } + + /** + * Creates a {@link Query} instance using the given {@link ParameterAccessor} + * + * @param accessor must not be {@literal null}. + * @return + */ + protected abstract Query createQuery(ParametersParameterAccessor accessor); + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isCountQuery() + */ + protected boolean isCountQuery() { + return getQueryMethod().isCountQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isExistsQuery() + */ + + protected boolean isExistsQuery() { + return getQueryMethod().isExistsQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isDeleteQuery() + */ + protected boolean isDeleteQuery() { + return getQueryMethod().isDeleteQuery(); + } + + /** + * Return whether the query has an explicit limit set. + * + * @return + * @since 2.0.4 + */ + protected abstract boolean isLimiting(); + + public CouchbaseOperations getOperations() { + return operations; + } +} 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 new file mode 100644 index 000000000..c695d5cd4 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java @@ -0,0 +1,275 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation.ReactiveFindByQuery; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.*; +import org.springframework.data.util.ClassTypeInformation; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.model.EntityInstantiators; + +import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseQueryExecution.DeleteExecution; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for reactive {@link RepositoryQuery} implementations for Couchbase. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.0 + */ +public abstract class AbstractReactiveCouchbaseQuery implements RepositoryQuery { + + private final ReactiveCouchbaseQueryMethod method; + private final ReactiveCouchbaseOperations operations; + private final EntityInstantiators instantiators; + // private final FindWithProjection findOperationWithProjection; + private final ReactiveFindByQuery findOperationWithProjection; + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /** + * Creates a new {@link AbstractReactiveCouchbaseQuery} from the given {@link ReactiveCouchbaseQueryMethod} and + * {@link org.springframework.data.couchbase.core.CouchbaseOperations}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public AbstractReactiveCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); + Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + + this.method = method; + this.operations = operations; + this.instantiators = new EntityInstantiators(); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + EntityMetadata metadata = method.getEntityInformation(); + Class type = metadata.getJavaType(); + + this.findOperationWithProjection = operations.findByQuery(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + */ + public ReactiveCouchbaseQueryMethod getQueryMethod() { + return method; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + */ + public Object execute(Object[] parameters) { + + return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) + : execute(new ReactiveCouchbaseParameterAccessor(method, parameters)); + } + + @SuppressWarnings("unchecked") + private Object executeDeferred(Object[] parameters) { + + ReactiveCouchbaseParameterAccessor parameterAccessor = new ReactiveCouchbaseParameterAccessor(method, parameters); + + if (getQueryMethod().isCollectionQuery()) { + return Flux.defer(() -> (Publisher) execute(parameterAccessor)); + } + + return Mono.defer(() -> (Mono) execute(parameterAccessor)); + } + + private Object execute(ReactiveCouchbaseParameterAccessor parameterAccessor) { + + // ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(), + // parameterAccessor); + + TypeInformation returnType = ClassTypeInformation + .from(method.getResultProcessor().getReturnedType().getReturnedType()); + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor); + Class typeToRead = processor.getReturnedType().getTypeToRead(); + + if (typeToRead == null && returnType.getComponentType() != null) { + typeToRead = returnType.getComponentType().getType(); + } + + return doExecute(method, processor, parameterAccessor, typeToRead); + } + + /** + * Execute the {@link RepositoryQuery} of the given method with the parameters provided by the + * {@link ParametersParameterAccessor accessor} + * + * @param method the {@link ReactiveCouchbaseQueryMethod} invoked. Never {@literal null}. + * @param processor {@link ResultProcessor} for post procession. Never {@literal null}. + * @param accessor for providing invocation arguments. Never {@literal null}. + * @param typeToRead the desired component target type. Can be {@literal null}. + */ + protected Object doExecute(ReactiveCouchbaseQueryMethod method, ResultProcessor processor, + ParametersParameterAccessor accessor, @Nullable Class typeToRead) { + + Query query = createQuery(accessor); + + query = applyAnnotatedConsistencyIfPresent(query); + // query = applyAnnotatedCollationIfPresent(query, accessor); + + ReactiveFindByQueryOperation.FindByQueryWithQuery find = typeToRead == null // + ? findOperationWithProjection // + : findOperationWithProjection; // TODO .as(typeToRead); + + String collection = "_default._default";// method.getEntityInformation().getCollectionName(); + + ReactiveCouchbaseQueryExecution execution = getExecution(accessor, + new ReactiveCouchbaseQueryExecution.ResultProcessingConverter(processor, getOperations(), instantiators), find); + return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + } + + /** + * Returns the execution instance to use. + * + * @param accessor must not be {@literal null}. + * @param resultProcessing must not be {@literal null}. + * @return + */ + private ReactiveCouchbaseQueryExecution getExecution(ParameterAccessor accessor, + Converter resultProcessing, ReactiveFindByQueryOperation.FindByQueryWithQuery operation) { + return new ReactiveCouchbaseQueryExecution.ResultProcessingExecution(getExecutionToWrap(accessor, operation), + resultProcessing); + } + + private ReactiveCouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, + ReactiveFindByQueryOperation.FindByQueryWithQuery operation) { + + if (isDeleteQuery()) { + return new DeleteExecution(getOperations(), method); + // } else if (isTailable(method)) { + // return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).tail(); + } else if (method.isCollectionQuery()) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); + } else if (isCountQuery()) { + return (q, t, c) -> operation.matching(q).count(); + } else if (isExistsQuery()) { + return (q, t, c) -> operation.matching(q).exists(); + } else { + return (q, t, c) -> { + + ReactiveFindByQueryOperation.TerminatingFindByQuery find = operation.matching(q); + + if (isCountQuery()) { + return find.count(); + } + + return isLimiting() ? find.first() : find.one(); + }; + } + } + + private boolean isTailable(ReactiveCouchbaseQueryMethod method) { + return false; // method.getTailableAnnotation() != null; + } + + /** + * Add a scan consistency from {@link org.springframework.data.couchbase.repository.ScanConsistency} to the given + * {@link Query} if present. + * + * @param query the {@link Query} to potentially apply the sort to. + * @return the query with potential scan consistency applied. + * @since 4.1 + */ + Query applyAnnotatedConsistencyIfPresent(Query query) { + + if (!method.hasScanConsistencyAnnotation()) { + return query; + } + return query.with(method.getScanConsistencyAnnotation().query()); + } + + /** + * Creates a {@link Query} instance using the given {@link ParametersParameterAccessor}. Will delegate to + * {@link #createQuery(ParametersParameterAccessor)} by default but allows customization of the count query to be + * triggered. + * + * @param accessor must not be {@literal null}. + * @return + */ + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return /*applyQueryMetaAttributesWhenPresent*/(createQuery(accessor)); + } + + /** + * Creates a {@link Query} instance using the given {@link ParameterAccessor} + * + * @param accessor must not be {@literal null}. + * @return + */ + protected abstract Query createQuery(ParametersParameterAccessor accessor); + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isCountQuery() + */ + protected boolean isCountQuery() { + return getQueryMethod().isCountQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isExistsQuery() + */ + + protected boolean isExistsQuery() { + return getQueryMethod().isExistsQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isDeleteQuery() + */ + protected boolean isDeleteQuery() { + return getQueryMethod().isDeleteQuery(); + } + + /** + * Return whether the query has an explicit limit set. + * + * @return + * @since 2.0.4 + */ + protected abstract boolean isLimiting(); + + public ReactiveCouchbaseOperations getOperations() { + return operations; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/BooleanUtil.java b/src/main/java/org/springframework/data/couchbase/repository/query/BooleanUtil.java new file mode 100644 index 000000000..b7113e07b --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/BooleanUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +/** + * Utility class containing methods to interact with boolean values. + * + * @author Mark Paluch + * @since 2.0.9 + */ +final class BooleanUtil { + + private BooleanUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Count the number of {@literal true} values. + * + * @param values + * @return the number of values that are {@literal true}. + */ + static int countBooleanTrueValues(boolean... values) { + + int count = 0; + + for (boolean value : values) { + + if (value) { + count++; + } + } + + return count; + } +} 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 new file mode 100644 index 000000000..79f1c93ed --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java @@ -0,0 +1,305 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.support.PageableExecutionUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a + * {@link org.springframework.data.repository.query.QueryMethod} a {@link Query} can be executed in various flavors. + * TODO: seems to be a lot of duplication with ReactiveCouchbaseQueryExecution + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Christoph Strobl + * @author Michael Reiche + */ +@FunctionalInterface +interface CouchbaseQueryExecution { + + Object execute(Query query, Class type, String collection); + + /** + * {@link CouchbaseQueryExecution} removing documents matching the query. + * + * @author Mark Paluch + * @author Artyom Gabeev + */ + + final class DeleteExecution implements CouchbaseQueryExecution { + + private final CouchbaseOperations operations; + private final CouchbaseQueryMethod method; + + public DeleteExecution(CouchbaseOperations operations, CouchbaseQueryMethod method) { + this.operations = operations; + this.method = method; + } + + /* + * (non-Javadoc) + * @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) { + + // Class type = method.getEntityInformation().getJavaType(); + + // if (method.isCollectionQuery()) { + return operations.removeByQuery(type).matching(query).all(); + // } + + } + + } + + /** + * An {@link ReactiveCouchbaseQueryExecution} that wraps the results of the given delegate with the given result + * processing. + */ + final class ResultProcessingExecution implements CouchbaseQueryExecution { + + private final CouchbaseQueryExecution delegate; + private final Converter converter; + + public ResultProcessingExecution(CouchbaseQueryExecution delegate, Converter converter) { + + Assert.notNull(delegate, "Delegate must not be null!"); + Assert.notNull(converter, "Converter must not be null!"); + + this.delegate = delegate; + this.converter = converter; + } + + @Override + public Object execute(Query query, Class type, String collection) { + return converter.convert(delegate.execute(query, type, collection)); + } + } + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + * + * @author Mark Paluch + */ + final class ResultProcessingConverter implements Converter { + + private final ResultProcessor processor; + private final CouchbaseOperations operations; + private final EntityInstantiators instantiators; + + public ResultProcessingConverter(ResultProcessor processor, CouchbaseOperations operations, + EntityInstantiators instantiators) { + + Assert.notNull(processor, "Processor must not be null!"); + Assert.notNull(operations, "Operations must not be null!"); + Assert.notNull(instantiators, "Instantiators must not be null!"); + + this.processor = processor; + this.operations = operations; + this.instantiators = instantiators; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + ReturnedType returnedType = processor.getReturnedType(); + + if (isVoid(returnedType)) { + + if (source instanceof Mono) { + return ((Mono) source).then(); + } + + if (source instanceof Publisher) { + return Flux.from((Publisher) source).then(); + } + } + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) { + return source; + } + + if (!operations.getConverter().getMappingContext().hasPersistentEntityFor(returnedType.getReturnedType())) { + return source; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + operations.getConverter().getMappingContext(), instantiators); + + return processor.processResult(source, converter); + } + } + + static boolean isVoid(ReturnedType returnedType) { + return returnedType.getReturnedType().equals(Void.class); + } + + /** + * {@link CouchbaseQueryExecution} for {@link Slice} query methods. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @since 1.5 + */ + final class SlicedExecution implements CouchbaseQueryExecution { + + private final ExecutableFindByQueryOperation.ExecutableFindByQuery find; + private final Pageable pageable; + + public SlicedExecution(ExecutableFindByQueryOperation.ExecutableFindByQuery find, Pageable pageable) { + + Assert.notNull(find, "Find must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + + this.find = find; + this.pageable = pageable; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution#execute(org.springframework.data.couchbase.core.query.Query) + */ + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object execute(Query query, Class type, String collection) { + + int pageSize = pageable.getPageSize(); + + // Apply Pageable but tweak limit to peek into next page + Query modifiedQuery = query.with(pageable).limit(pageSize + 1); + List result = find.matching(modifiedQuery).all(); + + boolean hasNext = result.size() > pageSize; + + return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); + } + } + + /** + * {@link CouchbaseQueryExecution} for pagination queries. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Christoph Strobl + */ + final class PagedExecution implements CouchbaseQueryExecution { + + private final ExecutableFindByQueryOperation.ExecutableFindByQuery operation; + private final Pageable pageable; + + public PagedExecution(ExecutableFindByQueryOperation.ExecutableFindByQuery operation, Pageable pageable) { + + Assert.notNull(operation, "Operation must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + + this.operation = operation; + this.pageable = pageable; + } + + /* + * (non-Javadoc) + * @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) { + + int overallLimit = query.getLimit(); + + ExecutableFindByQueryOperation.TerminatingFindByQuery matching = operation.matching(query); + + // Apply raw pagination + query.with(pageable); + + // Adjust limit if page would exceed the overall limit + if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) { + query.limit((int) (overallLimit - pageable.getOffset())); + } + + return PageableExecutionUtils.getPage(matching.all(), pageable, () -> { + + long count = operation.matching(query.skip(-1).limit(-1)).count(); + return overallLimit != 0 ? Math.min(count, overallLimit) : count; + }); + } + } + + /* + final class ReactivePagedExecution implements CouchbaseQueryExecution { // TODO ?? + + private final ReactiveFindByQueryOperation.ReactiveFindByQuery operation; + private final Pageable pageable; + + public ReactivePagedExecution(ReactiveFindByQueryOperation.ReactiveFindByQuery operation, Pageable pageable) { + + Assert.notNull(operation, "Operation must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + + this.operation = operation; + this.pageable = pageable; + } + + + /xx* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution#execute(org.springframework.data.couchbase.core.query.Query) + *xx/ + @Override + public Object execute(Query query) { + + int overallLimit = query.getLimit(); + + ReactiveFindByQueryOperation.TerminatingFindByQuery matching = operation.matching(query); + + // Apply raw pagination + query.with(pageable); + + // Adjust limit if page would exceed the overall limit + if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) { + query.limit((int) (overallLimit - pageable.getOffset())); + } + + return PageableExecutionUtils.getPage(matching.all().collectList().block(), pageable, () -> { + + long count = operation.matching(query.skip(-1).limit(-1)).count().block(); + return overallLimit != 0 ? Math.min(count, overallLimit) : count; + }); + } + } + */ +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java index 43223010f..9b92f90f5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java @@ -25,10 +25,13 @@ import org.springframework.data.couchbase.core.query.View; import org.springframework.data.couchbase.core.query.WithConsistency; import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.util.StringUtils; /** @@ -152,6 +155,14 @@ public WithConsistency getConsistencyAnnotation() { return method.getAnnotation(WithConsistency.class); } + public boolean hasScanConsistencyAnnotation() { + return getScanConsistencyAnnotation() != null; + } + + public ScanConsistency getScanConsistencyAnnotation() { + return method.getAnnotation(ScanConsistency.class); + } + /** * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found * nor the attribute was specified. @@ -163,6 +174,24 @@ public String getInlineN1qlQuery() { return StringUtils.hasText(query) ? query : null; } + /** + * is this a 'delete'? + * + * @return is this a 'delete'? + */ + public boolean isDeleteQuery() { + return getName().toLowerCase().startsWith("delete"); + } + + /** + * is this an 'exists' query? + * + * @return is this an 'exists' query? + */ + public boolean isExistsQuery() { + return getName().toLowerCase().startsWith("exists"); + } + /** * indicates if the method begins with "count" * @@ -177,4 +206,14 @@ public boolean isCountQuery() { public String toString() { return super.toString(); } + + public boolean hasReactiveWrapperParameter() { + for (Parameter p : getParameters()) { + if (ReactiveWrapperConverters.supports(p.getType())) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java index bef5d5161..c7fd87bd4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java @@ -18,28 +18,35 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ +@Deprecated public class CouchbaseRepositoryQuery implements RepositoryQuery { private final CouchbaseOperations operations; private final CouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, - final NamedQueries namedQueries) { + final NamedQueries namedQueries, final QueryMethodEvaluationContextProvider evaluationContextProvider) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; + throw new RuntimeException("Deprecated"); } @Override public Object execute(final Object[] parameters) { - return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters); + return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, evaluationContextProvider) + .execute(parameters); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/couchbase/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 000000000..62b3d4478 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Michael Reiche + */ +class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Creates a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param entityInstantiators must not be {@literal null}. + */ + public DtoInstantiatingConverter(Class dtoType, + MappingContext, CouchbasePersistentProperty> context, + EntityInstantiators entityInstantiators) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = entityInstantiators.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + final PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); + final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + final PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); + final PreferredConstructor> constructor = targetEntity + .getPersistenceConstructor(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName().toString())); + } + }); + + final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties(new SimplePropertyHandler() { + + @Override + public void doWithPersistentProperty(PersistentProperty property) { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + } + }); + + return dto; + } +} 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 d9d3eed39..b4fe83457 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 @@ -18,32 +18,43 @@ import java.util.ArrayList; import java.util.List; +import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ +@Deprecated public class N1qlRepositoryQueryExecutor { private final CouchbaseOperations operations; private final CouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, - final NamedQueries namedQueries) { + final NamedQueries namedQueries, final QueryMethodEvaluationContextProvider evaluationContextProvider) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; } + private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + /** * see also {@link ReactiveN1qlRepositoryQueryExecutor#execute(Object[] parameters) execute } * @@ -54,26 +65,39 @@ public Object execute(final Object[] parameters) { final Class domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType(); final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - // this is identical to ReactiveN1qlRespositoryQueryExecutor, - // except for the type of 'q', and the call to oneValue() vs one() + // counterpart to ReactiveN1qlRespositoryQueryExecutor, Query query; ExecutableFindByQueryOperation.ExecutableFindByQuery q; if (queryMethod.hasN1qlAnnotation()) { query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), operations.getBucketName(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries).createQuery(); + SPEL_PARSER, evaluationContextProvider, namedQueries).createQuery(); } else { final PartTree tree = new PartTree(queryMethod.getName(), domainClass); query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); } - q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations.findByQuery(domainClass).matching(query); + ExecutableFindByQueryOperation.ExecutableFindByQuery operation = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations + .findByQuery(domainClass).consistentWith(buildQueryScanConsistency()); if (queryMethod.isCountQuery()) { - return q.count(); + return operation.matching(query).count(); } else if (queryMethod.isCollectionQuery()) { - return q.all(); + return operation.matching(query).all(); + } else if (queryMethod.isPageQuery()) { + Pageable p = accessor.getPageable(); + return new CouchbaseQueryExecution.PagedExecution(operation, p).execute(query, null, null); } else { - return q.oneValue(); + return operation.matching(query).oneValue(); } } + private QueryScanConsistency buildQueryScanConsistency() { + QueryScanConsistency scanConsistency = QueryScanConsistency.NOT_BOUNDED; + if (queryMethod.hasConsistencyAnnotation()) { + scanConsistency = queryMethod.getConsistencyAnnotation().value(); + } else if (queryMethod.hasScanConsistencyAnnotation()) { + scanConsistency = queryMethod.getScanConsistencyAnnotation().query(); + } + return scanConsistency; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java new file mode 100644 index 000000000..c0d5d0485 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.*; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.StringUtils; + +/** + * {@link RepositoryQuery} implementation for Couchbase. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @author Thomas Darimont + * @author Mark Paluch + */ +public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery { + + private final PartTree tree; + // private final boolean isGeoNearQuery; + private final MappingContext context; + private final ResultProcessor processor; + private final CouchbaseConverter converter; + + /** + * Creates a new {@link PartTreeCouchbaseQuery} from the given {@link QueryMethod} and {@link CouchbaseTemplate}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public PartTreeCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + super(method, operations, expressionParser, evaluationContextProvider); + + this.processor = method.getResultProcessor(); + // TODO - why the for loop? Why not just : + // this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()) + for (PartTree.OrPart parts : this.tree = new PartTree(method.getName(), + processor.getReturnedType().getDomainType())) { + } + + this.context = operations.getConverter().getMappingContext(); + this.converter = operations.getConverter(); + } + + /** + * Return the {@link PartTree} backing the query. + * + * @return the tree + */ + public PartTree getTree() { + return tree; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor, boolean) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter); + Query query = creator.createQuery(); + + if (tree.isLimiting()) { + query.limit(tree.getMaxResults()); + } + return query; + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#createCountQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter).createQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isDeleteQuery() + */ + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return tree.isLimiting(); + } +} 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 new file mode 100644 index 000000000..4106a0ffa --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java @@ -0,0 +1,170 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a + * {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractReactiveCouchbaseQuery} can be + * executed in various flavors. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.0 + */ +interface ReactiveCouchbaseQueryExecution { + + Object execute(Query query, Class type, String collection); + + /** + * {@link ReactiveCouchbaseQueryExecution} removing documents matching the query. + * + * @author Mark Paluch + * @author Artyom Gabeev + */ + + final class DeleteExecution implements ReactiveCouchbaseQueryExecution { + + private final ReactiveCouchbaseOperations operations; + private final CouchbaseQueryMethod method; + + public DeleteExecution(ReactiveCouchbaseOperations operations, CouchbaseQueryMethod method) { + this.operations = operations; + this.method = method; + } + + /* + * (non-Javadoc) + * @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) { + + // if (method.isCollectionQuery()) { + return operations.removeByQuery(type).matching(query).all(); + // } + + /* + if (method.isQueryForEntity() && !ClassUtils.isPrimitiveOrWrapper(method.getReturnedObjectType())) { + return operations.removeByQuery(query, type); + } + + return operations.removeByQuery(query, type, collection) + .map(deleteResult -> deleteResult.wasAcknowledged() ? deleteResult.getDeletedCount() : 0L); + */ + } + + } + + /** + * An {@link ReactiveCouchbaseQueryExecution} that wraps the results of the given delegate with the given result + * processing. + */ + final class ResultProcessingExecution implements ReactiveCouchbaseQueryExecution { + + private final ReactiveCouchbaseQueryExecution delegate; + private final Converter converter; + + public ResultProcessingExecution(ReactiveCouchbaseQueryExecution delegate, Converter converter) { + + Assert.notNull(delegate, "Delegate must not be null!"); + Assert.notNull(converter, "Converter must not be null!"); + + this.delegate = delegate; + this.converter = converter; + } + + @Override + public Object execute(Query query, Class type, String collection) { + return converter.convert(delegate.execute(query, type, collection)); + } + } + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + * + * @author Mark Paluch + */ + final class ResultProcessingConverter implements Converter { + + private final ResultProcessor processor; + private final ReactiveCouchbaseOperations operations; + private final EntityInstantiators instantiators; + + public ResultProcessingConverter(ResultProcessor processor, ReactiveCouchbaseOperations operations, + EntityInstantiators instantiators) { + + Assert.notNull(processor, "Processor must not be null!"); + Assert.notNull(operations, "Operations must not be null!"); + Assert.notNull(instantiators, "Instantiators must not be null!"); + + this.processor = processor; + this.operations = operations; + this.instantiators = instantiators; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + ReturnedType returnedType = processor.getReturnedType(); + + if (isVoid(returnedType)) { + + if (source instanceof Mono) { + return ((Mono) source).then(); + } + + if (source instanceof Publisher) { + return Flux.from((Publisher) source).then(); + } + } + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) { + return source; + } + + if (!operations.getConverter().getMappingContext().hasPersistentEntityFor(returnedType.getReturnedType())) { + return source; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + operations.getConverter().getMappingContext(), instantiators); + + return processor.processResult(source, converter); + } + } + + static boolean isVoid(ReturnedType returnedType) { + return returnedType.getReturnedType().equals(Void.class); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java new file mode 100644 index 000000000..0da9048b0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.lang.reflect.Method; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ClassUtils; + +/** + * Reactive specific implementation of {@link CouchbaseQueryMethod}. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.0 + */ +public class ReactiveCouchbaseQueryMethod extends CouchbaseQueryMethod { + + private static final ClassTypeInformation PAGE_TYPE = ClassTypeInformation.from(Page.class); + private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); + + private final Method method; + private final Lazy isCollectionQuery; + + /** + * Creates a new {@link ReactiveCouchbaseQueryMethod} from the given {@link Method}. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param projectionFactory must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public ReactiveCouchbaseQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, + MappingContext, CouchbasePersistentProperty> mappingContext) { + + super(method, metadata, projectionFactory, mappingContext); + + if (hasParameterOfType(method, Pageable.class)) { + + TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); + + boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); + boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType()) + && (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()) + || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())); + + if (singleWrapperWithWrappedPageableResult) { + throw new InvalidDataAccessApiUsageException( + String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).", + ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + } + + if (!multiWrapper) { + throw new IllegalStateException(String.format( + "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s", + method.toString())); + } + + if (hasParameterOfType(method, Sort.class)) { + throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " + + "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString())); + } + } + + this.method = method; + this.isCollectionQuery = Lazy.of(() -> !(isPageQuery() || isSliceQuery()) + && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod#createParameters(java.lang.reflect.Method) + */ + /* + @Override + protected CouchbaseParameters createParameters(Method method) { + return new CouchbaseParameters(method, isGeoNearQuery(method)); + }*/ + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isModifyingQuery() + */ + @Override + public boolean isModifyingQuery() { + return super.isModifyingQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isQueryForEntity() + */ + @Override + public boolean isQueryForEntity() { + return super.isQueryForEntity(); + } + + /* + * All reactive query methods are streaming queries. + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isStreamQuery() + */ + @Override + public boolean isStreamQuery() { + return true; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java index 128a89e62..d2b6b40c5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java @@ -16,34 +16,56 @@ package org.springframework.data.couchbase.repository.query; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ -public class ReactiveCouchbaseRepositoryQuery implements RepositoryQuery { +@Deprecated +public class ReactiveCouchbaseRepositoryQuery extends AbstractReactiveCouchbaseQuery { private final ReactiveCouchbaseOperations operations; - private final CouchbaseQueryMethod queryMethod; + private final ReactiveCouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations, - final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { + final ReactiveCouchbaseQueryMethod queryMethod, final NamedQueries namedQueries, + final QueryMethodEvaluationContextProvider evaluationContextProvider) { + super(queryMethod, operations, new SpelExpressionParser(), evaluationContextProvider); this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; + throw new RuntimeException("Deprecated"); } @Override public Object execute(final Object[] parameters) { - return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters); + return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, evaluationContextProvider) + .execute(parameters); } @Override - public QueryMethod getQueryMethod() { + protected Query createQuery(ParametersParameterAccessor accessor) { + return null; + } + + @Override + protected boolean isLimiting() { + // TODO + return false; + } + + @Override + public ReactiveCouchbaseQueryMethod getQueryMethod() { return queryMethod; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java index 2a5409806..c15fa09df 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java @@ -15,8 +15,10 @@ */ package org.springframework.data.couchbase.repository.query; +import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation; +import org.springframework.data.domain.Pageable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.*; @@ -32,18 +34,23 @@ /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ +@Deprecated public class ReactiveN1qlRepositoryQueryExecutor { private final ReactiveCouchbaseOperations operations; - private final CouchbaseQueryMethod queryMethod; + private final ReactiveCouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations operations, - final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { + final ReactiveCouchbaseQueryMethod queryMethod, final NamedQueries namedQueries, + QueryMethodEvaluationContextProvider evaluationContextProvider) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; } /** @@ -53,30 +60,17 @@ public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations ope * @return */ public Object execute(final Object[] parameters) { - final Class domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType(); - final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - final String namedQueryName = queryMethod.getNamedQueryName(); - // this is identical to ExecutableN1qlRespositoryQueryExecutor, - // except for the type of 'q', and the call to one() vs oneValue() + // counterpart to N1qlRespositoryQueryExecutor, - Query query; - ReactiveFindByQueryOperation.ReactiveFindByQuery q; if (queryMethod.hasN1qlAnnotation()) { - query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), operations.getBucketName(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries).createQuery(); + return new ReactiveStringBasedCouchbaseQuery(queryMethod, operations, + new SpelExpressionParser(), evaluationContextProvider, namedQueries).execute(parameters); } else { - final PartTree tree = new PartTree(queryMethod.getName(), domainClass); - query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); - } - q = (ReactiveFindByQueryOperation.ReactiveFindByQuery) operations.findByQuery(domainClass).matching(query); - if (queryMethod.isCountQuery()) { - return q.count(); - } else if (queryMethod.isCollectionQuery()) { - return q.all(); - } else { - return q.one(); + return new ReactivePartTreeCouchbaseQuery(queryMethod, operations, new SpelExpressionParser(), + evaluationContextProvider).execute(parameters); } + } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java new file mode 100644 index 000000000..598d814d8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java @@ -0,0 +1,107 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.query.*; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Reactive PartTree {@link RepositoryQuery} implementation for Couchbase. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.0 + */ +public class ReactivePartTreeCouchbaseQuery extends AbstractReactiveCouchbaseQuery { + + private final PartTree tree; + private final CouchbaseConverter converter; + private static final Logger LOG = LoggerFactory.getLogger(ReactivePartTreeCouchbaseQuery.class); + + /** + * Creates a new {@link ReactivePartTreeCouchbaseQuery} from the given {@link QueryMethod} and + * {@link CouchbaseTemplate}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public ReactivePartTreeCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + super(method, operations, expressionParser, evaluationContextProvider); + this.tree = new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()); + this.converter = operations.getConverter(); + } + + /** + * Return the {@link PartTree} backing the query. + * + * @return the tree + */ + public PartTree getTree() { + return tree; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbease.repository.query.AbstractCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor, boolean) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter); + Query query = creator.createQuery(); + + if (tree.isLimiting()) { + query.limit(tree.getMaxResults()); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Created query {} for * fields.", query.export()); + } + return query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#createCountQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createCountQuery(ParametersParameterAccessor accessor) { + Query query = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter).createQuery(); + if (LOG.isDebugEnabled()) { + LOG.debug("Created query {} for * fields.", query.export()); + } + return query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return tree.isLimiting(); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java index 3cfd991e8..b82e60908 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java @@ -32,7 +32,9 @@ * * @author Subhashni Balakrishnan * @since 3.0 + * @deprecated */ +@Deprecated public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery { private final PartTree partTree; 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 new file mode 100644 index 000000000..f39847c9e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java @@ -0,0 +1,118 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * Query to use a plain JSON String to create the {@link Query} to actually execute. + * + * @author Mark Paluch + * @author Christoph Strobl + * @author Michael Reiche + * @since 2.0 + */ +public class ReactiveStringBasedCouchbaseQuery extends AbstractReactiveCouchbaseQuery { + + private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveStringBasedCouchbaseQuery.class); + private final boolean isCountQuery; + private final boolean isExistsQuery; + private final boolean isDeleteQuery; + private final NamedQueries namedQueries; + + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /** + * Creates a new {@link ReactiveStringBasedCouchbaseQuery} for the given {@link String}, {@link CouchbaseQueryMethod}, + * {@link CouchbaseOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + * @param namedQueries + */ + public ReactiveStringBasedCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider, + NamedQueries namedQueries) { + + super(method, operations, expressionParser, evaluationContextProvider); + + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + this.isCountQuery = method.isCountQuery(); + this.isExistsQuery = method.isExistsQuery(); + this.isDeleteQuery = method.isDeleteQuery(); + + if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { + throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); + } + + this.namedQueries = namedQueries; + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + // StringN1qlQueryCreator creator = new StringN1qlQueryCreator( accessor, getQueryMethod()); + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), + getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider, + namedQueries); + Query query = creator.createQuery(); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Created query %s for * fields.", query.export() /*, query.getFieldsObject()*/)); + } + + return query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + // TODO + return false; + } + + private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, + boolean isDeleteQuery) { + return BooleanUtil.countBooleanTrueValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java index 5a1058a3d..b2840674e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java @@ -39,7 +39,9 @@ * * @author Subhashni Balakrishnan * @since 3.0 + * @deprecated */ +@Deprecated public class ReactiveStringN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery { private final StringBasedN1qlQueryParser queryParser; 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 new file mode 100644 index 000000000..3c969e88c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java @@ -0,0 +1,139 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * Query to use a plain JSON String to create the {@link Query} to actually execute. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @author Thomas Darimont + * @author Mark Paluch + * @author Michael Reiche + */ +public class StringBasedCouchbaseQuery extends AbstractCouchbaseQuery { + + private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; + private static final Logger LOG = LoggerFactory.getLogger(StringBasedCouchbaseQuery.class); + + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + private final boolean isCountQuery; + private final boolean isExistsQuery; + private final boolean isDeleteQuery; + private final NamedQueries namedQueries; + + /** + * Creates a new {@link StringBasedCouchbaseQuery} for the given {@link String}, {@link CouchbaseQueryMethod}, + * {@link CouchbaseOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. + * + * @param method must not be {@literal null}. + * @param couchbaseOperations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null} + * @param namedQueries + */ + public StringBasedCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations couchbaseOperations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider, + NamedQueries namedQueries) { + + super(method, couchbaseOperations, expressionParser, evaluationContextProvider); + + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + this.isCountQuery = method.isCountQuery(); + this.isExistsQuery = method.isExistsQuery(); + this.isDeleteQuery = method.isDeleteQuery(); + + if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { + throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); + } + this.namedQueries = namedQueries; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), + getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider, + namedQueries); + Query query = creator.createQuery(); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Created query %s for * fields.", query.export() /*, query.getFieldsObject()*/)); + } + + return query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return isCountQuery; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return isExistsQuery; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isDeleteQuery() + */ + @Override + protected boolean isDeleteQuery() { + return this.isDeleteQuery; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return false; + } + + private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, + boolean isDeleteQuery) { + return BooleanUtil.countBooleanTrueValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1; + } +} 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 05213c457..6fdb3ac4e 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 @@ -56,10 +56,9 @@ public class StringN1qlQueryCreator extends AbstractQueryCreator iterator) { - PersistentPropertyPath path = context.getPersistentPropertyPath( - part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); return from(part, property, where(path.toDotPath()), iterator); } @@ -113,8 +111,7 @@ protected QueryCriteria and(final Part part, final QueryCriteria base, final Ite return create(part, iterator); } - PersistentPropertyPath path = context.getPersistentPropertyPath( - part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); return from(part, property, base.and(path.toDotPath()), iterator); @@ -138,23 +135,23 @@ protected Query complete(QueryCriteria criteria, Sort sort) { return q; } - private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, - final QueryCriteria criteria, final Iterator parameters) { + private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria, + final Iterator parameters) { final Part.Type type = part.getType(); switch (type) { - case SIMPLE_PROPERTY: - return criteria; //.eq(parameters.next()); // this will be the dummy from PartTree - default: - throw new IllegalArgumentException("Unsupported keyword!"); + case SIMPLE_PROPERTY: + return criteria; // .eq(parameters.next()); // this will be the dummy from PartTree + default: + throw new IllegalArgumentException("Unsupported keyword!"); } } // copied from StringN1qlBasedQuery private N1QLExpression getExpression(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) { - EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext( - getQueryMethod().getParameters(), runtimeParameters); + EvaluationContext evaluationContext = evaluationContextProvider + .getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters); N1QLExpression parsedStatement = x(this.queryParser.doParse(parser, evaluationContext, false)); Sort sort = accessor.getSort(); @@ -165,13 +162,11 @@ private N1QLExpression getExpression(ParameterAccessor accessor, Object[] runtim if (queryMethod.isPageQuery()) { Pageable pageable = accessor.getPageable(); Assert.notNull(pageable, "Pageable must not be null!"); - parsedStatement = parsedStatement.limit(pageable.getPageSize()).offset( - Math.toIntExact(pageable.getOffset())); + parsedStatement = parsedStatement.limit(pageable.getPageSize()).offset(Math.toIntExact(pageable.getOffset())); } else if (queryMethod.isSliceQuery()) { Pageable pageable = accessor.getPageable(); Assert.notNull(pageable, "Pageable must not be null!"); - parsedStatement = parsedStatement.limit(pageable.getPageSize() + 1).offset( - Math.toIntExact(pageable.getOffset())); + parsedStatement = parsedStatement.limit(pageable.getPageSize() + 1).offset(Math.toIntExact(pageable.getOffset())); } return parsedStatement; } @@ -185,5 +180,3 @@ private static Object[] getParameters(ParameterAccessor accessor) { return params.toArray(); } } - - diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index 747c9d76e..a02f3ec2c 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -23,10 +23,9 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; -import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; -import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; -import org.springframework.data.couchbase.repository.query.CouchbaseRepositoryQuery; +import org.springframework.data.couchbase.repository.query.*; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -89,14 +88,14 @@ public void setBeanClassLoader(ClassLoader classLoader) { * Returns entity information based on the domain class. * * @param domainClass the class for the entity. - * @param the value type - * @param the id type. + * @param the value type + * @param the id type. * @return entity information for that domain class. */ @Override public CouchbaseEntityInformation getEntityInformation(Class domainClass) { - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( - domainClass); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext + .getRequiredPersistentEntity(domainClass); return new MappingCouchbaseEntityInformation<>(entity); } @@ -152,29 +151,18 @@ public CouchbaseQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluat @Override public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata metadata, final ProjectionFactory factory, final NamedQueries namedQueries) { - final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( - metadata.getRepositoryInterface(), metadata.getDomainType()); + final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping + .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); - return new CouchbaseRepositoryQuery(couchbaseOperations, queryMethod, namedQueries); - /*CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); - String namedQueryName = queryMethod.getNamedQueryName(); - if (queryMethod.hasN1qlAnnotation()) { - - if (queryMethod.hasInlineN1qlQuery()) { - return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations, - SPEL_PARSER, evaluationContextProvider); - } else if (namedQueries.hasQuery(namedQueryName)) { - String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations, SPEL_PARSER, - evaluationContextProvider); - } - + return new StringBasedCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider, namedQueries); + } else { + return new PartTreeCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider); } - - return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);*/ } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java index 5cc49c1ff..d15094f9b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java @@ -23,9 +23,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; -import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; -import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; -import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseRepositoryQuery; +import org.springframework.data.couchbase.repository.query.*; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -33,6 +31,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -84,14 +83,14 @@ public void setBeanClassLoader(ClassLoader classLoader) { * Returns entity information based on the domain class. * * @param domainClass the class for the entity. - * @param the value type - * @param the id type. + * @param the value type + * @param the id type. * @return entity information for that domain class. */ @Override public CouchbaseEntityInformation getEntityInformation(Class domainClass) { - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( - domainClass); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext + .getRequiredPersistentEntity(domainClass); return new MappingCouchbaseEntityInformation<>(entity); } @@ -106,8 +105,8 @@ public CouchbaseEntityInformation getEntityInformation(Class d */ @Override protected final Object getTargetRepository(final RepositoryInformation metadata) { - ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( - metadata.getRepositoryInterface(), metadata.getDomainType()); + ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping + .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); SimpleReactiveCouchbaseRepository repository = getTargetRepositoryViaReflection(metadata, entityInformation, couchbaseOperations); @@ -149,10 +148,18 @@ public CouchbaseQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluat @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { - final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( - metadata.getRepositoryInterface(), metadata.getDomainType()); - return new ReactiveCouchbaseRepositoryQuery(couchbaseOperations, - new CouchbaseQueryMethod(method, metadata, factory, mappingContext), namedQueries); + final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping + .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); + ReactiveCouchbaseQueryMethod queryMethod = new ReactiveCouchbaseQueryMethod(method, metadata, factory, + mappingContext); + + if (queryMethod.hasN1qlAnnotation()) { + return new ReactiveStringBasedCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider, namedQueries); + } else { + return new ReactivePartTreeCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider); + } } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index e7a9608c2..4cba5e03d 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -46,4 +46,16 @@ public String getIata() { public String getIcao() { return icao; } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ id: "); + sb.append(getId()); + sb.append(", iata: "); + sb.append(iata); + sb.append(", icao: "); + sb.append(icao); + sb.append(" }"); + return sb.toString(); + } } 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 06ce1fec1..335d358c0 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -21,6 +21,8 @@ import org.jetbrains.annotations.NotNull; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.stereotype.Repository; @@ -46,9 +48,13 @@ public interface AirportRepository extends PagingAndSortingRepository findAllByIata(String iata); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Airport findByIata(String iata); + @Query("#{#n1ql.selectEntity} where iata = $1") List getAllByIata(String iata); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) long countByIataIn(String... iata); @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) @@ -61,4 +67,6 @@ public interface AirportRepository extends PagingAndSortingRepository findAllByIataNot(String iata, Pageable pageable); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 54f56573e..689b432b5 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,6 +16,10 @@ package org.springframework.data.couchbase.domain; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import org.springframework.data.couchbase.repository.ScanConsistency; @@ -25,6 +29,8 @@ import com.couchbase.client.java.query.QueryScanConsistency; +import java.util.ArrayList; + /** * template class for Reactive Couchbase operations * @@ -57,4 +63,21 @@ public interface ReactiveAirportRepository extends ReactiveSortingRepository findById(String var1); + + // use parameter type PageRequest instead of Pageable. Pageable requires a return type of Page<> + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Flux findAllByIataLike(String iata, final PageRequest page); + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Mono findByIata(String iata); + + // This is not efficient. See findAllByIataLike for efficient reactive paging + default public Mono> findAllAirportsPaged(Pageable pageable) { + return count().flatMap(airportCount -> { + return findAll(pageable.getSort()) + .buffer(pageable.getPageSize(), (pageable.getPageNumber() * pageable.getPageSize())) + .elementAt(pageable.getPageNumber(), new ArrayList<>()) + .map(airports -> new PageImpl(airports, pageable, airportCount)); + }); + } } 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 c4ea6cd88..2ad5db354 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -41,6 +41,9 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; @@ -111,14 +114,19 @@ void findBySimpleProperty() { Airport vie = null; try { vie = new Airport("airports::vie", "vie", "loww"); - airportRepository.save(vie); - sleep(1000); + vie = airportRepository.save(vie); List airports = airportRepository.findAllByIata("vie"); - assertEquals(vie.getId(), airports.get(0).getId()); + assertEquals(1, airports.size()); + System.out.println("findAllByIata(0): " + airports.get(0)); + Airport airport1 = airportRepository.findById(airports.get(0).getId()).get(); + assertEquals(airport1.getIata(), vie.getIata()); + System.out.println("findById: " + airport1); + Airport airport2 = airportRepository.findByIata(airports.get(0).getIata()); + assertEquals(airport1.getId(), vie.getId()); + System.out.println("findByIata: " + airport2); } finally { airportRepository.delete(vie); } - } @Test @@ -135,6 +143,11 @@ void count() { airportRepository.save(airport); } + Pageable pageable = PageRequest.of(0, 2); + Page aPage = airportRepository.findAllByIataNot("JFK", pageable); + assertEquals(iatas.length - 1, aPage.getTotalElements()); + assertEquals(pageable.getPageSize(), aPage.getContent().size()); + long airportCount = airportRepository.count(); assertEquals(7, airportCount); diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index dd04aeb97..2419f55bc 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.time.Duration; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -39,9 +40,14 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * template class for Reactive Couchbase operations @@ -87,10 +93,16 @@ void findBySimpleProperty() { Airport vie = null; try { vie = new Airport("airports::vie", "vie", "loww"); - airportRepository.save(vie).block(); + vie = airportRepository.save(vie).block(); List airports = airportRepository.findAllByIata("vie").collectList().block(); - // TODO - System.err.println(airports); + assertEquals(1, airports.size()); + System.out.println("findAllByIata(0): " + airports.get(0)); + Airport airport1 = airportRepository.findById(airports.get(0).getId()).block(); + assertEquals(airport1.getIata(), vie.getIata()); + System.out.println("findById: " + airport1); + Airport airport2 = airportRepository.findByIata(airports.get(0).getIata()).block(); + assertEquals(airport1.getId(), vie.getId()); + System.out.println("findByIata: " + airport2); } finally { airportRepository.delete(vie).block(); } @@ -108,6 +120,20 @@ void count() { airportRepository.save(airport).block(); } + int page = 0; + for (; page < 10;) { + PageRequest pageable = PageRequest.of(page++, 2); + Flux airportFlux = airportRepository.findAllByIataLike("S%", pageable); + List airportList = airportFlux.collectList().block(); + if (airportList.isEmpty()) { + break; + } + for (Airport a : airportList) { + System.out.println(a.getIata() + " " + a.getIcao()); + } + System.out.println("-----"); + } + Long airportCount = airportCount = airportRepository.count().block(); assertEquals(iatas.length, airportCount); 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 7b42450d2..2a2674406 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 @@ -44,6 +44,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.query.*; +import org.springframework.expression.spel.standard.SpelExpressionParser; import java.lang.reflect.Method; import java.util.Optional; @@ -83,7 +84,7 @@ void createsQueryCorrectly() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser(),QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); Query query = creator.createQuery(); assertEquals( @@ -101,7 +102,7 @@ void createsQueryCorrectly2() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser() ,QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); Query query = creator.createQuery(); assertEquals( @@ -120,7 +121,7 @@ void wrongNumberArgs() throws Exception { try { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser() , QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); } catch (IllegalArgumentException e) { return; } @@ -137,7 +138,7 @@ void doesNotHaveAnnotation() throws Exception { try { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); } catch (IllegalArgumentException e) { return; } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index 640ff3079..f3693bbfc 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -50,6 +50,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.query.*; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** @@ -88,7 +89,7 @@ void findUsingStringNq1l() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"), - queryMethod, converter, config().bucketname(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, config().bucketname(), new SpelExpressionParser(),QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); Query query = creator.createQuery(); System.out.println(query.toN1qlString(couchbaseTemplate.reactive(), Airline.class, false));