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