Add project(fields) to findByQuery similar to same method on findById. (#1212)

Closes #1208, #1213.
This commit is contained in:
Michael Reiche
2021-09-12 17:17:47 -07:00
committed by GitHub
parent b3c2f005ed
commit 4bd82ea23c
17 changed files with 369 additions and 111 deletions

View File

@@ -221,7 +221,7 @@ public interface ExecutableFindByQueryOperation {
}
/**
* Fluent method to specify scan consistency. Scan consistency may also come from an annotation.
* Fluent method to specify scan consistency. Scan consistency may also come from an annotation.
*
* @param <T> the entity type to use for the results.
*/
@@ -254,12 +254,30 @@ public interface ExecutableFindByQueryOperation {
<R> FindByQueryWithConsistency<R> as(Class<R> returnType);
}
/**
* Fluent method to specify fields to project.
*
* @param <T> the entity type to use for the results.
*/
interface FindByQueryWithProjecting<T> extends FindByQueryWithProjection<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param fields to project
* @return new instance of {@link ReactiveFindByQueryOperation.FindByQueryWithConsistency}.
* @throws IllegalArgumentException if returnType is {@literal null}.
*/
FindByQueryWithProjection<T> project(String[] fields);
}
/**
* Fluent method to specify DISTINCT fields
*
* @param <T> the entity type to use for the results.
*/
interface FindByQueryWithDistinct<T> extends FindByQueryWithProjection<T>, WithDistinct<T> {
interface FindByQueryWithDistinct<T> extends FindByQueryWithProjecting<T>, WithDistinct<T> {
/**
* Finds the distinct values for a specified {@literal field} across a single collection

View File

@@ -19,7 +19,6 @@ import java.util.List;
import java.util.stream.Stream;
import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport;
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.util.Assert;
@@ -45,7 +44,7 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu
@Override
public <T> ExecutableFindByQuery<T> findByQuery(final Class<T> domainType) {
return new ExecutableFindByQuerySupport<T>(template, domainType, domainType, ALL_QUERY, null, null, null, null,
null);
null, null);
}
static class ExecutableFindByQuerySupport<T> implements ExecutableFindByQuery<T> {
@@ -60,22 +59,24 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu
private final String collection;
private final QueryOptions options;
private final String[] distinctFields;
private final String[] fields;
ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class<?> domainType, final Class<T> returnType,
final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection,
final QueryOptions options, final String[] distinctFields) {
final QueryOptions options, final String[] distinctFields, final String[] fields) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.query = query;
this.reactiveSupport = new ReactiveFindByQuerySupport<T>(template.reactive(), domainType, returnType, query,
scanConsistency, scope, collection, options, distinctFields,
scanConsistency, scope, collection, options, distinctFields, fields,
new NonReactiveSupportWrapper(template.support()));
this.scanConsistency = scanConsistency;
this.scope = scope;
this.collection = collection;
this.options = options;
this.distinctFields = distinctFields;
this.fields = fields;
}
@Override
@@ -102,38 +103,47 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu
scanCons = scanConsistency;
}
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection,
options, distinctFields);
options, distinctFields, fields);
}
@Override
@Deprecated
public FindByQueryInScope<T> consistentWith(final QueryScanConsistency scanConsistency) {
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields);
collection, options, distinctFields, fields);
}
@Override
public FindByQueryConsistentWith<T> withConsistency(final QueryScanConsistency scanConsistency) {
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields);
collection, options, distinctFields, fields);
}
@Override
public <R> FindByQueryWithConsistency<R> as(final Class<R> returnType) {
Assert.notNull(returnType, "returnType must not be null!");
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields);
collection, options, distinctFields, fields);
}
@Override
public FindByQueryWithProjection<T> project(String[] fields) {
Assert.notNull(fields, "Fields must not be null");
Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified");
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, fields);
}
@Override
public FindByQueryWithProjection<T> distinct(final String[] distinctFields) {
Assert.notNull(distinctFields, "distinctFields must not be null!");
Assert.notNull(distinctFields, "distinctFields must not be null");
Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified");
// Coming from an annotation, this cannot be null.
// But a non-null but empty distinctFields means distinct on all fields
// So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null.
String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields;
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, dFields);
collection, options, dFields, fields);
}
@Override
@@ -144,8 +154,8 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu
@Override
public long count() {
Long l = reactiveSupport.count().block();
if ( l == null ){
throw new CouchbaseQueryExecutionException("count query did not return a count : "+query.export());
if (l == null) {
throw new CouchbaseQueryExecutionException("count query did not return a count : " + query.export());
}
return l;
}
@@ -159,19 +169,19 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu
public TerminatingFindByQuery<T> withOptions(final QueryOptions options) {
Assert.notNull(options, "Options must not be null.");
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields);
collection, options, distinctFields, fields);
}
@Override
public FindByQueryInCollection<T> inScope(final String scope) {
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields);
collection, options, distinctFields, fields);
}
@Override
public FindByQueryWithConsistency<T> inCollection(final String collection) {
return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields);
collection, options, distinctFields, fields);
}
}

View File

@@ -201,12 +201,30 @@ public interface ReactiveFindByQueryOperation {
<R> FindByQueryWithConsistency<R> as(Class<R> returnType);
}
/**
* Fluent method to specify fields to project.
*
* @param <T> the entity type to use for the results.
*/
interface FindByQueryWithProjecting<T> extends FindByQueryWithProjection<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param fields to project
* @return new instance of {@link FindByQueryWithConsistency}.
* @throws IllegalArgumentException if returnType is {@literal null}.
*/
FindByQueryWithProjection<T> project(String[] fields);
}
/**
* Fluent method to specify DISTINCT fields
*
* @param <T> the entity type to use for the results.
*/
interface FindByQueryWithDistinct<T> extends FindByQueryWithProjection<T>, WithDistinct<T> {
interface FindByQueryWithDistinct<T> extends FindByQueryWithProjecting<T>, WithDistinct<T> {
/**
* Finds the distinct values for a specified {@literal field} across a single {@link } or view.

View File

@@ -50,7 +50,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
@Override
public <T> ReactiveFindByQuery<T> findByQuery(final Class<T> domainType) {
return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, null, null, null, null,
template.support());
null, template.support());
}
static class ReactiveFindByQuerySupport<T> implements ReactiveFindByQuery<T> {
@@ -63,12 +63,13 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
private final String collection;
private final String scope;
private final String[] distinctFields;
private final String[] fields;
private final QueryOptions options;
private final ReactiveTemplateSupport support;
ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class<?> domainType,
final Class<T> returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope,
final String collection, final QueryOptions options, final String[] distinctFields,
final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields,
final ReactiveTemplateSupport support) {
Assert.notNull(domainType, "domainType must not be null!");
Assert.notNull(returnType, "returnType must not be null!");
@@ -81,6 +82,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
this.collection = collection;
this.options = options;
this.distinctFields = distinctFields;
this.fields = fields;
this.support = support;
}
@@ -94,57 +96,65 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
scanCons = scanConsistency;
}
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection,
options, distinctFields, support);
options, distinctFields, fields, support);
}
@Override
public TerminatingFindByQuery<T> withOptions(final QueryOptions options) {
Assert.notNull(options, "Options must not be null.");
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, support);
collection, options, distinctFields, fields, support);
}
@Override
public FindByQueryInCollection<T> inScope(final String scope) {
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, support);
collection, options, distinctFields, fields, support);
}
@Override
public FindByQueryWithConsistency<T> inCollection(final String collection) {
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, support);
collection, options, distinctFields, fields, support);
}
@Override
@Deprecated
public FindByQueryConsistentWith<T> consistentWith(QueryScanConsistency scanConsistency) {
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, support);
collection, options, distinctFields, fields, support);
}
@Override
public FindByQueryWithConsistency<T> withConsistency(QueryScanConsistency scanConsistency) {
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, support);
collection, options, distinctFields, fields, support);
}
@Override
public <R> FindByQueryWithConsistency<R> as(Class<R> returnType) {
Assert.notNull(returnType, "returnType must not be null!");
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, support);
collection, options, distinctFields, fields, support);
}
@Override
public FindByQueryWithProjection<T> project(String[] fields) {
Assert.notNull(fields, "Fields must not be null");
Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified");
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, distinctFields, fields, support);
}
@Override
public FindByQueryWithDistinct<T> distinct(final String[] distinctFields) {
Assert.notNull(distinctFields, "distinctFields must not be null!");
Assert.notNull(distinctFields, "distinctFields must not be null");
Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified");
// Coming from an annotation, this cannot be null.
// But a non-null but empty distinctFields means distinct on all fields
// So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null.
String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields;
return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope,
collection, options, dFields, support);
collection, options, dFields, fields, support);
}
@Override
@@ -228,7 +238,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
private String assembleEntityQuery(final boolean count, String[] distinctFields, String collection) {
return query.toN1qlSelectString(template, collection, this.domainType, this.returnType, count,
query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields);
query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields, fields);
}
}
}

View File

@@ -320,13 +320,13 @@ public class Query {
}
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) {
return toN1qlSelectString(template, null, domainClass, null, isCount, null);
return toN1qlSelectString(template, null, domainClass, null, isCount, null, null);
}
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass,
Class returnClass, boolean isCount, String[] distinctFields) {
Class returnClass, boolean isCount, String[] distinctFields, String[] fields) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass,
returnClass, isCount, distinctFields);
returnClass, isCount, distinctFields, fields);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.selectEntity); // select ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
@@ -340,7 +340,7 @@ public class Query {
public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) {
StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass, null,
false, null);
false, null, null);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.delete); // delete ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
@@ -350,7 +350,7 @@ public class Query {
}
StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, String collectionName,
Class domainClass, Class returnClass, boolean isCount, String[] distinctFields) {
Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, String[] fields) {
String typeKey = template.getConverter().getTypeKey();
final CouchbasePersistentEntity<?> persistentEntity = template.getConverter().getMappingContext()
.getRequiredPersistentEntity(domainClass);
@@ -363,7 +363,7 @@ public class Query {
}
StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(template.getBucketName(), collectionName,
template.getConverter(), domainClass, returnClass, typeKey, typeValue, distinctFields);
template.getConverter(), domainClass, returnClass, typeKey, typeValue, isCount, distinctFields, fields);
return isCount ? sbnqp.getCountContext() : sbnqp.getStatementContext();
}

View File

@@ -15,13 +15,13 @@
*/
package org.springframework.data.couchbase.core.query;
import java.util.Locale;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.support.TemplateUtils;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.support.TemplateUtils;
import java.util.Locale;
/**
* Query created from the string in @Query annotation in the repository interface.
@@ -56,7 +56,7 @@ public class StringQuery extends Query {
@Override
public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collection, Class domainClass,
Class resultClass, boolean isCount, String[] distinctFields) {
Class resultClass, boolean isCount, String[] distinctFields, String[] fields) {
final StringBuilder statement = new StringBuilder();
boolean makeCount = isCount && inlineN1qlQuery != null
&& !inlineN1qlQuery.toLowerCase(Locale.ROOT).contains("count(");
@@ -95,6 +95,6 @@ public class StringQuery extends Query {
*/
@Override
public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) {
return toN1qlSelectString(template, collectionName, domainClass, domainClass, false, null);
return toN1qlSelectString(template, collectionName, domainClass, domainClass, false, null, null);
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2020-2021 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.support;
/**
* A common interface for all of Insert, Replace, Upsert that take Projection
*
* @author Michael Reiche
* @param <R> - the entity class
*/
public interface WithProjecting<R> {
Object project(String[] fields);
}

View File

@@ -87,7 +87,7 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase<
CouchbaseQueryExecution execution = getExecution(accessor,
new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), null);
return execution.execute(query, processor.getReturnedType().getDomainType(), typeToRead, null);
}
/**
@@ -115,20 +115,21 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase<
if (isDeleteQuery()) {
return new DeleteExecution(getOperations(), getQueryMethod());
} else if (isTailable(getQueryMethod())) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of all()
return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of
// all()
} else if (getQueryMethod().isCollectionQuery()) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all();
return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all();
} else if (getQueryMethod().isStreamQuery()) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).stream();
return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).stream();
} else if (isCountQuery()) {
return (q, t, c) -> operation.matching(q).count();
return (q, t, r, c) -> operation.as(r).matching(q).count();
} else if (isExistsQuery()) {
return (q, t, c) -> operation.matching(q).exists();
return (q, t, r, c) -> operation.as(r).matching(q).exists();
} else if (getQueryMethod().isPageQuery()) {
return new PagedExecution(operation, accessor.getPageable());
} else {
return (q, t, c) -> {
TerminatingFindByQuery<?> find = operation.matching(q);
return (q, t, r, c) -> {
TerminatingFindByQuery<?> find = operation.as(r).matching(q);
if (isCountQuery()) {
return find.count();
}

View File

@@ -87,7 +87,7 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu
ReactiveCouchbaseQueryExecution execution = getExecution(accessor,
new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), null);
return execution.execute(query, processor.getReturnedType().getDomainType(), typeToRead, null);
}
/**
@@ -98,7 +98,7 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu
* @return
*/
private ReactiveCouchbaseQueryExecution getExecution(ParameterAccessor accessor,
Converter<Object, Object> resultProcessing, ReactiveFindByQueryOperation.FindByQueryWithQuery<?> operation) {
Converter<Object, Object> resultProcessing, ReactiveFindByQuery<?> operation) {
return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing);
}
@@ -110,23 +110,24 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu
* @return
*/
private ReactiveCouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor,
ReactiveFindByQueryOperation.FindByQueryWithQuery<?> operation) {
ReactiveFindByQuery<?> operation) {
if (isDeleteQuery()) {
return new DeleteExecution(getOperations(), getQueryMethod());
} else if (isTailable(getQueryMethod())) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of all()
return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of
// all()
} else if (getQueryMethod().isCollectionQuery()) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all();
return (q, t, r, c) -> operation.as(r).matching(q.with(accessor.getPageable())).all();
// } else if (getQueryMethod().isStreamQuery()) {
// return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all().toStream();
} else if (isCountQuery()) {
return (q, t, c) -> operation.matching(q).count();
return (q, t, r, c) -> operation.as(r).matching(q).count();
} else if (isExistsQuery()) {
return (q, t, c) -> operation.matching(q).exists();
return (q, t, r, c) -> operation.as(r).matching(q).exists();
} else {
return (q, t, c) -> {
ReactiveFindByQueryOperation.TerminatingFindByQuery<?> find = operation.matching(q);
return (q, t, r, c) -> {
ReactiveFindByQueryOperation.TerminatingFindByQuery<?> find = operation.as(r).matching(q);
return isLimiting() ? find.first() : find.one();
};
}

View File

@@ -39,7 +39,7 @@ import org.springframework.util.Assert;
@FunctionalInterface
interface CouchbaseQueryExecution {
Object execute(Query query, Class<?> type, String collection);
Object execute(Query query, Class<?> type, Class<?> returnType, String collection);
/**
* {@link CouchbaseQueryExecution} removing documents matching the query.
@@ -60,7 +60,7 @@ interface CouchbaseQueryExecution {
* @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery.Execution#execute(org.springframework.data.couchbase.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public Object execute(Query query, Class<?> type, String collection) {
public Object execute(Query query, Class<?> type, Class<?> returnType, String collection) {
return operations.removeByQuery(type).matching(query).all();
}
@@ -83,8 +83,8 @@ interface CouchbaseQueryExecution {
}
@Override
public Object execute(Query query, Class<?> type, String collection) {
return converter.convert(delegate.execute(query, type, collection));
public Object execute(Query query, Class<?> type, Class<?> returnType, String collection) {
return converter.convert(delegate.execute(query, type, returnType, collection));
}
}
@@ -109,11 +109,11 @@ interface CouchbaseQueryExecution {
*/
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Object execute(Query query, Class<?> type, String collection) {
public Object execute(Query query, Class<?> type, Class<?> returnType, String collection) {
int pageSize = pageable.getPageSize();
// Apply Pageable but tweak limit to peek into next page
Query modifiedQuery = query.skip(pageable.getOffset()).limit(pageSize + 1);
List result = find.matching(modifiedQuery).all();
List result = find.as(returnType).matching(modifiedQuery).all();
boolean hasNext = result.size() > pageSize;
return new SliceImpl<Object>(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext);
}
@@ -139,9 +139,9 @@ interface CouchbaseQueryExecution {
* @see org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution#execute(org.springframework.data.couchbase.core.query.Query)
*/
@Override
public Object execute(Query query, Class<?> type, String collection) {
public Object execute(Query query, Class<?> type, Class<?> returnType, String collection) {
int overallLimit = 0; // query.getLimit();
TerminatingFindByQuery<?> matching = operation.matching(query);
TerminatingFindByQuery<?> matching = operation.as(returnType).matching(query);
// Adjust limit if page would exceed the overall limit
if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) {
query.limit((int) (overallLimit - pageable.getOffset()));

View File

@@ -80,7 +80,7 @@ public class N1qlRepositoryQueryExecutor {
return operation.matching(query).all();
} else if (queryMethod.isPageQuery()) {
Pageable p = accessor.getPageable();
return new CouchbaseQueryExecution.PagedExecution(operation, p).execute(query, null, null);
return new CouchbaseQueryExecution.PagedExecution(operation, p).execute(query, null, null, null);
} else {
return operation.matching(query).oneValue();
}

View File

@@ -28,9 +28,10 @@ import org.springframework.util.Assert;
* @author Michael Reiche
* @since 4.1
*/
@FunctionalInterface
interface ReactiveCouchbaseQueryExecution {
Object execute(Query query, Class<?> type, String collection);
Object execute(Query query, Class<?> type, Class<?> returnType, String collection);
/**
* {@link ReactiveCouchbaseQueryExecution} removing documents matching the query.
@@ -51,7 +52,7 @@ interface ReactiveCouchbaseQueryExecution {
* @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery.Execution#execute(org.springframework.data.couchbase.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public Object execute(Query query, Class<?> type, String collection) {
public Object execute(Query query, Class<?> type, Class<?> returnType, String collection) {
return operations.removeByQuery(type)/*.inCollection(collection)*/.matching(query).all();
}
@@ -74,8 +75,8 @@ interface ReactiveCouchbaseQueryExecution {
}
@Override
public Object execute(Query query, Class<?> type, String collection) {
return converter.convert(delegate.execute(query, type, collection));
public Object execute(Query query, Class<?> type, Class<?> returnType, String collection) {
return converter.convert(delegate.execute(query, type, returnType, collection));
}
}

View File

@@ -21,12 +21,15 @@ import static org.springframework.data.couchbase.core.support.TemplateUtils.SELE
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.couchbase.client.core.error.CouchbaseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
@@ -127,28 +130,38 @@ public class StringBasedN1qlQueryParser {
this.queryMethod = queryMethod;
this.couchbaseConverter = couchbaseConverter;
String collection = queryMethod.getCollection();
this.statementContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, false, null);
this.countContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, true, null);
this.statementContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, false, null,
null);
this.countContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, true, null,
null);
this.parsedExpression = getExpression(accessor, getParameters(accessor), null, parser, evaluationContextProvider);
checkPlaceholders(this.parsedExpression.toString());
}
public StringBasedN1qlQueryParser(String bucketName, String collection, CouchbaseConverter couchbaseConverter,
Class domainClass, Class resultClass, String typeField, String typeValue, String[] distinctFields) {
Class domainClass, Class resultClass, String typeField, String typeValue, boolean isCount,
String[] distinctFields, String[] fields) {
this.statement = null;
this.queryMethod = null;
this.couchbaseConverter = couchbaseConverter;
this.statementContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue,
false, distinctFields);
this.countContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue,
true, distinctFields);
if (!isCount) {
this.statementContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField,
typeValue, false, distinctFields, fields);
this.countContext = null;
} else {
this.statementContext = null;
this.countContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue,
true, distinctFields, fields);
}
this.parsedExpression = null;
}
public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, Class domainClass, Class resultClass,
String typeField, String typeValue, boolean isCount, String[] distinctFields) {
String typeField, String typeValue, boolean isCount, String[] distinctFields, String[] fields) {
String b = collection != null ? collection : bucketName;
String projectedFields = getProjectedFields(b, resultClass);
Assert.isTrue(!(distinctFields != null && fields != null),
"only one of project(fields) and distinct(distinctFields) can be specified");
String projectedFields = getProjectedFields(b, resultClass, fields);
String entity = "META(" + i(b) + ").id AS " + SELECT_ID + ", META(" + i(b) + ").cas AS " + SELECT_CAS;
String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS;
String selectEntity;
@@ -163,7 +176,7 @@ public class StringBasedN1qlQueryParser {
} else if (isCount) {
selectEntity = "SELECT " + count + " FROM " + i(b);
} else {
selectEntity = "SELECT " + entity + ", " + projectedFields + " FROM " + i(b);
selectEntity = "SELECT " + entity + (!projectedFields.isEmpty() ? ", " : " ") + projectedFields + " FROM " + i(b);
}
String typeSelection = "`" + typeField + "` = \"" + typeValue + "\"";
@@ -177,50 +190,77 @@ public class StringBasedN1qlQueryParser {
return i(distinctFields).toString();
}
private String getProjectedFields(String b, Class resultClass) {
private String getProjectedFields(String b, Class resultClass, String[] fields) {
String projectedFields = i(b) + ".*";
if (resultClass != null) {
PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass);
StringBuilder sb = new StringBuilder();
getProjectedFieldsInternal(b, null, sb, persistentEntity.getTypeInformation()/*, ""*/);
getProjectedFieldsInternal(b, null, sb, persistentEntity.getTypeInformation(), fields/*, ""*/);
projectedFields = sb.toString();
}
return projectedFields;
}
private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentProperty parent, StringBuilder sb,
TypeInformation resultClass/*, String path*/) {
TypeInformation resultClass, String[] fields/*, String path*/) {
PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass);
// CouchbasePersistentProperty property = path.getLeafProperty();
persistentEntity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
@Override
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
if (prop.isIdProperty() && parent == null) {
return;
if (resultClass != null) {
Set<String> fieldList = fields != null ? new HashSet<>(Arrays.asList(fields)) : null;
PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass);
// CouchbasePersistentProperty property = path.getLeafProperty();
persistentEntity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
@Override
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
if (prop.isIdProperty() && parent == null) {
return;
}
if (prop.isVersionProperty()) {
return;
}
String projectField = null;
if (fieldList == null || fieldList.contains(prop.getFieldName())) {
PersistentPropertyPath<CouchbasePersistentProperty> path = couchbaseConverter.getMappingContext()
.getPersistentPropertyPath(prop.getName(), resultClass.getType());
projectField = N1qlQueryCreator.addMetaIfRequired(bucketName, path, prop).toString();
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(projectField); // from N1qlQueryCreator
}
if (fieldList != null) {
fieldList.remove(prop.getFieldName());
}
// The current limitation is that only top-level properties can be projected
// This traversing of nested data structures would need to replicate the processing done by
// MappingCouchbaseConverter. Either the read or write
// And the n1ql to project lower-level properties is complex
// if (!conversions.isSimpleType(prop.getType())) {
// getProjectedFieldsInternal(prop, sb, prop.getTypeInformation(), path+prop.getName()+".");
// } else {
// }
}
if (prop.isVersionProperty()) {
return;
}
PersistentPropertyPath<CouchbasePersistentProperty> path = couchbaseConverter.getMappingContext()
.getPersistentPropertyPath(prop.getName(), resultClass.getType());
// The current limitation is that only top-level properties can be projected
// This traversing of nested data structures would need to replicate the processing done by
// MappingCouchbaseConverter. Either the read or write
// And the n1ql to project lower-level properties is complex
// if (!conversions.isSimpleType(prop.getType())) {
// getProjectedFieldsInternal(prop, sb, prop.getTypeInformation(), path+prop.getName()+".");
// } else {
});
// throw an exception if there is an request for a field not in the entity.
// needs further discussion as removing a field from an entity could cause this and not necessarily be an error
if (fieldList != null && !fieldList.isEmpty()) {
throw new CouchbaseException(
"projected fields (" + fieldList + ") not found in entity: " + persistentEntity.getName());
}
} else {
for (String field : fields) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(N1qlQueryCreator.addMetaIfRequired(bucketName, path, prop)); // from N1qlQueryCreator
// }
sb.append(x(field));
}
});
}
}
// this static method can be used to test the parsing behavior for Couchbase specific spel variables

View File

@@ -27,10 +27,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import com.couchbase.client.core.error.CouchbaseException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
@@ -38,18 +41,23 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById;
import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById;
import org.springframework.data.couchbase.core.support.OneAndAllEntity;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.Course;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.PersonValue;
import org.springframework.data.couchbase.domain.Submission;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserAnnotated;
import org.springframework.data.couchbase.domain.UserAnnotated2;
import org.springframework.data.couchbase.domain.UserAnnotated3;
import org.springframework.data.couchbase.domain.UserSubmission;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import com.couchbase.client.java.kv.PersistTo;
import com.couchbase.client.java.kv.ReplicateTo;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
;
@@ -86,8 +94,8 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
modifying.setVersion(user.getVersion());
modified = couchbaseTemplate.replaceById(User.class).one(modifying);
assertEquals(modifying, modified);
if(user == modified){
throw new RuntimeException ( " user == modified ");
if (user == modified) {
throw new RuntimeException(" user == modified ");
}
assertNotEquals(user, modified);
assertEquals(NaiveAuditorAware.AUDITOR, modified.getCreatedBy());
@@ -107,6 +115,52 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
couchbaseTemplate.removeById().one(user.getId());
}
@Test
void findProjected() {
User user = new User(UUID.randomUUID().toString(), "firstname", "lastname");
couchbaseTemplate.insertById(User.class).one(user);
User found = couchbaseTemplate.findById(User.class).project(new String[] { "firstname" }).one(user.getId());
System.err.println(found);
couchbaseTemplate.removeById(User.class).one(user.getId());
}
@Test
void findProjecting() {
User user = new User(UUID.randomUUID().toString(), "firstname", "lastname");
couchbaseTemplate.insertById(User.class).one(user);
List<User> found = couchbaseTemplate.findByQuery(User.class).project(new String[] { "firstname" })
.withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)).all();
assertEquals(1, found.size());
assertNotEquals(user, found.get(0), "should have found this document");
assertEquals(user.getFirstname(), found.get(0).getFirstname(), "firstname should match");
assertNull(found.get(0).getLastname(), "lastname should be null");
couchbaseTemplate.removeById(User.class).one(user.getId());
}
@Test
void findProjectingPath() {
UserSubmission user = new UserSubmission();
user.setId(UUID.randomUUID().toString());
user.setUsername("dave");
user.setRoles(Arrays.asList("role1", "role2"));
Address address = new Address();
address.setStreet("1234 Olcott Street");
address.setCity("Santa Clara");
user.setAddress(address);
user.setSubmissions(
Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123)));
couchbaseTemplate.upsertById(UserSubmission.class).one(user);
assertThrows(CouchbaseException.class, () -> couchbaseTemplate.findByQuery(UserSubmission.class).project(new String[] { "address.street" })
.withConsistency(QueryScanConsistency.REQUEST_PLUS).all());
List<UserSubmission> found = couchbaseTemplate.findByQuery(UserSubmission.class).project(new String[] { "address" })
.withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
assertEquals(found.size(), 1);
assertEquals(found.get(0).getAddress(), address);
assertNull(found.get(0).getUsername(), "username should have been null");
couchbaseTemplate.removeById(User.class).one(user.getId());
}
@Test
void withDurability()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import java.util.Objects;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.couchbase.core.mapping.Document;
/**
* AirportMini entity
*
* @author Michael Reiche
*/
@Document
public class AirportMini extends ComparableEntity {
@Id private String id;
private String iata;
private Address address;
@PersistenceConstructor
public AirportMini(final String id, final String iata) {
this.id = id;
this.iata = iata;
}
public String getId() {
return id;
}
public String getIata() {
return iata;
}
public void setIata(String iata) {
this.iata = iata;
}
@Override
public int hashCode() {
return Objects.hash(id, iata);
}
}

View File

@@ -68,6 +68,9 @@ public interface AirportRepository extends CouchbaseRepository<Airport, String>,
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> findAllByIata(String iata);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<AirportMini> getByIata(String iata);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
@ComposedMetaAnnotation(collection = "_default", timeoutMs = 1000)
Airport findByIata(String iata);

View File

@@ -56,6 +56,7 @@ import org.springframework.data.couchbase.core.query.N1QLExpression;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.AirportMini;
import org.springframework.data.couchbase.domain.AirportRepository;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.Person;
@@ -239,6 +240,20 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
}
}
@Test
void findBySimplePropertyReturnType() {
Airport vie = null;
try {
vie = new Airport("airports::vie", "vie", "low6");
vie = airportRepository.save(vie);
List<AirportMini> airports = airportRepository.getByIata("vie");
assertEquals(1, airports.size());
System.out.println(airports.get(0));
} finally {
airportRepository.delete(vie);
}
}
@Test
void findByTypeAlias() {
Airport vie = null;