Skip to content

Commit 9d8249e

Browse files
committed
Support derived queries from methods named findDistinctField1AndField2ByField3().
Closes #1200.
1 parent dffd203 commit 9d8249e

File tree

7 files changed

+177
-20
lines changed

7 files changed

+177
-20
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

+45-3
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}.
@@ -136,7 +178,7 @@ public Query with(final Pageable pageable) {
136178
}
137179
this.limit = pageable.getPageSize();
138180
this.skip = pageable.getOffset();
139-
if(!this.sort.equals(pageable.getSort()))
181+
if (!this.sort.equals(pageable.getSort()))
140182
this.sort.and(pageable.getSort());
141183
return this;
142184
}
@@ -176,7 +218,7 @@ public Query with(final Sort sort) {
176218
return this;
177219
}
178220

179-
public Query withoutSort(){
221+
public Query withoutSort() {
180222
this.sort = Sort.unsorted();
181223
return this;
182224
}
@@ -294,7 +336,7 @@ public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String coll
294336
appendString(statement, n1ql.selectEntity); // select ...
295337
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
296338
appendWhere(statement, new int[] { 0 }, template.getConverter()); // criteria on this Query
297-
if(!isCount){
339+
if (!isCount) {
298340
appendSort(statement);
299341
appendSkipAndLimit(statement);
300342
}
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

+14-2
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ public interface AirportRepository extends CouchbaseRepository<Airport, String>,
114114
Long countFancyExpression(@Param("projectIds") List<String> projectIds, @Param("planIds") List<String> planIds,
115115
@Param("active") Boolean active);
116116

117-
@Query("SELECT 1 FROM `#{#n1ql.bucket}` WHERE 0 = 1" )
117+
@Query("SELECT 1 FROM `#{#n1ql.bucket}` WHERE 0 = 1")
118118
Long countBad();
119119

120-
@Query("SELECT count(*) FROM `#{#n1ql.bucket}`" )
120+
@Query("SELECT count(*) FROM `#{#n1ql.bucket}`")
121121
Long countGood();
122122

123123
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
@@ -126,6 +126,18 @@ Long countFancyExpression(@Param("projectIds") List<String> projectIds, @Param("
126126
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
127127
Optional<Airport> findByIdAndIata(String id, String iata);
128128

129+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
130+
List<Airport> findDistinctIcaoBy();
131+
132+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
133+
List<Airport> findDistinctIcaoAndIataBy();
134+
135+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
136+
Long countDistinctIcaoAndIataBy();
137+
138+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
139+
Long countDistinctIcaoBy();
140+
129141
@Retention(RetentionPolicy.RUNTIME)
130142
@Target({ ElementType.METHOD, ElementType.TYPE })
131143
// @Meta

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

+40-8
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import java.util.concurrent.Future;
4040
import java.util.stream.Collectors;
4141

42-
import com.couchbase.client.java.kv.UpsertOptions;
4342
import org.junit.jupiter.api.BeforeEach;
4443
import org.junit.jupiter.api.Test;
4544
import org.springframework.beans.factory.annotation.Autowired;
@@ -89,6 +88,7 @@
8988
import com.couchbase.client.java.json.JsonArray;
9089
import com.couchbase.client.java.kv.GetResult;
9190
import com.couchbase.client.java.kv.MutationState;
91+
import com.couchbase.client.java.kv.UpsertOptions;
9292
import com.couchbase.client.java.query.QueryOptions;
9393
import com.couchbase.client.java.query.QueryScanConsistency;
9494

@@ -188,9 +188,9 @@ void annotatedFieldFindName() {
188188
Person person2 = personRepository.findById(person.getId().toString()).get();
189189
assertEquals(person.getSalutation(), person2.getSalutation());
190190
// needs fix from datacouch_1184
191-
//List<Person> persons3 = personRepository.findBySalutation("Mrs");
192-
//assertEquals(1, persons3.size());
193-
//assertEquals(person.getSalutation(), persons3.get(0).getSalutation());
191+
// List<Person> persons3 = personRepository.findBySalutation("Mrs");
192+
// assertEquals(1, persons3.size());
193+
// assertEquals(person.getSalutation(), persons3.get(0).getSalutation());
194194
} finally {
195195
personRepository.deleteById(person.getId().toString());
196196
}
@@ -476,13 +476,13 @@ void count() {
476476
}
477477
}
478478

479-
@Test
480-
void badCount(){
479+
@Test
480+
void badCount() {
481481
assertThrows(CouchbaseQueryExecutionException.class, () -> airportRepository.countBad());
482482
}
483483

484-
@Test
485-
void goodCount(){
484+
@Test
485+
void goodCount() {
486486
airportRepository.countGood();
487487
}
488488

@@ -528,6 +528,38 @@ void threadSafeParametersTest() throws Exception {
528528
}
529529
}
530530

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

0 commit comments

Comments
 (0)