DATACOUCH-136 - Add @N1QL query annotation.

The annotation allows to execute N1QL queries that are provided inline. For ease of use, one can use the placeholder $SELECT_ENTITY$ instead of writing the SELECT and FROM clauses.

QueryParams passed in as a method parameter will be used to enrich the Query. Other parameters on the annotated method will be used as values for positional placeholders in the statement.

The template's findByN1QL now expect enough data to reconstruct the full CouchbaseDocument (including ID and CAS), the ad hoc version of it has been renamed findByN1QLProjection.
This commit is contained in:
Simon Baslé
2015-07-06 16:17:39 +02:00
parent b9f23f3fd5
commit 9deaf6b480
14 changed files with 637 additions and 18 deletions

View File

@@ -31,6 +31,7 @@ import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
/**
* Defines common operations on the Couchbase data source, most commonly implemented by {@link CouchbaseTemplate}.
@@ -40,6 +41,9 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
*/
public interface CouchbaseOperations {
String SELECT_ID = "_ID";
String SELECT_CAS = "_CAS";
/**
* Save the given object.
* <p/>
@@ -210,18 +214,42 @@ public interface CouchbaseOperations {
ViewResult queryView(ViewQuery query);
/**
* Query the N1QL Service for JSON data of type T. This is done via a {@link Query} that can
* contain a {@link Statement}, additional query parameters ({@link QueryParams})
* and placeholder values if the statement contains placeholders.
* <p>Use {@link Query}'s factory methods to construct this.</p>
* Query the N1QL Service for JSON data of type T. Enough data to construct the full
* entity is expected to be selected, including the metadata {@value #SELECT_ID} and
* {@value #SELECT_CAS} (document id and cas, obtained through N1QL's
* "{@code META(bucket).id AS} {@value #SELECT_ID}" and
* "{@code META(bucket).cas AS} {@value #SELECT_CAS}").
* <p>This is done via a {@link Query} that contains a {@link Statement} and possibly
* additional query parameters ({@link QueryParams}) and placeholder values if the
* statement contains placeholders.
* <br/>
* Use {@link Query}'s factory methods to construct such a Query.</p>
*
* @param n1ql the N1QL query.
* @param entityClass the target class for the returned entities.
* @param <T> the entity class
* @return the list of entities matching this query.
* @throws CouchbaseQueryExecutionException if the id and cas are not selected.
*/
<T> List<T> findByN1QL(Query n1ql, Class<T> entityClass);
/**
* Query the N1QL Service for partial JSON data of type T. The selected field will be
* used in a {@link TranslationService#decodeFragment(String, Class) straightforward decoding}
* (no document, metadata like id nor cas) to map to a "fragment class".
* <p>This is done via a {@link Query} that contains a {@link Statement} and possibly
* additional query parameters ({@link QueryParams}) and placeholder values if the
* statement contains placeholders.
* <br/>
* Use {@link Query}'s factory methods to construct such a Query.</p>
*
* @param n1ql the N1QL query.
* @param fragmentClass the target class for the returned fragments.
* @param <T> the fragment class
* @return the list of entities matching this query.
*/
<T> List<T> findByN1QLProjection(Query n1ql, Class<T> fragmentClass);
/**
* Query the N1QL Service with direct access to the {@link QueryResult}.
* <p>

View File

@@ -322,8 +322,44 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
List<QueryRow> allRows = queryResult.allRows();
List<T> result = new ArrayList<T>(allRows.size());
for (QueryRow row : allRows) {
String json = row.value().toString();
T decoded = translationService.decodeFragment(json, entityClass);
JsonObject json = row.value();
String id = json.getString(SELECT_ID);
Long cas = json.getLong(SELECT_CAS);
if (id == null || cas == null) {
throw new CouchbaseQueryExecutionException("Unable to retrieve enough metadata for N1QL to entity mapping, " +
"have you selected " + SELECT_ID + " and " + SELECT_CAS + "?");
}
json = json.removeKey("_ID").removeKey("_CAS");
RawJsonDocument entityDoc = RawJsonDocument.create(id, json.toString(), cas);
T decoded = mapToEntity(id, entityDoc, entityClass);
result.add(decoded);
}
return result;
}
else {
StringBuilder message = new StringBuilder("Unable to execute query due to the following n1ql errors: ");
for (JsonObject error : queryResult.errors()) {
message.append('\n').append(error);
}
throw new CouchbaseQueryExecutionException(message.toString());
}
}
catch (TranscodingException e) {
throw new CouchbaseQueryExecutionException("Unable to execute query", e);
}
}
@Override
public <T> List<T> findByN1QLProjection(Query n1ql, Class<T> entityClass) {
try {
QueryResult queryResult = queryN1QL(n1ql);
if (queryResult.finalSuccess()) {
List<QueryRow> allRows = queryResult.allRows();
List<T> result = new ArrayList<T>(allRows.size());
for (QueryRow row : allRows) {
JsonObject json = row.value();
T decoded = translationService.decodeFragment(json.toString(), entityClass);
result.add(decoded);
}
return result;

View File

@@ -0,0 +1,53 @@
/*
* 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.core.view;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.QueryAnnotation;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
/**
* Annotation to support the use of N1QL queries with Couchbase.
*
* Using it without parameter will resolve the query from the method name. Providing a value
* (an inline N1QL statement) will execute that statement instead.
*
* In this case, one can use a placeholder notation of {@code ?0}, {@code ?1} and so on.
*
* Also, the placeholder {@code $BUCKET$} can be used to be replaced by the underlying {@link CouchbaseTemplate}
* associated bucket name.
*
* @author Simon Baslé.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@QueryAnnotation
public @interface N1QL {
/**
* Takes a N1QL statement string to define the actual query to be executed. This one will take precedence over the
* method name then.
*/
String value() default "";
}

View File

@@ -0,0 +1,126 @@
/*
* 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.Iterator;
import java.util.List;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.query.Query;
import com.couchbase.client.java.query.QueryParams;
import com.couchbase.client.java.query.QueryPlan;
import com.couchbase.client.java.query.Statement;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.query.ParameterAccessor;
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.util.StreamUtils;
/**
* Abstract base for all Couchbase {@link RepositoryQuery}. It is in charge of inspecting the parameters
* and choosing the correct {@link Query} implementation to use.
*/
public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
private final CouchbaseQueryMethod queryMethod;
private final CouchbaseOperations couchbaseOperations;
protected AbstractN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
this.queryMethod = queryMethod;
this.couchbaseOperations = couchbaseOperations;
}
protected abstract Statement getStatement();
@Override
public Object execute(Object[] parameters) {
Statement statement = getStatement();
ParameterAccessor parameterAccessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
Query query = buildQuery(statement, parameterAccessor.iterator());
return executeDependingOnType(query, queryMethod, queryMethod.isPageQuery(), queryMethod.isModifyingQuery(),
queryMethod.isSliceQuery());
}
protected static Query buildQuery(Statement statement, Iterator<Object> paramIterator) {
JsonArray queryValues = JsonArray.create();
QueryParams queryParams = null;
QueryPlan preparedPayload = null;
while (paramIterator.hasNext()) {
Object next = paramIterator.next();
if (next instanceof QueryParams) {
queryParams = (QueryParams) next;
}
else if (next instanceof QueryPlan) {
preparedPayload = (QueryPlan) next;
}
else {
queryValues.add(next);
}
}
Query query;
if (preparedPayload != null) {
query = Query.prepared(preparedPayload, queryValues.isEmpty() ? null : queryValues, queryParams);
}
else if (!queryValues.isEmpty()) {
query = Query.parametrized(statement, queryValues, queryParams);
}
else {
query = Query.simple(statement, queryParams);
}
return query;
}
protected Object executeDependingOnType(Query query, QueryMethod queryMethod,
boolean isPage, boolean isSlice, boolean isModifying) {
if (isPage || isSlice || isModifying) {
throw new UnsupportedOperationException("Slice, page and modifying queries not yet supported");
}
if (queryMethod.isCollectionQuery()) {
return executeCollection(query);
} else if (queryMethod.isQueryForEntity()) {
return executeEntity(query);
} else {
return executeStream(query);
}
}
protected List<?> executeCollection(Query query) {
List<?> result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType());
return result;
}
protected Object executeEntity(Query query) {
List<?> result = executeCollection(query);
return result.isEmpty() ? null : result.get(0);
}
protected Object executeStream(Query query) {
return StreamUtils.createStreamFromIterator(executeCollection(query).iterator());
}
@Override
public CouchbaseQueryMethod getQueryMethod() {
return this.queryMethod;
}
}

View File

@@ -16,19 +16,24 @@
package org.springframework.data.couchbase.repository.query;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.view.N1QL;
import org.springframework.data.couchbase.core.view.View;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
/**
* Represents a query method with couchbase extensions.
* Represents a query method with couchbase extensions, allowing to discover
* if View-based query or N1QL-based query must be used.
*
* @author Michael Nitschinger
* @author Simon Baslé
*/
public class CouchbaseQueryMethod extends QueryMethod {
@@ -53,7 +58,7 @@ public class CouchbaseQueryMethod extends QueryMethod {
}
/**
* Returns the @View annoation if set, null otherwise.
* Returns the @View annotation if set, null otherwise.
*
* @return the view annotation of present.
*/
@@ -61,6 +66,41 @@ public class CouchbaseQueryMethod extends QueryMethod {
return method.getAnnotation(View.class);
}
/**
* If the method has a @N1QL annotation.
*
* @return true if it has the annotation, false otherwise.
*/
public boolean hasN1qlAnnotation() {
return getN1qlAnnotation() != null;
}
/**
* Returns the @N1QL annotation if set, null otherwise.
*
* @return the n1ql annotation if present.
*/
public N1QL getN1qlAnnotation() {
return method.getAnnotation(N1QL.class);
}
/**
* If the method has a @N1QL annotation with an inline N1QL statement inside.
*
* @return true if this has the annotation and N1QL inline statement, false otherwise.
*/
public boolean hasInlineN1qlQuery() {
return getInlineN1qlQuery() != null;
}
/**
* Returns the query string declared in a {@link N1QL} annotation or {@literal null} if neither the annotation found
* nor the attribute was specified.
*
* @return the query statement if present.
*/
public String getInlineN1qlQuery() {
String query = (String) AnnotationUtils.getValue(getN1qlAnnotation());
return StringUtils.hasText(query) ? query : null;
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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 com.couchbase.client.java.query.Query;
import com.couchbase.client.java.query.Statement;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* A {@link RepositoryQuery} for Couchbase, based on N1QL and a String statement.
* The statement can contain placeholders for the {@link #PLACEHOLDER_BUCKET bucket namespace},
* the {@link #PLACEHOLDER_ENTITY ID and CAS fields} necessary for entity reconstruction or
* a shortcut that covers {@link #PLACEHOLDER_SELECT_FROM SELECT AND FROM clauses}.
*
* @author Simon Baslé
*/
public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
public static final String PLACEHOLDER_SELECT_FROM = "$SELECT_ENTITY$";
public static final String PLACEHOLDER_BUCKET = "$BUCKET$";
public static final String PLACEHOLDER_ENTITY = "$ENTITY$";
private final Statement statement;
public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
super(queryMethod, couchbaseOperations);
this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name());
}
protected static Statement prepare(String statement, String bucketName) {
String b = "`" + bucketName + "`";
String entity = "META(" + b + ").id AS " + CouchbaseOperations.SELECT_ID +
", META(" + b + ").cas AS " + CouchbaseOperations.SELECT_CAS;
String selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b;
String result = statement;
if (statement.contains(PLACEHOLDER_SELECT_FROM)) {
result = result.replaceFirst("\\$SELECT_ENTITY\\$", selectEntity);
} else {
if (statement.contains(PLACEHOLDER_BUCKET)) {
result = result.replaceAll("\\$BUCKET\\$", b);
}
if (statement.contains(PLACEHOLDER_ENTITY)) {
result = result.replaceFirst("\\$ENTITY\\$", entity);
}
}
return Query.simple(result).statement();
}
@Override
public Statement getStatement() {
return this.statement;
}
}

View File

@@ -44,6 +44,7 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
for (Object param : runtimeParams) {
if (param instanceof ViewQuery) {
query = (ViewQuery) param;
//FIXME clone the ViewQuery and use the @View design / viewname
} else {
throw new IllegalStateException("Unknown query param: " + param);
}

View File

@@ -16,11 +16,15 @@
package org.springframework.data.couchbase.repository.support;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
import org.springframework.data.couchbase.repository.query.ViewBasedCouchbaseQuery;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.MappingException;
@@ -32,9 +36,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.lang.reflect.Method;
/**
* Factory to create {@link SimpleCouchbaseRepository} instances.
*
@@ -125,12 +126,25 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
}
/**
* Currently, only views are supported. N1QL support to be added.
* Strategy to lookup Couchbase queries implementation to be used.
*/
private class CouchbaseQueryLookupStrategy implements QueryLookupStrategy {
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) {
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, mappingContext);
String namedQueryName = queryMethod.getNamedQueryName();
if (queryMethod.hasN1qlAnnotation()) {
if (queryMethod.hasInlineN1qlQuery()) {
return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations);
} else if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations);
} else {
//FIXME return new PartBasedN1qlQuery(queryMethod, couchbaseOperations, namedQueries);
return new StringN1qlBasedQuery("SELECT 1", queryMethod, couchbaseOperations);
}
}
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
}
}