Skip to content

Commit bcf11ce

Browse files
authored
Support derived queries from methods named findDistinctF1AndF2ByF3() (#1202)
Support derived queries from methods named findDistinctF1AndF2ByF3(). Closes #1200.
1 parent f0880da commit bcf11ce

File tree

7 files changed

+164
-7
lines changed

7 files changed

+164
-7
lines changed

src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public Flux<T> all() {
176176
}).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> {
177177
String id = "";
178178
long cas = 0;
179-
if (distinctFields == null) {
179+
if (!query.isDistinct() && distinctFields == null) {
180180
if (row.getString(TemplateUtils.SELECT_ID) == null) {
181181
return Flux.error(new CouchbaseException(
182182
"query did not project " + TemplateUtils.SELECT_ID + ". Either use #{#n1ql.selectEntity} or project "
@@ -227,7 +227,8 @@ public Mono<Boolean> exists() {
227227
}
228228

229229
private String assembleEntityQuery(final boolean count, String[] distinctFields, String collection) {
230-
return query.toN1qlSelectString(template, collection, this.domainType, this.returnType, count, distinctFields);
230+
return query.toN1qlSelectString(template, collection, this.domainType, this.returnType, count,
231+
query.getDistinctFields() != null ? query.getDistinctFields() : distinctFields);
231232
}
232233
}
233234
}

src/main/java/org/springframework/data/couchbase/core/query/Query.java

+42
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public class Query {
5151
private JsonValue parameters = JsonValue.ja();
5252
private long skip;
5353
private int limit;
54+
private boolean distinct;
55+
private String[] distinctFields;
5456
private Sort sort = Sort.unsorted();
5557
private QueryScanConsistency queryScanConsistency;
5658
private Meta meta;
@@ -123,6 +125,46 @@ public Query limit(int limit) {
123125
return this;
124126
}
125127

128+
/**
129+
* Is this a DISTINCT query? {@code distinct}.
130+
*
131+
* @param distinct
132+
* @return
133+
*/
134+
public Query distinct(boolean distinct) {
135+
this.distinct = distinct;
136+
return this;
137+
}
138+
139+
/**
140+
* Is this a DISTINCT query? {@code distinct}.
141+
*
142+
* @return distinct
143+
*/
144+
public boolean isDistinct() {
145+
return distinct;
146+
}
147+
148+
/**
149+
* distinctFields for query (non-null but empty means all fields) ? {@code distinctFields}.
150+
*
151+
* @param distinctFields
152+
* @return
153+
*/
154+
public Query distinct(String[] distinctFields) {
155+
this.distinctFields = distinctFields;
156+
return this;
157+
}
158+
159+
/**
160+
* distinctFields for query (non-null but empty means all fields) ? {@code distinctFields}.
161+
*
162+
* @return distinctFields
163+
*/
164+
public String[] getDistinctFields() {
165+
return distinctFields;
166+
}
167+
126168
/**
127169
* Sets the given pagination information on the {@link Query} instance. Will transparently set {@code skip} and
128170
* {@code limit} as well as applying the {@link Sort} instance defined with the {@link Pageable}.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.couchbase.repository.query;
18+
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
22+
import org.springframework.data.mapping.PropertyPath;
23+
import org.springframework.data.repository.query.parser.Part;
24+
import org.springframework.data.repository.query.parser.PartTree;
25+
26+
/**
27+
* Extend PartTree to parse out distinct fields
28+
*
29+
* @author Michael Reiche
30+
*/
31+
public class CouchbasePartTree extends PartTree {
32+
private static final Pattern DISTINCT_TEMPLATE = Pattern
33+
.compile("^(find|read|get|query|search|stream|count)(Distinct)(\\p{Lu}.*?)(First|Top|By|$)");
34+
35+
String[] distinctFields;
36+
37+
public CouchbasePartTree(String methodName, Class<?> domainType) {
38+
super(methodName, domainType);
39+
maybeInitDistinctFields(methodName, domainType);
40+
}
41+
42+
String[] getDistinctFields() {
43+
return distinctFields;
44+
}
45+
46+
private void maybeInitDistinctFields(String methodName, Class<?> domainType) {
47+
if (isDistinct()) {
48+
Matcher grp = DISTINCT_TEMPLATE.matcher(methodName);
49+
if (grp.matches()) {
50+
String grp3 = grp.group(3);
51+
String[] names = grp.group(3).split("And");
52+
int parameterCount = names.length;
53+
distinctFields = new String[names.length];
54+
for (int i = 0; i < parameterCount; ++i) {
55+
Part.Type type = Part.Type.fromProperty(names[i]);
56+
PropertyPath path = PropertyPath.from(type.extractProperty(names[i]), domainType);
57+
distinctFields[i] = path.toDotPath();
58+
}
59+
} else {
60+
distinctFields = new String[0];
61+
}
62+
}
63+
}
64+
}

src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
4040
import org.springframework.data.repository.query.parser.Part;
4141
import org.springframework.data.repository.query.parser.PartTree;
42-
import org.springframework.util.Assert;
4342

4443
/**
4544
* @author Michael Nitschinger
@@ -52,6 +51,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
5251
public static final String META_CAS_PROPERTY = "cas";
5352
public static final String META_EXPIRATION_PROPERTY = "expiration";
5453

54+
private final PartTree tree;
5555
private final ParameterAccessor accessor;
5656
private final MappingContext<?, CouchbasePersistentProperty> context;
5757
private final QueryMethod queryMethod;
@@ -61,6 +61,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
6161
public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, final QueryMethod queryMethod,
6262
final CouchbaseConverter converter, final String bucketName) {
6363
super(tree, accessor);
64+
this.tree = tree;
6465
this.accessor = accessor;
6566
this.context = converter.getMappingContext();
6667
this.queryMethod = queryMethod;
@@ -79,10 +80,11 @@ protected QueryCriteria create(final Part part, final Iterator<Object> iterator)
7980
public Query createQuery() {
8081
Query q = this.createQuery((Optional.of(this.accessor).map(ParameterAccessor::getSort).orElse(Sort.unsorted())));
8182
Pageable pageable = accessor.getPageable();
82-
if(pageable.isPaged()) {
83+
if (pageable.isPaged()) {
8384
q.skip(pageable.getOffset());
8485
q.limit(pageable.getPageSize());
8586
}
87+
q.distinct(tree.isDistinct());
8688
return q;
8789
}
8890

src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 the original author or authors.
2+
* Copyright 2020-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@
3535
*/
3636
public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery {
3737

38-
private final PartTree tree;
38+
private final CouchbasePartTree tree;
3939
private final CouchbaseConverter converter;
4040

4141
/**
@@ -52,7 +52,7 @@ public PartTreeCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations o
5252
super(method, operations, expressionParser, evaluationContextProvider);
5353

5454
ResultProcessor processor = method.getResultProcessor();
55-
this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType());
55+
this.tree = new CouchbasePartTree(method.getName(), processor.getReturnedType().getDomainType());
5656
this.converter = operations.getConverter();
5757
}
5858

@@ -79,6 +79,9 @@ protected Query createQuery(ParametersParameterAccessor accessor) {
7979
if (tree.isLimiting()) {
8080
query.limit(tree.getMaxResults());
8181
}
82+
if (tree.isDistinct()) {
83+
query.distinct(tree.getDistinctFields());
84+
}
8285
return query;
8386

8487
}
@@ -128,4 +131,5 @@ protected boolean isDeleteQuery() {
128131
protected boolean isLimiting() {
129132
return tree.isLimiting();
130133
}
134+
131135
}

src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java

+12
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,18 @@ Long countFancyExpression(@Param("projectIds") List<String> projectIds, @Param("
130130
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
131131
Optional<Airport> findByIdAndIata(String id, String iata);
132132

133+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
134+
List<Airport> findDistinctIcaoBy();
135+
136+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
137+
List<Airport> findDistinctIcaoAndIataBy();
138+
139+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
140+
Long countDistinctIcaoAndIataBy();
141+
142+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
143+
Long countDistinctIcaoBy();
144+
133145
@Query("SELECT 1 FROM `#{#n1ql.bucket}` WHERE #{#n1ql.filter} " + " #{#projectIds != null ? 'AND blah IN $1' : ''} "
134146
+ " #{#planIds != null ? 'AND blahblah IN $2' : ''} " + " #{#active != null ? 'AND false = $3' : ''} ")
135147
Long countOne();

src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java

+32
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,38 @@ void threadSafeParametersTest() throws Exception {
533533
}
534534
}
535535

536+
@Test
537+
void distinct() {
538+
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
539+
String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" };
540+
541+
try {
542+
for (int i = 0; i < iatas.length; i++) {
543+
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */);
544+
couchbaseTemplate.insertById(Airport.class).one(airport);
545+
}
546+
547+
// distinct icao - parser requires 'By' on the end or it does not match pattern.
548+
List<Airport> airports1 = airportRepository.findDistinctIcaoBy();
549+
assertEquals(2, airports1.size());
550+
551+
List<Airport> airports2 = airportRepository.findDistinctIcaoAndIataBy();
552+
assertEquals(7, airports2.size());
553+
554+
// count( distinct { iata, icao } )
555+
long count1 = airportRepository.countDistinctIcaoAndIataBy();
556+
assertEquals(7, count1);
557+
558+
// count( distinct { icao } )
559+
long count2 = airportRepository.countDistinctIcaoBy();
560+
assertEquals(2, count2);
561+
562+
} finally {
563+
couchbaseTemplate.removeById()
564+
.all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet()));
565+
}
566+
}
567+
536568
@Test
537569
void stringQueryTest() throws Exception {
538570
Airport airport = new Airport("airports::vie", "vie", "lowx");

0 commit comments

Comments
 (0)