From 4b453d424639ec7f9996406ce111baa84fbd16dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simon=20Basl=C3=A9?=
Date: Wed, 15 Jul 2015 17:04:15 +0200
Subject: [PATCH] DATACOUCH-139 - Add limited support for @View query
derivation.
@View can now trigger a basic query derivation. If viewName's explicitly
set, query derivation is attempted. Otherwise, no derivation is made but
instead the viewName is guessed by using the method name and removing a
"find" or "count" prefix.
Methods prefixed with "count" (and in general detected as count
projections) will trigger a reduce on the view.
---
.../CouchbaseRepositoryViewListener.java | 8 +-
.../CouchbaseRepositoryViewTests.java | 90 +++++-
.../repository/CustomUserRepository.java | 44 ++-
.../data/couchbase/repository/User.java | 20 +-
.../data/couchbase/core/view/View.java | 13 +-
.../query/CouchbaseQueryMethod.java | 26 +-
.../query/ViewBasedCouchbaseQuery.java | 100 ++++--
.../repository/query/ViewQueryCreator.java | 292 ++++++++++++++++++
8 files changed, 545 insertions(+), 48 deletions(-)
create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java
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 13420e40..a99e8709 100644
--- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java
+++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -16,6 +16,7 @@
package org.springframework.data.couchbase.repository;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -32,6 +33,7 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
/**
* @author Michael Nitschinger
+ * @author Simon Baslé
*/
public class CouchbaseRepositoryViewListener extends DependencyInjectionTestExecutionListener {
@@ -51,8 +53,10 @@ 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); } }";
View view = DefaultView.create("customFindAllView", mapFunction, "_count");
- List views = Collections.singletonList(view);
+ View customFindByNameView = DefaultView.create("customFindByNameView", mapFunctionName, "_count");
+ List views = Arrays.asList(view, customFindByNameView);
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 ef603cdb..d4317f54 100644
--- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java
+++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -18,8 +18,10 @@ package org.springframework.data.couchbase.repository;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.view.Stale;
@@ -33,12 +35,14 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.data.couchbase.IntegrationTestApplicationConfig;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory;
+import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author David Harrigan
+ * @author Simon Baslé
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = IntegrationTestApplicationConfig.class)
@@ -77,13 +81,89 @@ public class CouchbaseRepositoryViewTests {
}
@Test
- public void shouldTrimOffFindOnCustomFinder() {
+ public void shouldDetectMethodNameWithoutPropertyAndIssueGenericQueryOnView() {
+ Iterable users = repository.findRandomMethodName();
+ assertNotNull(users);
+ assertTrue(users.iterator().hasNext());
+
try {
- repository.findAllSomething();
+ repository.findIncorrectExplicitView();
fail("Expected InvalidDataAccessResourceException");
} catch (InvalidDataAccessResourceUsageException e) {
assertTrue(e.getMessage(), e.getMessage().startsWith("View user/allSomething does not exist"));
}
}
+ @Test(expected = PropertyReferenceException.class)
+ public void shouldFailDeriveOnBadProperty() {
+ repository.findAllByUsernameEqualAndUserblablaIs("uname-1", "blabla");
+ }
+
+ @Test
+ public void shouldDeriveViewParametersAndReduce() {
+ long count = repository.countByUsernameGreaterThanEqualAndUsernameLessThan("uname-8", "uname-9");
+ assertEquals(12, count);
+ }
+
+ @Test
+ public void shouldDeriveViewParameters() {
+ String lowKey = "uname-1";
+ String middleKey = "uname-10";
+ String highKey = "uname-11";
+ List keys = Arrays.asList(lowKey, middleKey, highKey);
+
+ User u1 = repository.findByUsernameIs(lowKey);
+ User u2 = repository.findByUsernameIs(middleKey);
+ User u3 = repository.findByUsernameIs(highKey);
+
+
+ List in = repository.findAllByUsernameIn(keys);
+ List gteLte = repository.findByUsernameGreaterThanEqualAndUsernameLessThanEqual(lowKey, highKey);
+ List between = repository.findByUsernameBetween(lowKey, highKey);
+ List gteLimited = repository.findTop3ByUsernameGreaterThanEqual(lowKey);
+
+ assertNotNull(u1);
+ assertNotNull(u2);
+ assertNotNull(u3);
+
+ assertEquals(lowKey, u1.getUsername());
+ assertEquals(middleKey, u2.getUsername());
+ assertEquals(highKey, u3.getUsername());
+
+ List expected = Arrays.asList(u1, u2, u3);
+ assertEquals(expected, in);
+ assertEquals(expected, gteLte);
+ assertEquals(expected, between);
+ assertEquals(expected, gteLimited);
+ }
+
+ @Test
+ public void shouldDeriveToEmptyClause() {
+ List users = repository.findAllByUsername();
+ assertNotNull(users);
+ assertEquals(100, users.size());
+ }
+
+ @Test
+ public void shouldDetermineViewNameFromMethodPrefix() {
+ try {
+ repository.findByIncorrectView();
+ fail("Expected InvalidDataAccessResourceException");
+ } catch (InvalidDataAccessResourceUsageException e) {
+ assertTrue(e.getMessage(), e.getMessage().startsWith("View user/byIncorrectView does not exist"));
+ }
+ }
+
+ @Test
+ public void shouldDetermineViewNameFromCountPrefixAndReduce() {
+ long count = repository.countCustomFindAllView();
+ assertEquals(100, count);
+
+ try {
+ repository.countCustomFindInvalid();
+ fail("Expected InvalidDataAccessResourceException");
+ } catch (InvalidDataAccessResourceUsageException e) {
+ assertTrue(e.getMessage(), e.getMessage().startsWith("View user/customFindInvalid does not exist"));
+ }
+ }
}
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 cad54dc4..cd334a21 100644
--- a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java
+++ b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -16,10 +16,13 @@
package org.springframework.data.couchbase.repository;
+import java.util.List;
+
import org.springframework.data.couchbase.core.view.View;
/**
* @author David Harrigan
+ * @author Simon Baslé
*/
public interface CustomUserRepository extends CouchbaseRepository {
@@ -31,7 +34,42 @@ public interface CustomUserRepository extends CouchbaseRepository
@View(designDocument = "userCustom", viewName = "customCountView")
long count();
- @View
- Iterable findAllSomething();
+ @View(viewName = "allSomething")
+ Iterable findIncorrectExplicitView();
+ @View(viewName = "customFindAllView")
+ Iterable findRandomMethodName();
+
+ @View(viewName = "customFindByNameView")
+ long countByUsernameGreaterThanEqualAndUsernameLessThan(String lowBound, String highBound);
+
+ @View(viewName = "customFindByNameView")
+ User findByUsernameIs(String lowKey);
+
+ @View(viewName = "customFindByNameView")
+ List findAllByUsernameIn(List keys);
+
+ @View(viewName = "customFindByNameView")
+ List findByUsernameGreaterThanEqualAndUsernameLessThanEqual(String lowKey, String highKey);
+
+ @View(viewName = "customFindByNameView")
+ List findByUsernameBetween(String lowKey, String highKey);
+
+ @View(viewName = "customFindByNameView")
+ List findTop3ByUsernameGreaterThanEqual(String lowKey);
+
+ @View(viewName = "customFindAllView")
+ List findAllByUsername();
+
+ @View(viewName = "customFindAllView")
+ List findAllByUsernameEqualAndUserblablaIs(String s, String blabla);
+
+ @View
+ List findByIncorrectView();
+
+ @View
+ long countCustomFindAllView();
+
+ @View
+ long countCustomFindInvalid();
}
diff --git a/src/integration/java/org/springframework/data/couchbase/repository/User.java b/src/integration/java/org/springframework/data/couchbase/repository/User.java
index c5e1c257..adbfef81 100644
--- a/src/integration/java/org/springframework/data/couchbase/repository/User.java
+++ b/src/integration/java/org/springframework/data/couchbase/repository/User.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -20,6 +20,7 @@ import org.springframework.data.annotation.Id;
/**
* @author Michael Nitschinger
+ * @author Simon Baslé
*/
public class User {
@@ -41,4 +42,21 @@ public class User {
return key;
}
+ @Override
+ public String toString() {
+ return this.key;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ User user = (User) o;
+ return key.equals(user.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
}
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 1767a523..87f12ea8 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -26,6 +26,7 @@ import java.lang.annotation.Target;
* Annotation to support the use of Views with Couchbase.
*
* @author David Harrigan.
+ * @author Simon Baslé
*/
@Documented
@Target(ElementType.METHOD)
@@ -33,18 +34,16 @@ import java.lang.annotation.Target;
public @interface View {
/**
- * The name of the Design Document to use.
- *
- * If the {@link #viewName()} field is set, this field is mandatory.
+ * The name of the Design Document to use. If omitted, defaults to one derived from the entity class name.
*
* @return name of the Design Document.
*/
String designDocument() default "";
/**
- * The name of the View to use.
- *
- * If the {@link #designDocument()} field is set, his field is mandatory.
+ * The name of the View to use. If omitted, defaults to one derived from the method name (stripped of prefix "find" or
+ * "count"). This is mandatory to trigger a query derivation from the method name (ie. a View query with parameters
+ * like limit, startkey, etc...).
*
* @return name of the View
*/
diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java
index cb645e99..0661e7d6 100644
--- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java
+++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013, 2014 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -63,11 +63,33 @@ public class CouchbaseQueryMethod extends QueryMethod {
* @return true if it has the annotation and full view specified.
*/
public boolean hasViewSpecification() {
+ return hasDesignDoc() && hasViewName();
+ }
+
+ /**
+ * If the method has a @View annotation with the designDocument specified.
+ *
+ * @return true if it has the design document specified.
+ */
+ public boolean hasDesignDoc() {
View annotation = getViewAnnotation();
if (annotation == null) {
return false;
}
- return StringUtils.hasText(annotation.designDocument()) && StringUtils.hasText(annotation.viewName());
+ return StringUtils.hasText(annotation.designDocument());
+ }
+
+ /**
+ * If the method has a @View annotation with the viewName specified.
+ *
+ * @return true if it has the view name specified.
+ */
+ public boolean hasViewName() {
+ View annotation = getViewAnnotation();
+ if (annotation == null) {
+ return false;
+ }
+ return StringUtils.hasText(annotation.viewName());
}
/**
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 df1cd860..67d60de4 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013, 2014 the original author or authors.
+ * Copyright 2013-2015 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.
@@ -16,17 +16,27 @@
package org.springframework.data.couchbase.repository.query;
+import java.util.List;
+
+import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.view.ViewQuery;
+import com.couchbase.client.java.view.ViewResult;
+import com.couchbase.client.java.view.ViewRow;
import org.springframework.data.couchbase.core.CouchbaseOperations;
+import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.StringUtils;
/**
* Execute a repository query through the View mechanism.
*
* @author Michael Nitschinger
+ * @author Simon Baslé
*/
public class ViewBasedCouchbaseQuery implements RepositoryQuery {
@@ -40,24 +50,73 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
@Override
public Object execute(Object[] runtimeParams) {
- ViewQuery query = null;
- for (Object param : runtimeParams) {
- if (param instanceof ViewQuery) {
- query = (ViewQuery) param;
- //FIXME clone the ViewQuery and use the @View design / viewname
+ if (method.hasViewName()) { //only allow derivation on @View explicitly defining a viewName
+ return deriveAndExecute(runtimeParams);
+ } else {
+ return guessViewAndExecute();
+ }
+ }
+
+ protected Object guessViewAndExecute() {
+ String designDoc = designDocName(method);
+ String methodName = method.getName();
+ boolean isReduce = methodName.startsWith("count");
+ String viewName = StringUtils.uncapitalize(methodName.replaceFirst("find|count", ""));
+
+ ViewQuery simpleQuery = ViewQuery.from(designDoc, viewName);
+ if (isReduce) {
+ simpleQuery.reduce(isReduce);
+ return executeReduce(simpleQuery, designDoc, viewName);
+ } else {
+ return execute(simpleQuery);
+ }
+ }
+
+ protected Object deriveAndExecute(Object[] runtimeParams) {
+ String designDoc = designDocName(method);
+ String viewName = method.getViewAnnotation().viewName();
+ ViewQuery baseQuery = ViewQuery.from(designDoc, viewName);
+ try {
+ PartTree tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
+
+ ViewQueryCreator creator = new ViewQueryCreator(tree,
+ new ParametersParameterAccessor(method.getParameters(), runtimeParams),
+ baseQuery);
+
+ ViewQuery query = creator.createQuery();
+
+ if (tree.isCountProjection() == Boolean.TRUE) {
+ return executeReduce(query, designDoc, viewName);
} else {
- throw new IllegalStateException("Unknown query param: " + param);
+ return execute(query);
}
+ } catch (PropertyReferenceException e) {
+ if (e.getPropertyName().equals(method.getName())) {
+ return execute(baseQuery);
+ }
+ throw e;
}
+ }
- if (query == null) {
- query = ViewQuery.from(designDocName(), viewName());
- }
- query.reduce(false);
-
+ protected Object execute(ViewQuery query) {
return operations.findByView(query, method.getEntityInformation().getJavaType());
}
+ protected Object executeReduce(ViewQuery query, String designDoc, String viewName) {
+ ViewResult viewResult = operations.queryView(query);
+ List allRows = viewResult.allRows();
+ JsonObject error = viewResult.error();
+ if (error != null) {
+ throw new CouchbaseQueryExecutionException("Error while reducing on view " + designDoc + "/" + viewName +
+ ": " + error);
+ }
+ if (allRows == null || allRows.isEmpty()) {
+ return null;
+ } else{
+ return allRows.get(0).value();
+ }
+ }
+
@Override
public QueryMethod getQueryMethod() {
return method;
@@ -68,7 +127,7 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
*
* @return the design document name.
*/
- private String designDocName() {
+ private static String designDocName(CouchbaseQueryMethod method) {
if (method.hasViewSpecification()) {
return method.getViewAnnotation().designDocument();
} else if (method.hasViewAnnotation()) {
@@ -78,19 +137,4 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
}
}
- /**
- * Returns the best-guess view name.
- *
- * @return the view name.
- */
- private String viewName() {
- if (method.hasViewSpecification()) {
- return method.getViewAnnotation().viewName();
- } else if (method.hasViewAnnotation()) {
- return StringUtils.uncapitalize(method.getName().replaceFirst("find", ""));
- } else {
- throw new IllegalStateException("View-based query should only happen on a method with @View annotation");
- }
- }
-
}
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
new file mode 100644
index 00000000..23b737a7
--- /dev/null
+++ b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2012-2015 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
+ *
+ * http://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.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import com.couchbase.client.java.document.json.JsonArray;
+import com.couchbase.client.java.document.json.JsonObject;
+import com.couchbase.client.java.view.ViewQuery;
+
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.ParameterAccessor;
+import org.springframework.data.repository.query.parser.AbstractQueryCreator;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * A QueryCreator that will enrich a {@link ViewQuery} using query derivation mechanisms
+ * and the parsed {@link PartTree}.
+ *
+ * Support for query derivation keywords is very limited (and especially you have to use one valid entity property name
+ * in your query naming, and compound key views are not supported).
+ *
+ * Here are the {@link Part.Type} supported:
+ *
STARTING_WITH: (only with String key). uses {@link ViewQuery#startKey(String) startkey},
+ * {@link ViewQuery#endKey(String) endkey(exclusive)}. Will append special unicode char \uefff.
+ *
SIMPLE_PROPERTY: (aka "Is", "Equals"). This one can have no argument if used alone
+ * (eg. "findAllByUsername"), otherwise uses {@link ViewQuery#key(String) key}
+ *
IN: uses {@link ViewQuery#keys(JsonArray) keys} (provide a collection or array)
+ *
+ *
+ * Additionally, {@link PartTree#isLimiting()} will use {@link ViewQuery#limit(int) limit}
+ * and {@link PartTree#isCountProjection()} will trigger a {@link ViewQuery#reduce() reduce}.
+ */
+public class ViewQueryCreator extends AbstractQueryCreator {
+
+ private ViewQuery query;
+ private final PartTree tree;
+ private final int treeCount;
+
+ public ViewQueryCreator(PartTree tree, ParameterAccessor parameters, ViewQuery query) {
+ super(tree, parameters);
+ this.query = query;
+ this.tree = tree;
+
+ //sanity check the partTree since we have strong restrictions on what's supported:
+ int i = 0;
+ Set properties = new HashSet();
+ for (PartTree.OrPart parts : tree) {
+ for (Part part : parts) {
+ i++;
+ properties.add(part.getProperty().toDotPath());
+ }
+ }
+ this.treeCount = i;
+ if (properties.size() > 1) {
+ throw new IllegalArgumentException("View-based queries do not support compound keys");
+ }
+ }
+
+ @Override
+ protected ViewQuery create(Part part, Iterator