Support derived queries from methods named findDistinctF1AndF2ByF3() (#1202)

Support derived queries from methods named findDistinctF1AndF2ByF3().

Closes #1200.
This commit is contained in:
Michael Reiche
2021-08-23 13:51:55 -07:00
committed by GitHub
parent f0880daf7a
commit bcf11ce856
7 changed files with 164 additions and 7 deletions

View File

@@ -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);
}
}
}

View File

@@ -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}.

View File

@@ -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];
}
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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");