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.
This commit is contained in:
Simon Baslé
2015-07-15 17:04:15 +02:00
parent 4920acc214
commit 4b453d4246
8 changed files with 545 additions and 48 deletions

View File

@@ -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.
* <p/>
* 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.
* <p/>
* 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
*/

View File

@@ -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());
}
/**

View File

@@ -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<ViewRow> 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");
}
}
}

View File

@@ -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}.
* <p>
* 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).
* <br/>
* Here are the {@link Part.Type} supported:
* <ul>
* <li><b>GREATER_THAN_EQUAL:</b> uses {@link ViewQuery#startKey(String) startkey}</li>
* <li><b>LESS_THAN_EQUAL:</b> uses {@link ViewQuery#endKey(String) endkey} {@link ViewQuery#inclusiveEnd() inclusive}</li>
* <li><b>LESS_THAN / BEFORE:</b> uses {@link ViewQuery#endKey(String) endkey}</li>
* <li><b>BETWEEN:</b> uses {@link ViewQuery#startKey(String) startkey},
* {@link ViewQuery#endKey(String) endkey(exclusive)}</li>
* <li><b>STARTING_WITH:</b> (only with String key). uses {@link ViewQuery#startKey(String) startkey},
* {@link ViewQuery#endKey(String) endkey(exclusive)}. Will append special unicode char <code>\uefff</code>.</li>
* <li><b>SIMPLE_PROPERTY:</b> (aka "Is", "Equals"). This one can have no argument if used alone
* (eg. "findAllByUsername"), otherwise uses {@link ViewQuery#key(String) key}</li>
* <li><b>IN:</b> uses {@link ViewQuery#keys(JsonArray) keys} (provide a collection or array)</li>
* </ul>
* </p>
* 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<ViewQuery, ViewQuery> {
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<String> properties = new HashSet<String>();
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<Object> iterator) {
switch (part.getType()) {
case GREATER_THAN_EQUAL:
startKey(iterator);
break;
case LESS_THAN_EQUAL:
query.inclusiveEnd(true);
case BEFORE:
case LESS_THAN://fall-through on purpose here
endKey(iterator);
break;
case BETWEEN:
startKey(iterator);
endKey(iterator);
break;
case STARTING_WITH: //starting_with only supports String keys
String nameStart = nextString(iterator);
query.startKey(nameStart).endKey(nameStart + "\uefff");
query.inclusiveEnd(false);
break;
case SIMPLE_PROPERTY:
key(iterator);
break;
case IN:
query.keys(in(iterator));
break;
default:
throw new IllegalArgumentException("Unsupported keyword in View query derivation: " + part.toString());
}
return query;
}
private void startKey(Iterator<Object> iterator) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for startKey");
}
Object next = iterator.next();
if (next instanceof String) {
query.startKey((String) next);
} else if (next instanceof Boolean) {
query.startKey((Boolean) next);
} else if (next instanceof Double) {
query.startKey((Double) next);
} else if (next instanceof Integer) {
query.startKey((Integer) next);
} else if (next instanceof Long) {
query.startKey((Long) next);
} else if (next instanceof Collection) {
//when creating a JsonArray, the from(List) method is preferred because it will convert internal
//Lists and Maps to JsonObject and JsonArray respectively
List<Object> arrayContent = new ArrayList<Object>((Collection) next);
query.startKey(JsonArray.from(arrayContent));
} else if (next.getClass().isArray()) {
List<Object> arrayContent = Arrays.asList((Object[]) next);
query.startKey(JsonArray.from(arrayContent));
} else if (next instanceof JsonArray) { //discouraged, since it's leaking store-specifics in the method signature
query.startKey((JsonArray) next);
} else if (next instanceof JsonObject) { //discouraged, since it's leaking store-specifics in the method signature
query.startKey((JsonObject) next);
} else {
throw new IllegalArgumentException("Unsupported parameter type for startKey: " + next.getClass());
}
}
private void endKey(Iterator<Object> iterator) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for endKey");
}
Object next = iterator.next();
if (next instanceof String) {
query.endKey((String) next);
} else if (next instanceof Boolean) {
query.endKey((Boolean) next);
} else if (next instanceof Double) {
query.endKey((Double) next);
} else if (next instanceof Integer) {
query.endKey((Integer) next);
} else if (next instanceof Long) {
query.endKey((Long) next);
} else if (next instanceof Collection) {
//when creating a JsonArray, the from(List) method is preferred because it will convert internal
//Lists and Maps to JsonObject and JsonArray respectively
List<Object> arrayContent = new ArrayList<Object>((Collection) next);
query.endKey(JsonArray.from(arrayContent));
} else if (next.getClass().isArray()) {
List<Object> arrayContent = Arrays.asList((Object[]) next);
query.endKey(JsonArray.from(arrayContent));
} else if (next instanceof JsonArray) { //discouraged, since it's leaking store-specifics in the method signature
query.endKey((JsonArray) next);
} else if (next instanceof JsonObject) { //discouraged, since it's leaking store-specifics in the method signature
query.endKey((JsonObject) next);
} else {
throw new IllegalArgumentException("Unsupported parameter type for endKey: " + next.getClass());
}
}
private void key(Iterator<Object> iterator) {
if (!iterator.hasNext() && treeCount > 1) {
throw new IllegalArgumentException("Not enough parameters for key");
} else if (!iterator.hasNext()) {
//probably pattern like findAllByUsername(), just apply query without parameters
return;
}
Object next = iterator.next();
if (next instanceof String) {
query.key((String) next);
} else if (next instanceof Boolean) {
query.key((Boolean) next);
} else if (next instanceof Double) {
query.key((Double) next);
} else if (next instanceof Integer) {
query.key((Integer) next);
} else if (next instanceof Long) {
query.key((Long) next);
} else if (next instanceof Collection) {
//when creating a JsonArray, the from(List) method is preferred because it will convert internal
//Lists and Maps to JsonObject and JsonArray respectively
List<Object> arrayContent = new ArrayList<Object>((Collection) next);
query.key(JsonArray.from(arrayContent));
} else if (next.getClass().isArray()) {
List<Object> arrayContent = Arrays.asList((Object[]) next);
query.key(JsonArray.from(arrayContent));
} else if (next instanceof JsonArray) { //discouraged, since it's leaking store-specifics in the method signature
query.key((JsonArray) next);
} else if (next instanceof JsonObject) { //discouraged, since it's leaking store-specifics in the method signature
query.key((JsonObject) next);
} else {
throw new IllegalArgumentException("Unsupported parameter type for key: " + next.getClass());
}
}
private String nextString(Iterator<Object> iterator) {
if (!iterator.hasNext()) {
throw new IllegalStateException("Not enough parameters");
}
Object next = iterator.next();
if (!(next instanceof String)) {
throw new IllegalArgumentException("Expected String, got " + next.getClass().getName());
}
return (String) next;
}
private JsonArray in(Iterator<Object> iterator) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for in");
}
Object next = iterator.next();
List<Object> values;
if (next instanceof Collection) {
values = new ArrayList<Object>((Collection<?>) next);
} else if (next.getClass().isArray()) {
values = Arrays.asList((Object[]) next);
} else {
values = Collections.singletonList(next);
}
//using the from(List) method ensure that contained Lists and Maps will be converted to JsonArrays and JsonObjects
return JsonArray.from(values);
}
@Override
protected ViewQuery and(Part part, ViewQuery base, Iterator<Object> iterator) {
return create(part, iterator);//and not really supported, all query derivation mutate the original ViewQuery
}
@Override
protected ViewQuery or(ViewQuery base, ViewQuery criteria) {
//this won't be called unless there's a Or keyword in the method
throw new UnsupportedOperationException("Or is not supported for View-based queries");
}
@Override
protected ViewQuery complete(ViewQuery criteria, Sort sort) {
boolean descending = false;
if (sort != null) {
int sortCount = 0;
Iterator<Sort.Order> it = sort.iterator();
while(it.hasNext()) {
sortCount++;
if (!it.next().isAscending()) {
descending = true;
}
}
if (sortCount > 1) {
throw new IllegalArgumentException("Detected " + sortCount + " sort instructions, maximum one supported");
}
query.descending(descending);
}
if (tree.isLimiting()) {
query.limit(tree.getMaxResults());
}
query.reduce(tree.isCountProjection() == Boolean.TRUE);
return query;
}
}