DATACOUCH-168 - Support and document all reduce functions

Detection whether or not reduce should be activated is now centralized. Triggers are:
  - using the count prefix instead of find in method name
  - setting reduce = true in the View annotation explicitly.

The reduce can thus be returning any kind of value (added an integration example with _stats), which is now properly documented.
This commit is contained in:
Simon Baslé
2015-10-20 17:44:43 +02:00
parent ec129e86a8
commit 58d98c5f28
8 changed files with 71 additions and 18 deletions

View File

@@ -56,9 +56,11 @@ public class CouchbaseRepositoryViewListener extends DependencyInjectionTestExec
private void createAndWaitForDesignDocs(final Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository.User\") { emit(null, null); } }";
String mapFunctionName = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository.User\") { emit(doc.username, null); } }";
String mapFunctionAge = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository.User\") { emit(doc.age, doc.age); } }";
View view = DefaultView.create("customFindAllView", mapFunction, "_count");
View customFindByNameView = DefaultView.create("customFindByNameView", mapFunctionName, "_count");
List<View> views = Arrays.asList(view, customFindByNameView);
View customFindByAgeStatsView = DefaultView.create("customFindByAgeStatsView", mapFunctionAge, "_stats");
List<View> views = Arrays.asList(view, customFindByNameView, customFindByAgeStatsView);
DesignDocument designDoc = DesignDocument.create("user", views);
client.bucketManager().upsertDesignDocument(designDoc);

View File

@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.List;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
@@ -111,6 +112,17 @@ public class CouchbaseRepositoryViewTests {
assertEquals(12, count);
}
@Test
public void shouldDeriveViewParametersAndReduceNonNumerical() {
JsonObject reduceResult = repository.findByAgeLessThan(50);
assertNotNull(reduceResult);
assertEquals(51, (long) reduceResult.getLong("count"));
assertEquals(50, (long) reduceResult.getLong("max"));
assertEquals(0, (long) reduceResult.getLong("min"));
assertEquals(1275, (long) reduceResult.getLong("sum"));
}
@Test
public void shouldDeriveViewParameters() {
String lowKey = "uname-1";

View File

@@ -18,6 +18,8 @@ package org.springframework.data.couchbase.repository;
import java.util.List;
import com.couchbase.client.java.document.json.JsonObject;
import org.springframework.data.couchbase.core.view.View;
/**
@@ -72,4 +74,7 @@ public interface CustomUserRepository extends CouchbaseRepository<User, String>
@View
long countCustomFindInvalid();
@View(viewName = "customFindByAgeStatsView", reduce = true)
JsonObject findByAgeLessThan(int maxAge);
}

View File

@@ -61,8 +61,15 @@ List<User> findFirst3ByLastnameEquals(String lastName);
[[couchbase.migrating.reduce-in-views]]
=== Reduce in views
TIP: Reduce is now supported. It will be triggered by prefixing the method name with `count` instead of `find`.
Reduce is now supported in view-based querying.
It can be triggered by prefixing the method name with `count` instead of `find`.
For example: `countByLastnameContains(String word)` instead of `findByLastnameContains(String word)`.
WARNING: Don't forget to specify an `int/long` reduce function (not limited to `_count`) in your view if you plan to use that. Similarly, views backing a query derivation should emit a simple key (not `null` nor a compound key).
Alternatively, it can be explicitly be activated by setting `reduce = true` on the `@View` annotation.
Be sure to construct your view correctly:
* specify a reduce function that matches the method return type, which can be anything, eg. long or JSON object
* emit a simple key (not `null` nor a compound key).
* emit a value suitable for the reduce to work (typically `_count` doesn't need any particular value, but `_stats` will need a numerical value, in addition to the key).

View File

@@ -315,7 +315,9 @@ For view-based query derivation, here are the supported keywords (A and B are me
|`IsIn,In`|`findByFieldIn`|`keys=[A]` - A should be a `Collection`/`Array` with elements compatible for storage in a `JsonArray` (or a single element to be stored in a `JsonArray`)
|===============
TIP: Note that the `reduce function` (not always a count) will be activated by prefixing with `count` and that <<repositories.limit-query-result>> is also supported.
Note that both `reduce functions` and <<repositories.limit-query-result>> are also supported.
TIP: In order to trigger a `reduce`, you can use the `count` prefix instead of `find`. But sometimes is doesn't make much sense (eg. because you actually use the `_stats` built in function, which returns a JSON object). So alternatively you can also explicitly ask for reduce to be executed by setting `reduce = true` in the `@View` annotation. Be sure to specify a return type that make sense for the reduce function of your view.
WARNING: Compound keys are not supported, and neither are Or composition, Ignore Case and Order By. You have to include a valid entity property in the naming of your method.

View File

@@ -52,4 +52,6 @@ public @interface View {
*/
String viewName() default "";
boolean reduce() default false;
}

View File

@@ -68,13 +68,14 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
protected Object guessViewAndExecute() {
String designDoc = designDocName(method);
String methodName = method.getName();
boolean isReduce = methodName.startsWith("count");
boolean isExplicitReduce = method.hasViewAnnotation() && method.getViewAnnotation().reduce();
boolean isReduce = methodName.startsWith("count") || isExplicitReduce;
String viewName = StringUtils.uncapitalize(methodName.replaceFirst("find|count", ""));
ViewQuery simpleQuery = ViewQuery.from(designDoc, viewName)
.stale(operations.getDefaultConsistency().viewConsistency());
if (isReduce) {
simpleQuery.reduce(isReduce);
simpleQuery.reduce();
return executeReduce(simpleQuery, designDoc, viewName);
} else {
return execute(simpleQuery);
@@ -94,15 +95,14 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
//use a ViewQueryCreator to complete the base query
ViewQueryCreator creator = new ViewQueryCreator(tree, new ParametersParameterAccessor(method.getParameters(), runtimeParams),
baseQuery, operations.getConverter());
ViewQuery query = creator.createQuery();
method.getViewAnnotation(), baseQuery, operations.getConverter());
ViewQueryCreator.DerivedViewQuery result = creator.createQuery();
//detect count prefix in the method name and consider it triggers a reduce
if (tree.isCountProjection() == Boolean.TRUE) {
return executeReduce(query, designDoc, viewName);
if (result.isReduce) {
return executeReduce(result.builtQuery, designDoc, viewName);
} else {
//otherwise just execute the query
return execute(query);
return execute(result.builtQuery);
}
} catch (PropertyReferenceException e) {
/*

View File

@@ -30,6 +30,7 @@ import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.view.ViewQuery;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.view.View;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -58,18 +59,21 @@ import org.springframework.data.repository.query.parser.PartTree;
* </ul>
* </p>
* Additionally, {@link PartTree#isLimiting()} will use {@link ViewQuery#limit(int) limit}
* and {@link PartTree#isCountProjection()} will trigger a {@link ViewQuery#reduce() reduce}.
* and either {@link View#reduce()} or {@link PartTree#isCountProjection()} will trigger a {@link ViewQuery#reduce() reduce}.
*/
public class ViewQueryCreator extends AbstractQueryCreator<ViewQuery, ViewQuery> {
public class ViewQueryCreator extends AbstractQueryCreator<ViewQueryCreator.DerivedViewQuery, ViewQuery> {
private ViewQuery query;
private final View viewAnnotation;
private final PartTree tree;
private final int treeCount;
private final CouchbaseConverter converter;
public ViewQueryCreator(PartTree tree, ParameterAccessor parameters, ViewQuery query, CouchbaseConverter converter) {
public ViewQueryCreator(PartTree tree, ParameterAccessor parameters, View viewAnnotation,
ViewQuery query, CouchbaseConverter converter) {
super(tree, parameters);
this.query = query;
this.viewAnnotation = viewAnnotation;
this.tree = tree;
this.converter = converter;
@@ -268,7 +272,7 @@ public class ViewQueryCreator extends AbstractQueryCreator<ViewQuery, ViewQuery>
}
@Override
protected ViewQuery complete(ViewQuery criteria, Sort sort) {
protected DerivedViewQuery complete(ViewQuery criteria, Sort sort) {
boolean descending = false;
if (sort != null) {
@@ -290,8 +294,27 @@ public class ViewQueryCreator extends AbstractQueryCreator<ViewQuery, ViewQuery>
query.limit(tree.getMaxResults());
}
query.reduce(tree.isCountProjection() == Boolean.TRUE);
boolean isCount = tree.isCountProjection() == Boolean.TRUE;
boolean isExplicitReduce = viewAnnotation != null && viewAnnotation.reduce();
if (isCount || isExplicitReduce) {
query.reduce();
}
return query;
return new DerivedViewQuery(query, tree.isLimiting(), isCount || isExplicitReduce);
}
/**
* Wrapper class allowing to see downstream if the built query was built with options like reduce and limit.
*/
protected static class DerivedViewQuery {
public final ViewQuery builtQuery;
public final boolean isLimited;
public final boolean isReduce;
public DerivedViewQuery(ViewQuery builtQuery, boolean isLimited, boolean isReduce) {
this.builtQuery = builtQuery;
this.isLimited = isLimited;
this.isReduce = isReduce;
}
}
}