diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java index 7ad4a4ec..ad67a203 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java @@ -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 views = Arrays.asList(view, customFindByNameView); + View customFindByAgeStatsView = DefaultView.create("customFindByAgeStatsView", mapFunctionAge, "_stats"); + List views = Arrays.asList(view, customFindByNameView, customFindByAgeStatsView); DesignDocument designDoc = DesignDocument.create("user", views); client.bucketManager().upsertDesignDocument(designDoc); diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java index 6e5c5a16..6f18bc15 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java @@ -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"; diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java index cd334a21..a3095818 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java @@ -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 @View long countCustomFindInvalid(); + + @View(viewName = "customFindByAgeStatsView", reduce = true) + JsonObject findByAgeLessThan(int maxAge); } diff --git a/src/main/asciidoc/migrating.adoc b/src/main/asciidoc/migrating.adoc index 851b40f3..3900e04d 100644 --- a/src/main/asciidoc/migrating.adoc +++ b/src/main/asciidoc/migrating.adoc @@ -61,8 +61,15 @@ List 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). \ No newline at end of file diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index bf84b817..6a88f731 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -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 <> is also supported. +Note that both `reduce functions` and <> 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/view/View.java b/src/main/java/org/springframework/data/couchbase/core/view/View.java index ad402b6d..cda37872 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/View.java +++ b/src/main/java/org/springframework/data/couchbase/core/view/View.java @@ -52,4 +52,6 @@ public @interface View { */ String viewName() default ""; + boolean reduce() default false; + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java index 930298e2..514bf7a3 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java @@ -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) { /* diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java index 13cc9187..a3d143dc 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java @@ -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; * *

* 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 { +public class ViewQueryCreator extends AbstractQueryCreator { 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 } @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 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; + } } }