From 9deaf6b4804ab051b286b59565bb125d8fdd0d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 6 Jul 2015 16:17:39 +0200 Subject: [PATCH] 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. --- .../core/CouchbaseTemplateTests.java | 2 +- .../SimpleCouchbaseRepositoryTests.java | 22 ++ .../couchbase/repository/UserRepository.java | 7 + .../couchbase/core/CouchbaseOperations.java | 36 +++- .../couchbase/core/CouchbaseTemplate.java | 40 +++- .../data/couchbase/core/view/N1QL.java | 53 +++++ .../query/AbstractN1qlBasedQuery.java | 126 ++++++++++++ .../query/CouchbaseQueryMethod.java | 44 +++- .../query/StringN1qlBasedQuery.java | 69 +++++++ .../query/ViewBasedCouchbaseQuery.java | 1 + .../support/CouchbaseRepositoryFactory.java | 22 +- .../couchbase/UnitTestApplicationConfig.java | 5 - .../query/AbstractN1qlBasedQueryTest.java | 190 ++++++++++++++++++ .../query/StringN1QlBasedQueryTest.java | 38 ++++ 14 files changed, 637 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/view/N1QL.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java index a7db9bf7..9876ce6d 100644 --- a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java @@ -241,7 +241,7 @@ public class CouchbaseTemplateTests { .where(x("type").eq(s("fullFragment")) .and(x("criteria").gt(1)))); - List fragments = template.findByN1QL(query, Fragment.class); + List fragments = template.findByN1QLProjection(query, Fragment.class); assertNotNull(fragments); assertFalse(fragments.isEmpty()); assertEquals(1, fragments.size()); diff --git a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java index 75676d96..e547bdf6 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java @@ -27,6 +27,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.IntegrationTestApplicationConfig; +import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -111,4 +112,25 @@ public class SimpleCouchbaseRepositoryTests { assertEquals(2, size); } + @Test + public void shouldFindByUsernameUsingN1ql() { + User user = repository.findByUsername("uname-1"); + assertNotNull(user); + assertEquals("testuser-1", user.getKey()); + assertEquals("uname-1", user.getUsername()); + } + + @Test + public void shouldFailFindByUsernameWithNoIdOrCas() { + try { + User user = repository.findByUsernameBadSelect("uname-1"); + fail("shouldFailFindByUsernameWithNoIdOrCas"); + } catch (CouchbaseQueryExecutionException e) { + assertTrue(e.getMessage().contains("_ID")); + assertTrue(e.getMessage().contains("_CAS")); + } catch (Exception e) { + fail("CouchbaseQueryExecutionException expected"); + } + } + } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java index 866a3ea5..f2a39a39 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java @@ -18,6 +18,7 @@ package org.springframework.data.couchbase.repository; import com.couchbase.client.java.view.ViewQuery; +import org.springframework.data.couchbase.core.view.N1QL; import org.springframework.data.couchbase.core.view.View; /** @@ -28,4 +29,10 @@ public interface UserRepository extends CouchbaseRepository { @View(designDocument = "user", viewName = "all") Iterable customViewQuery(ViewQuery query); + @N1QL("$SELECT_ENTITY$ WHERE username = $1") + User findByUsername(String username); + + @N1QL("SELECT * FROM $BUCKET$ WHERE username = $1") + User findByUsernameBadSelect(String username); + } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index 0610c9e0..bc8b19c0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -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. *

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

Use {@link Query}'s factory methods to construct this.

+ * 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}"). + *

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. + *
+ * Use {@link Query}'s factory methods to construct such a Query.

* * @param n1ql the N1QL query. * @param entityClass the target class for the returned entities. * @param the entity class * @return the list of entities matching this query. + * @throws CouchbaseQueryExecutionException if the id and cas are not selected. */ List findByN1QL(Query n1ql, Class 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". + *

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. + *
+ * Use {@link Query}'s factory methods to construct such a Query.

+ * + * @param n1ql the N1QL query. + * @param fragmentClass the target class for the returned fragments. + * @param the fragment class + * @return the list of entities matching this query. + */ + List findByN1QLProjection(Query n1ql, Class fragmentClass); + /** * Query the N1QL Service with direct access to the {@link QueryResult}. *

diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 7f707c05..80382ef1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -322,8 +322,44 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP List allRows = queryResult.allRows(); List result = new ArrayList(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 List findByN1QLProjection(Query n1ql, Class entityClass) { + try { + QueryResult queryResult = queryN1QL(n1ql); + + if (queryResult.finalSuccess()) { + List allRows = queryResult.allRows(); + List result = new ArrayList(allRows.size()); + for (QueryRow row : allRows) { + JsonObject json = row.value(); + T decoded = translationService.decodeFragment(json.toString(), entityClass); result.add(decoded); } return result; diff --git a/src/main/java/org/springframework/data/couchbase/core/view/N1QL.java b/src/main/java/org/springframework/data/couchbase/core/view/N1QL.java new file mode 100644 index 00000000..840ab0a0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/view/N1QL.java @@ -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 ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java new file mode 100644 index 00000000..86e1d990 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -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 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; + } +} 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 5442c663..d9e167dc 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 @@ -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; + } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java new file mode 100644 index 00000000..2bdb49c2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java @@ -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; + } +} 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 d37e8bfc..85fd29fd 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 @@ -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); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index 4371c475..4c69668b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -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); } } diff --git a/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java b/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java index 62be5f77..8a2379a6 100644 --- a/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java +++ b/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java @@ -7,15 +7,10 @@ import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.CouchbaseBucket; import com.couchbase.client.java.CouchbaseCluster; -import com.couchbase.client.java.env.CouchbaseEnvironment; -import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; -import org.mockito.Mock; import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.WriteResultChecking; diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java new file mode 100644 index 00000000..38dc2bca --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java @@ -0,0 +1,190 @@ +package org.springframework.data.couchbase.repository.query; + +import static com.couchbase.client.java.query.Select.select; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.couchbase.client.java.document.json.JsonArray; +import com.couchbase.client.java.document.json.JsonObject; +import com.couchbase.client.java.query.ParametrizedQuery; +import com.couchbase.client.java.query.PreparedQuery; +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.Select; +import com.couchbase.client.java.query.SimpleQuery; +import com.couchbase.client.java.query.Statement; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; + +import org.springframework.data.couchbase.UnitTestApplicationConfig; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.util.ReflectionUtils; + +public class AbstractN1qlBasedQueryTest { + + @Test + public void testQueryPlanShouldProducePreparedQuery() throws Exception { + Statement st = select("*"); + QueryPlan plan = new QueryPlan(JsonObject.create()); + List params = new ArrayList(2); + params.add("test"); + params.add(plan); + + Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof PreparedQuery); + assertEquals(plan, query.statement()); + assertNull(query.params()); + assertNotNull(queryObject.get("args")); + assertEquals(1, queryObject.getArray("args").size()); + assertEquals("test", queryObject.getArray("args").getString(0)); + + params.add(QueryParams.build()); + query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + assertNotNull(query.params()); + } + + @Test + public void testEmptyArgumentsShouldProduceSimpleQuery() throws Exception { + Statement st = select("*"); + List params = Collections.emptyList(); + + Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof SimpleQuery); + assertEquals(st.toString(), query.statement().toString()); + assertNull(query.params()); + assertFalse(queryObject.containsKey("args")); + } + + @Test + public void testOnlyQueryParamsShouldProduceSimpleQuery() { + Statement st = select("*"); + QueryParams queryParams = QueryParams.build().scanWait(1, TimeUnit.DAYS); + List params = new ArrayList(1); + params.add(queryParams); + + Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof SimpleQuery); + assertEquals(st.toString(), query.statement().toString()); + assertEquals(queryParams, query.params()); + assertFalse(queryObject.containsKey("args")); + } + + @Test + public void testSimpleArgumentsShouldProduceParametrizedQuery() throws Exception { + Statement st = select("*"); + List params = new ArrayList(2); + params.add(123L); + params.add("test"); + + Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof ParametrizedQuery); + assertEquals(st.toString(), query.statement().toString()); + assertNull(query.params()); + assertTrue(queryObject.containsKey("args")); + JsonArray args = queryObject.getArray("args"); + assertEquals(2, args.size()); + assertEquals(123L, args.get(0)); + assertEquals("test", args.get(1)); + } + + @Test + public void testSimpleArgumentsAndQueryParamsShouldProduceParametrizedQuery() throws Exception { + Statement st = select("*"); + QueryParams queryParams = QueryParams.build().withContextId("toto"); + List params = new ArrayList(2); + params.add(123L); + params.add(queryParams); + params.add("test"); + + Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof ParametrizedQuery); + assertEquals(st.toString(), query.statement().toString()); + assertEquals(queryParams, query.params()); + assertTrue(queryObject.containsKey("args")); + JsonArray args = queryObject.getArray("args"); + assertEquals(2, args.size()); + assertEquals(123L, args.get(0)); + assertEquals("test", args.get(1)); + } + + @Test + public void shouldChooseCollectionExecutionWhenCollectionType() { + CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); + Query query = Mockito.mock(Query.class); + AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); + when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + .thenCallRealMethod(); + when(queryMethod.isCollectionQuery()).thenReturn(true); + + mock.executeDependingOnType(query, queryMethod, false, false, false); + verify(mock).executeCollection(any(Query.class)); + verify(mock, never()).executeEntity(any(Query.class)); + verify(mock, never()).executeStream(any(Query.class)); + } + + @Test + public void shouldChooseEntityExecutionWhenEntityType() { + CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); + Query query = Mockito.mock(Query.class); + AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); + when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + .thenCallRealMethod(); + when(queryMethod.isQueryForEntity()).thenReturn(true); + + mock.executeDependingOnType(query, queryMethod, false, false, false); + verify(mock, never()).executeCollection(any(Query.class)); + verify(mock).executeEntity(any(Query.class)); + verify(mock, never()).executeStream(any(Query.class)); + } + + @Test + public void shouldChooseStreamExecutionWhenStreamType() { + CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); + Query query = Mockito.mock(Query.class); + AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); + when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + .thenCallRealMethod(); + when(queryMethod.isStreamQuery()).thenReturn(true); + + mock.executeDependingOnType(query, queryMethod, false, false, false); + verify(mock, never()).executeCollection(any(Query.class)); + verify(mock, never()).executeEntity(any(Query.class)); + verify(mock).executeStream(any(Query.class)); + } + + @Test + public void shouldThrowWhenUnsupportedType() throws NoSuchMethodException { + CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); + Query query = Mockito.mock(Query.class); + AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); + when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + .thenCallRealMethod(); + + try { mock.executeDependingOnType(query, queryMethod, true, false, false); } catch (UnsupportedOperationException e) { } + try { mock.executeDependingOnType(query, queryMethod, false, true, false); } catch (UnsupportedOperationException e) { } + try { mock.executeDependingOnType(query, queryMethod, false, false, true); } catch (UnsupportedOperationException e) { } + verify(mock, never()).executeCollection(any(Query.class)); + verify(mock, never()).executeEntity(any(Query.class)); + verify(mock, never()).executeStream(any(Query.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java new file mode 100644 index 00000000..ef1a9a23 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java @@ -0,0 +1,38 @@ +package org.springframework.data.couchbase.repository.query; + +import static org.junit.Assert.*; +import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.PLACEHOLDER_BUCKET; +import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.PLACEHOLDER_ENTITY; +import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.PLACEHOLDER_SELECT_FROM; + +import com.couchbase.client.java.query.Statement; +import org.junit.Test; + +public class StringN1QlBasedQueryTest { + + @Test + public void testReplaceFullSelectPlaceholderOnce() throws Exception { + String statement = PLACEHOLDER_SELECT_FROM + " where " + PLACEHOLDER_SELECT_FROM; + Statement parsed = StringN1qlBasedQuery.prepare(statement, "B"); + + assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B` where " + + PLACEHOLDER_SELECT_FROM, parsed.toString()); + } + + @Test + public void testReplaceAllBucketPlaceholder() throws Exception { + String statement = "SELECT * FROM " + PLACEHOLDER_BUCKET + " WHERE " + PLACEHOLDER_BUCKET + ".test = 1"; + Statement parsed = StringN1qlBasedQuery.prepare(statement, "B"); + + assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed.toString()); + } + + @Test + public void testReplaceFirstEntityPlaceholder() throws Exception { + String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b where b.test = 1 and " + PLACEHOLDER_ENTITY; + Statement parsed = StringN1qlBasedQuery.prepare(statement, "A"); + + assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b where b.test = 1 and " + + PLACEHOLDER_ENTITY, parsed.toString()); + } +} \ No newline at end of file