Support derived queries from methods named findDistinctF1AndF2ByF3() (#1202)
Support derived queries from methods named findDistinctF1AndF2ByF3(). Closes #1200.
This commit is contained in:
@@ -176,7 +176,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
|
||||
}).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> {
|
||||
String id = "";
|
||||
long cas = 0;
|
||||
if (distinctFields == null) {
|
||||
if (!query.isDistinct() && distinctFields == null) {
|
||||
if (row.getString(TemplateUtils.SELECT_ID) == null) {
|
||||
return Flux.error(new CouchbaseException(
|
||||
"query did not project " + TemplateUtils.SELECT_ID + ". Either use #{#n1ql.selectEntity} or project "
|
||||
@@ -227,7 +227,8 @@ 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, distinctFields);
|
||||
return query.toN1qlSelectString(template, collection, this.domainType, this.returnType, count,
|
||||
query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ public class Query {
|
||||
private JsonValue parameters = JsonValue.ja();
|
||||
private long skip;
|
||||
private int limit;
|
||||
private boolean distinct;
|
||||
private String[] distinctFields;
|
||||
private Sort sort = Sort.unsorted();
|
||||
private QueryScanConsistency queryScanConsistency;
|
||||
private Meta meta;
|
||||
@@ -123,6 +125,46 @@ public class Query {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a DISTINCT query? {@code distinct}.
|
||||
*
|
||||
* @param distinct
|
||||
* @return
|
||||
*/
|
||||
public Query distinct(boolean distinct) {
|
||||
this.distinct = distinct;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a DISTINCT query? {@code distinct}.
|
||||
*
|
||||
* @return distinct
|
||||
*/
|
||||
public boolean isDistinct() {
|
||||
return distinct;
|
||||
}
|
||||
|
||||
/**
|
||||
* distinctFields for query (non-null but empty means all fields) ? {@code distinctFields}.
|
||||
*
|
||||
* @param distinctFields
|
||||
* @return
|
||||
*/
|
||||
public Query distinct(String[] distinctFields) {
|
||||
this.distinctFields = distinctFields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* distinctFields for query (non-null but empty means all fields) ? {@code distinctFields}.
|
||||
*
|
||||
* @return distinctFields
|
||||
*/
|
||||
public String[] getDistinctFields() {
|
||||
return distinctFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 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.repository.query;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* Extend PartTree to parse out distinct fields
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class CouchbasePartTree extends PartTree {
|
||||
private static final Pattern DISTINCT_TEMPLATE = Pattern
|
||||
.compile("^(find|read|get|query|search|stream|count)(Distinct)(\\p{Lu}.*?)(First|Top|By|$)");
|
||||
|
||||
String[] distinctFields;
|
||||
|
||||
public CouchbasePartTree(String methodName, Class<?> domainType) {
|
||||
super(methodName, domainType);
|
||||
maybeInitDistinctFields(methodName, domainType);
|
||||
}
|
||||
|
||||
String[] getDistinctFields() {
|
||||
return distinctFields;
|
||||
}
|
||||
|
||||
private void maybeInitDistinctFields(String methodName, Class<?> domainType) {
|
||||
if (isDistinct()) {
|
||||
Matcher grp = DISTINCT_TEMPLATE.matcher(methodName);
|
||||
if (grp.matches()) {
|
||||
String grp3 = grp.group(3);
|
||||
String[] names = grp.group(3).split("And");
|
||||
int parameterCount = names.length;
|
||||
distinctFields = new String[names.length];
|
||||
for (int i = 0; i < parameterCount; ++i) {
|
||||
Part.Type type = Part.Type.fromProperty(names[i]);
|
||||
PropertyPath path = PropertyPath.from(type.extractProperty(names[i]), domainType);
|
||||
distinctFields[i] = path.toDotPath();
|
||||
}
|
||||
} else {
|
||||
distinctFields = new String[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Michael Nitschinger
|
||||
@@ -52,6 +51,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
|
||||
public static final String META_CAS_PROPERTY = "cas";
|
||||
public static final String META_EXPIRATION_PROPERTY = "expiration";
|
||||
|
||||
private final PartTree tree;
|
||||
private final ParameterAccessor accessor;
|
||||
private final MappingContext<?, CouchbasePersistentProperty> context;
|
||||
private final QueryMethod queryMethod;
|
||||
@@ -61,6 +61,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
|
||||
public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, final QueryMethod queryMethod,
|
||||
final CouchbaseConverter converter, final String bucketName) {
|
||||
super(tree, accessor);
|
||||
this.tree = tree;
|
||||
this.accessor = accessor;
|
||||
this.context = converter.getMappingContext();
|
||||
this.queryMethod = queryMethod;
|
||||
@@ -79,10 +80,11 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
|
||||
public Query createQuery() {
|
||||
Query q = this.createQuery((Optional.of(this.accessor).map(ParameterAccessor::getSort).orElse(Sort.unsorted())));
|
||||
Pageable pageable = accessor.getPageable();
|
||||
if(pageable.isPaged()) {
|
||||
if (pageable.isPaged()) {
|
||||
q.skip(pageable.getOffset());
|
||||
q.limit(pageable.getPageSize());
|
||||
}
|
||||
q.distinct(tree.isDistinct());
|
||||
return q;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -35,7 +35,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
*/
|
||||
public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery {
|
||||
|
||||
private final PartTree tree;
|
||||
private final CouchbasePartTree tree;
|
||||
private final CouchbaseConverter converter;
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery {
|
||||
super(method, operations, expressionParser, evaluationContextProvider);
|
||||
|
||||
ResultProcessor processor = method.getResultProcessor();
|
||||
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
|
||||
this.tree = new CouchbasePartTree(method.getName(), processor.getReturnedType().getDomainType());
|
||||
this.converter = operations.getConverter();
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery {
|
||||
if (tree.isLimiting()) {
|
||||
query.limit(tree.getMaxResults());
|
||||
}
|
||||
if (tree.isDistinct()) {
|
||||
query.distinct(tree.getDistinctFields());
|
||||
}
|
||||
return query;
|
||||
|
||||
}
|
||||
@@ -128,4 +131,5 @@ public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery {
|
||||
protected boolean isLimiting() {
|
||||
return tree.isLimiting();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -130,6 +130,18 @@ public interface AirportRepository extends CouchbaseRepository<Airport, String>,
|
||||
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
|
||||
Optional<Airport> findByIdAndIata(String id, String iata);
|
||||
|
||||
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
|
||||
List<Airport> findDistinctIcaoBy();
|
||||
|
||||
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
|
||||
List<Airport> findDistinctIcaoAndIataBy();
|
||||
|
||||
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
|
||||
Long countDistinctIcaoAndIataBy();
|
||||
|
||||
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
|
||||
Long countDistinctIcaoBy();
|
||||
|
||||
@Query("SELECT 1 FROM `#{#n1ql.bucket}` WHERE #{#n1ql.filter} " + " #{#projectIds != null ? 'AND blah IN $1' : ''} "
|
||||
+ " #{#planIds != null ? 'AND blahblah IN $2' : ''} " + " #{#active != null ? 'AND false = $3' : ''} ")
|
||||
Long countOne();
|
||||
|
||||
@@ -533,6 +533,38 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void distinct() {
|
||||
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
|
||||
String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" };
|
||||
|
||||
try {
|
||||
for (int i = 0; i < iatas.length; i++) {
|
||||
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */);
|
||||
couchbaseTemplate.insertById(Airport.class).one(airport);
|
||||
}
|
||||
|
||||
// distinct icao - parser requires 'By' on the end or it does not match pattern.
|
||||
List<Airport> airports1 = airportRepository.findDistinctIcaoBy();
|
||||
assertEquals(2, airports1.size());
|
||||
|
||||
List<Airport> airports2 = airportRepository.findDistinctIcaoAndIataBy();
|
||||
assertEquals(7, airports2.size());
|
||||
|
||||
// count( distinct { iata, icao } )
|
||||
long count1 = airportRepository.countDistinctIcaoAndIataBy();
|
||||
assertEquals(7, count1);
|
||||
|
||||
// count( distinct { icao } )
|
||||
long count2 = airportRepository.countDistinctIcaoBy();
|
||||
assertEquals(2, count2);
|
||||
|
||||
} finally {
|
||||
couchbaseTemplate.removeById()
|
||||
.all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringQueryTest() throws Exception {
|
||||
Airport airport = new Airport("airports::vie", "vie", "lowx");
|
||||
|
||||
Reference in New Issue
Block a user